Skip to content

feat(admin): Operators page + readable org names (console clarity)#963

Merged
fuziontech merged 3 commits into
mainfrom
james/admin-operators-page
Jul 16, 2026
Merged

feat(admin): Operators page + readable org names (console clarity)#963
fuziontech merged 3 commits into
mainfrom
james/admin-operators-page

Conversation

@fuziontech

Copy link
Copy Markdown
Member

What

Adds a dedicated admin-only Operators page to the control-plane admin console for managing who can sign in to the console and at what role (admin / viewer), over the existing /api/v1/operators API — which had a full backend + API client + React-Query hooks but no page. Until now the first admin had to be minted by hand (break-glass internal-secret → curl the operators API); there was no in-console way to see or manage operators.

Why the two "users" concepts were confusing

The console had a page called Users that manages org database login accounts (what customers use to connect to their warehouse). "Operators" (console access) is a completely different thing, and both were reasonably called "users." This PR makes the distinction unmissable:

Page Route What it is
Org Users (renamed from "Users") /users Per-org database login accounts (PG wire / Flight SQL) customers connect with
Operators (new) /operators PostHog staff who can sign in to this admin console

Both pages now have explicit descriptions, an info note, and a reciprocal cross-link, so it's always clear which "users" you're looking at.

Changes

Frontend

  • New pages/Operators.tsx: list operators (email, role, granted-by, updated), add operator, change role, revoke — admin-gated, with the signed-in user tagged you and self-lockout warnings when demoting/revoking your own admin.
  • /operators route + nav entry (admin-only, ShieldCheck icon).
  • useOperators now gates on isAdmin + tolerates 403/404 (mirrors useDucklingDrift), so a viewer sees an "admin only" notice instead of an error.
  • Users.tsx → titled Org Users, sharpened description, cross-link to Operators.

Backend (no behavior change)

  • Narrowed the operators handler to a small operatorStore interface (satisfied by *configstore.ConfigStore), mirroring the existing apiStore pattern, so the handler — including the last-admin lockout guard — is unit-testable.

Tests

  • operators_api_test.go (new): list / upsert / validation (empty, wrong domain, bad role) / delete / viewer-403, and the last-admin guard (409 on demoting or deleting the final admin — the lockout-prevention path the harness can't safely exercise on a shared stack).
  • Operators.test.tsx (new): admin sees the list + affordances + "you" tag + Org-Users cross-link; viewer sees the admin-only notice (no list, no add).
  • admin_operators harness assertion: grant → list → role-change → validation → revoke round-trip on a throwaway @posthog.com probe (self-cleaning, isolated). The operators API previously had zero e2e coverage.

Docs

  • CLAUDE.md admin-console section updated for the new page, the Org-Users/Operators distinction, the last-admin guard, and the test list.

Verification

  • npm run typecheck ✅ · npm run test ✅ (69/69) · npm run build ✅ · npm run lint (no new warnings) ✅
  • go build -tags kubernetes ./... ✅ · go vet -tags kubernetes ./controlplane/admin/
  • go test -tags kubernetes ./controlplane/admin/ — operators tests ✅ (only pre-existing *_postgres_test.go fail locally: they need docker-compose, which isn't on this box)
  • bash -n tests/mw-dev/e2e/harness.sh

The full admin_operators e2e assertion runs against the real mw-dev cluster in the per-PR e2e-mw-dev workflow.

Screenshot

UI change — reviewer can pull the branch and just ui-dev to view; the Operators page follows the exact layout/idiom of the existing Users page (DataTable + header + dialogs).

🤖 Generated with Claude Code

Adds a dedicated admin-only "Operators" page to the control-plane admin
console for managing who can sign in and at what role (admin/viewer),
over the existing but previously UI-less /api/v1/operators API. Until
now the first admin had to be minted by hand via the break-glass token.

Disambiguates the two "users" concepts that were easy to confuse:
- "Org Users" (renamed from "Users") — per-org *database* login accounts
  customers use to connect to their warehouse (PG wire / Flight SQL).
- "Operators" (new) — PostHog staff who administer this console.
Both pages now carry explicit descriptions and a reciprocal cross-link
so "users" is never ambiguous.

Backend: narrow the operators handler to an operatorStore interface so
the last-admin lockout guard is unit-testable (operators_api_test.go),
mirroring the existing apiStore pattern. No behavior change.

Frontend: new Operators.tsx (list / add / change-role / revoke,
admin-gated, self-lockout warnings); route + nav entry; and gate
useOperators on isAdmin (403-tolerant) like useDucklingDrift. Vitest
coverage in Operators.test.tsx.

e2e: admin_operators harness assertion — grant / list / role-change /
validation / revoke round-trip on a throwaway probe (the SPA can't be
driven in-Job; the last-admin guard is unit-tested).

Docs: CLAUDE.md admin section updated for the new page + the distinction.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Jul 16, 2026

Copy link
Copy Markdown

Test Impact Plan

Deterministic summary of how this PR changes tests, CI runners, and coverage-risk signals.

Summary

Area Added Changed Deleted
Test files 1 1 0
E2E/journey files 0 0 0
Workflow files 0 0 0

Signals

  • Test cases: +8 / -0
  • Assertions: +31 / -0
  • Skips or known failures added: 0
  • Workflow continue-on-error added: 0
  • Workflow path filters added: 0
  • Test commands removed from justfile: 0
  • E2E/journey retry lines added: 0

Coverage risk: neutral or increased

No coverage-reduction warnings detected.

The impersonation org selector, the "you are impersonating" banner, and
the write-confirm dialog all showed only org UUIDs, which are impossible
to tell apart at a glance. Show the human-readable label (database name →
hostname alias → UUID) as the primary identifier, with the UUID as muted
secondary context, and sort the picker by label.

Extract the readable-name resolution (previously inlined in Overview)
into a shared `orgLabel()` helper in lib/format, unit-tested in
format.test.ts and reused by Overview so there's one source of truth.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fuziontech fuziontech changed the title feat(admin): Operators page to manage console access + roles feat(admin): Operators page + readable org names (console clarity) Jul 16, 2026
@fuziontech
fuziontech requested a review from a team July 16, 2026 17:15
Addresses the Fable review of the operators page (and a real e2e failure):

- e2e admin_operators tripped the last-admin guard: the probe was promoted
  to admin then deleted, but this shared stack has no other admin operators,
  so the probe became the SOLE admin and DELETE correctly 409'd — and the row
  was then unremovable by design, wedging future runs. Keep the probe
  viewer-only end-to-end (viewers are always deletable) and use a fresh probe
  email. The admin / role-change / last-admin-guard paths stay covered by
  operators_api_test.go.
- Operators revoke dialog wording: revoke does NOT block sign-in (a fresh
  @posthog.com SSO login re-provisions the user as a read-only viewer), so its
  real effect is dropping an admin back to viewer. Say that accurately instead
  of "they can no longer sign in".
- operators_api_test.go: assert viewer GET /operators -> 403, completing the
  RBAC coverage across all three routes.
- CLAUDE.md: sweep the stale tests/e2e-mw-dev/harness.sh path to the current
  tests/mw-dev/e2e/harness.sh.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@fuziontech
fuziontech merged commit 7adde4d into main Jul 16, 2026
27 checks passed
@fuziontech
fuziontech deleted the james/admin-operators-page branch July 16, 2026 17:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant