---
name: himetrica-live-now
description: Add an "Online now" widget to an admin panel using Himetrica realtime data - live visitor count, today's peak and daily active users, plus the identified users active in the last few minutes. Use when the user wants realtime/live visitors, who is online, active users right now, or a live presence widget in their admin panel.
---

# Himetrica "Online now"

Goal: a small widget on the admin home that answers "who is using the product right now".

Requires `himetrica-install` on the product (identified users need `identify`).

## 0. Gather

- Env (server only): `HIMETRICA_SECRET_KEY` (`hm_sk_...`) and `HIMETRICA_PROJECT_ID`
  (`GET https://app.himetrica.com/api/v1/me` returns it).
- Where the widget goes (usually next to the KPI header), the UI kit, and the admin auth
  guard used by the panel's server routes.
- How the admin links to a user's page (by id or email), to make names clickable.

## 1. Server client (create once, reuse if another himetrica-* skill already made it)

The secret key must only ever run on the server. If the app has no server (pure SPA on
Supabase/Firebase), put this in a serverless/edge function (Supabase Edge Function,
Vercel/Netlify function) and call that from the SPA. Never `NEXT_PUBLIC_`/`VITE_` it.

```ts
// lib/himetrica.server.ts  (server-only; in Next.js add: import "server-only";)
const BASE_URL = "https://app.himetrica.com/api/v1";
type Params = Record<string, string | number | boolean | undefined>;
const cache = new Map<string, { at: number; data: unknown }>();

export class HimetricaError extends Error {
  constructor(public status: number, public code: string, message: string) {
    super(message);
  }
}

export async function himetrica<T>(path: string, params: Params = {}, ttlSeconds = 60): Promise<T> {
  const key = process.env.HIMETRICA_SECRET_KEY;
  const projectId = process.env.HIMETRICA_PROJECT_ID;
  if (!key || !projectId) throw new HimetricaError(500, "NOT_CONFIGURED", "Missing HIMETRICA_SECRET_KEY or HIMETRICA_PROJECT_ID");

  const url = new URL(`${BASE_URL}/projects/${projectId}${path}`);
  for (const [k, v] of Object.entries(params)) if (v !== undefined) url.searchParams.set(k, String(v));

  const hit = cache.get(url.href);
  if (hit && Date.now() - hit.at < ttlSeconds * 1000) return hit.data as T;

  const res = await fetch(url, { headers: { "X-API-Key": key }, cache: "no-store" });
  const body = await res.json().catch(() => ({}));
  if (!res.ok) throw new HimetricaError(res.status, body.code ?? "HTTP_ERROR", body.error ?? res.statusText);

  if (cache.size > 500) cache.clear();
  cache.set(url.href, { at: Date.now(), data: body.data });
  return body.data as T;
}
```

(Deno/Supabase Edge: `Deno.env.get(...)` instead of `process.env`.)

## 2. Endpoints

`GET /analytics/realtime` →
`{ activeVisitors: number; peakToday: number; dailyActiveUsers: number }`

Identified people active recently (optional second half of the widget):
`GET /visitors?identifiedOnly=true&limit=10` (default sort `lastSeenAt DESC`) →
`{ visitors: Array<{ id; name; email; lastSeenAt; country; device; totalRevenueUsd; activeSubscriptions }> }`.
Keep only `lastSeenAt` within the last 5 minutes.

Do not call `/visitors/{ref}` here (10 req/min limit).

## 3. Admin route

`GET /api/admin/himetrica/live`, behind the admin guard. Both calls in parallel with a
**15 s** server cache (helper TTL `15`), so any number of open admin tabs costs Himetrica
at most 8 requests/min. Return a slim DTO:

```ts
{
  activeVisitors: number;
  peakToday: number;
  dailyActiveUsers: number;
  people: Array<{ name: string | null; email: string; lastSeenAt: string; country: string | null; paying: boolean }>;
}
```

`paying` = `activeSubscriptions > 0`. Drop everything else from the list rows (they carry
fields the browser doesn't need). On error return the last good value if cached, else 502.

## 4. Widget

- Big number `activeVisitors` with a pulsing green dot and the label "online now"; subline
  "peak today N · N active today".
- Below, up to 5 identified people: avatar initials, name (or email), "2 min ago",
  a small paid badge when `paying`. Each links to the admin's user page for that email.
  Empty list → just hide that section.
- Poll every 30 s from the client. Pause while `document.visibilityState === "hidden"`
  and refetch on `visibilitychange` back to visible. Use the project's data-fetching lib
  (SWR / React Query `refetchInterval`) if it has one.
- Errors: keep showing the last value, dim it; never flash an error banner every 30 s.

## Done when

- [ ] Secret key server-only; route behind the admin guard.
- [ ] 15 s server cache + 30 s client polling paused on hidden tabs.
- [ ] Only the slim DTO reaches the browser.
