Skip to content

[Admin] Surface the external-vs-operator usage split in the portal — #68's metric has no UI #269

Closed

Description

@serge-ivo

The gap

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:

  • Headline: N external users over the selected range — the [Strategy] Focus bet — 10 external users on ONE runtime agent before expanding #68 metric, stated plainly.
  • 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.

Acceptance

Part of #27 (Admin/Operator Portal). Measurement side landed already; this is the surface.

Activity

  1. serge-ivo commented on Aug 6, 2026

    @serge-ivo
    ContributorAuthor

    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:

    1. ADMIN_ALLOWLIST is server-only and invisible to the client, so the split panel stays authoritative and the footnote says so.
    2. 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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions