Context
eb2a84d (perf(admin): move the pools traffic read off the LiveView process) fixed the /admin/pools responsiveness problem: the multi-aggregate traffic read (~400 ms cold at the 24h window, ~13 s cold at 7d in a loaded deployment) used to run synchronously inside the LiveView process and was re-triggered by gateway usage events, so every click queued behind it. The read now runs through start_async with coalescing (one in-flight task, re-run flag, stale-window discard), structural data stays synchronous, and the dead token_usage_weekly scan was deleted.
Three follow-ups were deliberately deferred from that change. They are polish, not correctness: none of them is needed to keep the page responsive.
Progress
1. Connected-only fallback refresh tick
Status: completed in sub-issue 253 by 46bd71cd
Traffic metrics originally refreshed only on triggers: gateway usage events (1 s debounce → async task), pool/upstream lifecycle events, or operator actions. On a quiet instance nothing fired, so rolling-window numbers ("Requests 24h") slowly went stale — requests aged out of the window but the display kept the last computed totals. A crashed traffic task on a quiet instance also stayed at the "…" loading affordance until the next trigger.
The implementation mirrors maybe_start_connected_refresh from lib/codex_pooler_web/live/admin/pages/jobs_live.ex: a connected-only 60-second Process.send_after loop calls the existing coalesced Pool traffic loader and routes timer refreshes through the global live-update pause gate.
2. Compute per-pool traffic histograms lazily
Status: open in sub-issue 254
Every pool card eagerly builds and ships a bucketed traffic histogram (lib/codex_pooler_web/live/admin/components/pages/pools/list_components.ex, chart hook payload) on every merge, for every pool, whether or not the card is in view. Cheap at 3 pools; wasteful render CPU + WebSocket payload at tens of pools.
Proposal: compute/send histogram data only when a card is expanded or enters the viewport. Touches the stable chart selectors (ApexTimeSeriesChart, phx-update="ignore") that tests target, so it needs care. No urgency until pool counts per instance grow.
3. Back the 7d window with daily rollups
Status: open in sub-issue 255
The 7d window still aggregates raw requests/attempts/ledger_entries rows over 7 days (~13 s cold). Since the read is async this no longer freezes the page — but the operator watches the "…" placeholder for up to ~13 s after selecting 7d.
Proposal: rebuild the 7d path on the existing daily rollups (lib/codex_pooler/accounting/schemas/daily_rollup.ex, lib/codex_pooler/accounting/usage/rollups.ex) the way the Observatory reads already do. Non-trivial parts:
- reconcile per-UTC-day rollups with the still-hot current day (rollups + today's raw tail) under the same exclusion rules (
usage_unknown, settled-only cost);
- degrade gracefully when rollups are late or partial (fresh deploys, backfill in progress) — the raw path should remain the fallback;
- dedicated tests for window boundaries at day edges.
This is the highest-value remaining item: it is the only one with operator-visible impact today.
Related: other admin pages sharing the sync-read pattern
A code audit of every admin page (mount cost + event triggers + handle_info behavior) found the same disease — expensive read on the LiveView process re-triggered by hot events — on four more pages. The eb2a84d1 split (cheap structural read synchronous, expensive read via start_async with one-in-flight/re-run/stale-discard coalescing) applied to each, with page-specific constraints.
| Page |
Verdict |
Notes |
/admin/upstreams |
complete |
Tracked in sub-issue 256 and implemented in 5c25567c. Upload/dialog assigns and account snapshots are preserved across async merges. |
/admin/upstreams/:id (cockpit) |
complete |
Tracked in sub-issue 257. Request health and Pool contribution run asynchronously with bounded SQL aggregation in cd174f43, 6634a600, and fc128cfd. |
/admin/stats |
complete |
Tracked in sub-issue 258. Dashboard builds are generation-tagged, stale-safe, and coalesced in 6e99f0ad and fc128cfd. |
/admin/request-logs |
complete |
Tracked in sub-issue 259. COUNT, page rows, attempts, turns, and details load asynchronously with filter generations in 203a3a05 and fc128cfd. |
/admin/jobs |
watch |
Reloads are debounced and oban_jobs-bounded (24h retention), traffic-independent; residual risk is the 5s timer polling a moderately heavy sync read. Cheap hardening: scope the subscription to job_status. |
/admin/alerts |
watch |
No hot trigger; risk is unbounded incident history making first paint slow. Fix is query-side (SQL LIMIT/COUNT instead of load-all + Enum.take), not async. |
/admin/invites |
watch |
Hot reason (upstream_quota_windows_updated) rides the upstreams topic it legitimately needs; reason-filter the reload + scoped subscription + short coalescing timer. Read itself is cheap. |
/admin/api-keys |
fine |
Already the template: topic-scoped subscription, accounting reads removed. Latent N+1 per pool if instances reach 100+ visible pools (batch with IN queries). |
/admin/audit-logs |
fine |
No event-triggered reloads. Minor: unbounded total COUNT lacks a plain occurred_at index if audit volume grows. |
Remaining implementation order: rollup-backed 7d window → lazy per-Pool histograms. The watch items remain opportunistic hardening.
Context
eb2a84d (
perf(admin): move the pools traffic read off the LiveView process) fixed the/admin/poolsresponsiveness problem: the multi-aggregate traffic read (~400 ms cold at the 24h window, ~13 s cold at 7d in a loaded deployment) used to run synchronously inside the LiveView process and was re-triggered by gateway usage events, so every click queued behind it. The read now runs throughstart_asyncwith coalescing (one in-flight task, re-run flag, stale-window discard), structural data stays synchronous, and the deadtoken_usage_weeklyscan was deleted.Three follow-ups were deliberately deferred from that change. They are polish, not correctness: none of them is needed to keep the page responsive.
Progress
/admin/upstreamsaccount reloads off the LiveView process/admin/upstreams/:idrequest metrics asynchronously in SQL/admin/statsdashboard data asynchronously/admin/request-logsfiltered data asynchronously1. Connected-only fallback refresh tick
Status: completed in sub-issue 253 by
46bd71cdTraffic metrics originally refreshed only on triggers: gateway
usageevents (1 s debounce → async task), pool/upstream lifecycle events, or operator actions. On a quiet instance nothing fired, so rolling-window numbers ("Requests 24h") slowly went stale — requests aged out of the window but the display kept the last computed totals. A crashed traffic task on a quiet instance also stayed at the "…" loading affordance until the next trigger.The implementation mirrors
maybe_start_connected_refreshfromlib/codex_pooler_web/live/admin/pages/jobs_live.ex: a connected-only 60-secondProcess.send_afterloop calls the existing coalesced Pool traffic loader and routes timer refreshes through the global live-update pause gate.2. Compute per-pool traffic histograms lazily
Status: open in sub-issue 254
Every pool card eagerly builds and ships a bucketed traffic histogram (
lib/codex_pooler_web/live/admin/components/pages/pools/list_components.ex, chart hook payload) on every merge, for every pool, whether or not the card is in view. Cheap at 3 pools; wasteful render CPU + WebSocket payload at tens of pools.Proposal: compute/send histogram data only when a card is expanded or enters the viewport. Touches the stable chart selectors (
ApexTimeSeriesChart,phx-update="ignore") that tests target, so it needs care. No urgency until pool counts per instance grow.3. Back the 7d window with daily rollups
Status: open in sub-issue 255
The 7d window still aggregates raw
requests/attempts/ledger_entriesrows over 7 days (~13 s cold). Since the read is async this no longer freezes the page — but the operator watches the "…" placeholder for up to ~13 s after selecting 7d.Proposal: rebuild the 7d path on the existing daily rollups (
lib/codex_pooler/accounting/schemas/daily_rollup.ex,lib/codex_pooler/accounting/usage/rollups.ex) the way the Observatory reads already do. Non-trivial parts:usage_unknown, settled-only cost);This is the highest-value remaining item: it is the only one with operator-visible impact today.
Related: other admin pages sharing the sync-read pattern
A code audit of every admin page (mount cost + event triggers +
handle_infobehavior) found the same disease — expensive read on the LiveView process re-triggered by hot events — on four more pages. Theeb2a84d1split (cheap structural read synchronous, expensive read viastart_asyncwith one-in-flight/re-run/stale-discard coalescing) applied to each, with page-specific constraints./admin/upstreams5c25567c. Upload/dialog assigns and account snapshots are preserved across async merges./admin/upstreams/:id(cockpit)cd174f43,6634a600, andfc128cfd./admin/stats6e99f0adandfc128cfd./admin/request-logs203a3a05andfc128cfd./admin/jobsoban_jobs-bounded (24h retention), traffic-independent; residual risk is the 5s timer polling a moderately heavy sync read. Cheap hardening: scope the subscription tojob_status./admin/alertsEnum.take), not async./admin/invitesupstream_quota_windows_updated) rides theupstreamstopic it legitimately needs; reason-filter the reload + scoped subscription + short coalescing timer. Read itself is cheap./admin/api-keysINqueries)./admin/audit-logsoccurred_atindex if audit volume grows.Remaining implementation order: rollup-backed 7d window → lazy per-Pool histograms. The watch items remain opportunistic hardening.