You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
[Admin] Surface the external-vs-operator usage split in the portal — #68's metric has no UI #269
GET /v1/admin/usage/external was built for #68 — it answers the one question the focus bet is written in ("how many EXTERNAL users, on which agent"), splitting the ledger into operator vs everyone else. It has no UI. The only way to read the number the roadmap is prioritised against is to curl it.
store/admin/src/pages/Usage.tsx today renders byProvider / byModel / byKind / byUser / byAgent — all "what was spent", none "by whom, and are they us". The distinction is invisible: byUser lists opaque user ids with no operator flag, so a glance at that table cannot tell you whether the platform has any external users at all.
Build
Add an External vs operator panel at the top of the admin Usage page:
Per-agent table from byAgent: external users · calls · cost, sorted by external users (the endpoint already sorts this way).
Operator activity shown alongside rather than subtracted away, so the comparison is visible instead of implied.
operatorUnknown: true must render as "unknown", never as a number. When no operator can be identified every row looks external and the count is meaningless, not encouraging. This is the specific failure mode the endpoint was designed around — a UI that renders the raw count anyway defeats it.
Flag operator rows in the existing byUser table too, so the two panels can't disagree.
operatorUnknown renders as an explicit "cannot determine" state.
Zero external users renders as a clear, unambiguous zero — that is currently the true answer and it should be legible, not hidden behind an empty table.
Part of #27 (Admin/Operator Portal). Measurement side landed already; this is the surface.
Shipped in 32f1b37 (feat(admin): surface the external-vs-operator usage split).
An External vs operator panel now leads store/admin/src/pages/Usage.tsx:
Headline external-user count for the selected range.
Operator activity shown alongside (two cards, users · calls · cost) rather than netted out.
Per-agent table in the endpoint's own order — external users desc, then calls — preserved rather than re-sorted client-side.
operatorUnknown renders as "Cannot determine" with no counts at all, plus the two fixes that resolve it (admin role, ADMIN_ALLOWLIST). Showing the raw count there would turn the operator's own traffic into a falsely encouraging "we have users", which is the reading this endpoint exists to prevent.
Zero external users gets a legible zero and a sentence saying it is a measured zero, not a missing measurement.
Operator rows flagged in By user, derived from users.roles (the same live source operatorUserIds consults) plus the caller.
Two limits, stated in the UI rather than papered over:
ADMIN_ALLOWLIST is server-only and invisible to the client, so the split panel stays authoritative and the footnote says so.
If /v1/admin/users is truncated (total > returned), nothing is flagged — a half-flagged table is worse, because an unflagged operator row reads as a confirmed external user.
Also: the range control is a range id (7d/30d/90d/all) while the endpoint takes a rolling day count clamped to 365, so all is labelled "(max)" rather than quietly meaning something narrower than the rest of the page.
One finding worth recording: since the caller-uid fix, requireAdmin always returns a session and passes session.uid, so operatorUnknown can no longer fire through this route — the session?.uid optional chaining at routes/admin.ts:141 is now vestigial. The state is still rendered as first-class regardless: it is part of the response contract, and the UI must not be the reason a future regression reads as good news.
Typecheck clean (store/admin has no typecheck script so it is not covered by pnpm typecheck — verified separately with tsc -b, exit 0). Biome identical to baseline. 3380 tests pass. No UI test added: neither store app has test infra (no vitest/testing-library), so adding it was out of scope for this change.
Activity
feat(admin): surface the external-vs-operator usage split (#269)
serge-ivo commentedon Aug 6, 2026
Shipped in 32f1b37 (
feat(admin): surface the external-vs-operator usage split).An External vs operator panel now leads
store/admin/src/pages/Usage.tsx:operatorUnknownrenders as "Cannot determine" with no counts at all, plus the two fixes that resolve it (admin role,ADMIN_ALLOWLIST). Showing the raw count there would turn the operator's own traffic into a falsely encouraging "we have users", which is the reading this endpoint exists to prevent.users.roles(the same live sourceoperatorUserIdsconsults) plus the caller.Two limits, stated in the UI rather than papered over:
ADMIN_ALLOWLISTis server-only and invisible to the client, so the split panel stays authoritative and the footnote says so./v1/admin/usersis truncated (total > returned), nothing is flagged — a half-flagged table is worse, because an unflagged operator row reads as a confirmed external user.Also: the range control is a range id (
7d/30d/90d/all) while the endpoint takes a rolling day count clamped to 365, soallis labelled "(max)" rather than quietly meaning something narrower than the rest of the page.One finding worth recording: since the caller-uid fix,
requireAdminalways returns a session and passessession.uid, sooperatorUnknowncan no longer fire through this route — thesession?.uidoptional chaining atroutes/admin.ts:141is now vestigial. The state is still rendered as first-class regardless: it is part of the response contract, and the UI must not be the reason a future regression reads as good news.Typecheck clean (
store/adminhas notypecheckscript so it is not covered bypnpm typecheck— verified separately withtsc -b, exit 0). Biome identical to baseline. 3380 tests pass. No UI test added: neither store app has test infra (no vitest/testing-library), so adding it was out of scope for this change.