Real-time dashboard for AI usage analytics

Real-time dashboard for AI usage analytics

With the sync engine pulling usage data from every account into a single database, the next step was making that data useful without requiring SQL queries. The client needed a dashboard — something they could open during the day and see what's happening across all accounts in real time: who's using what, how much it's costing, and where the money is going. But beyond simple monitoring, they also needed to tie usage back to specific client projects for billing purposes. The AI coding tool doesn't know which project you're working on — it just logs that you made a request. Connecting those requests to billable work was the real challenge.

overview and performance

The dashboard opens to a multi-account overview: each account shows the associated email, current billing period, total spend so far, and a breakdown by model. Below that, a set of charts visualizes usage trends over time — daily cost curves, model distribution pie charts, and a stacked bar view showing how spending shifts between fast and premium models throughout the week. Charts are built with Recharts and backed by Zustand for client-side state management. The data loads fast on first paint because the heavy aggregation happens server-side: Prisma queries pull pre-computed summaries from materialized views in PostgreSQL, not raw event tables. That optimization came later in the project — initially the dashboard queried the events table directly, and as the dataset grew past a few hundred thousand rows, page load times crept from under a second to nearly four. Materialized views for billing-period summaries brought it back down to sub-second, refreshed hourly by a cron job.

Hourly materialized view refresh

Pre-aggregated billing summaries keep first paint fast once event tables grew past hundreds of thousands of rows.

real-time layer

The real-time component runs on Server-Sent Events. When the sync engine writes new usage events to the database, it triggers an SSE broadcast to any connected dashboard clients. The browser receives these events and updates the charts and totals without a page refresh. Getting SSE to work reliably through Nginx required specific proxy configuration that isn't obvious from the docs — you need long read timeouts, proxy buffering disabled, and the `X-Accel-Buffering: no` header to prevent Nginx from holding the response stream in memory. Without those settings, the SSE connection would silently buffer for 30-60 seconds and then dump a batch of events all at once, making the "real-time" experience feel more like a delayed replay. On the client side, SSE connections drop silently when a laptop goes to sleep or switches networks. The browser's native EventSource doesn't handle reconnection gracefully in every case, so we added a heartbeat mechanism: the server sends a ping every 15 seconds, and if the client doesn't receive one within 30 seconds, it tears down the connection and reconnects. Simple, but it eliminated the "dashboard looks frozen" reports.

15-second SSE heartbeat

Scheduled pings and 30-second client timeouts tear down and reconnect EventSource after sleep or network changes.

work sessions and access

Work session tracking was the feature that turned this from a monitoring tool into a billing tool. The idea is straightforward: group usage events into work sessions — contiguous blocks of activity separated by gaps — and let the user assign each session to a client project. In practice, defining what counts as a "session" required heuristics. We settled on a 30-minute inactivity gap as the session boundary: if there's no usage for 30 minutes, the next event starts a new session. This was tuned based on the client's actual work patterns — they typically switch projects during natural breaks (lunch, meetings), and a 30-minute gap captures that boundary reasonably well. Each session shows its duration, total cost, models used, and a request count. The user tags sessions with project names from a dropdown, and the system generates per-project cost reports at the end of each billing cycle. Retrofitting this onto data that was originally collected without any concept of sessions meant backfilling — the algorithm runs retroactively over historical events, which took some careful handling to avoid reprocessing already-tagged sessions.

Access is protected with JWT authentication, generated and validated using the jose library. The token includes the user's role, and the API layer checks permissions on every request. Currently there's a single admin role, but the structure supports adding read-only viewers or per-account scoping later. The frontend is React 18 in Next.js 14, with Zustand managing everything from the active account filter to the selected date range to the live SSE event buffer. Keeping state in Zustand rather than scattered across component state made the real-time updates significantly easier to implement — a single store update propagates to every chart and summary component that depends on it.

results

The dashboard now gives the client full visibility into their AI tool spending across every account, in real time, with the ability to trace costs back to specific client engagements. The takeaway: real-time features are deceptively expensive in complexity. SSE is simpler than WebSockets on paper, but the operational details — Nginx configuration, reconnection logic, heartbeats, buffering — add up to a non-trivial amount of infrastructure work. If the data doesn't genuinely need to be live, polling at a short interval is almost always the better choice. In this case, the client watches the dashboard during active work sessions and wants to see costs tick up as they go — real-time genuinely matters here, so the complexity was justified.

Stack

Frontend: Next.js 14, React 18, Tailwind, Recharts, Zustand

Backend: Next.js API Routes, Prisma 5, PostgreSQL (materialized views)

Real-time: Server-Sent Events (EventSource), custom heartbeat/reconnect

Auth: jose (JWT)

Infra: Nginx (SSE proxy configuration), node-cron

2026, «VOSGLOS». All rights reserved.