Skip to content

v1.7 — Admin Evolution

Promote the standalone admin dashboard from "MVP that ships" to a multi-role platform with throttled access, a self-health probe, role-aware nav, GDPR rails, and end-to-end observability.

Summary

The standalone admin dashboard (shipped in v1.3) was deliberately minimal — a single super-admin role, no rate limiting, no observability, and no role-based UI. v1.7 evolved it into a platform suitable for delegating support, moderation, and review work to non-engineers.

The milestone introduced a real role system (super_admin, admin, moderator, support) backed by an authorize_admin_role! helper, Relay-style cursor pagination across six admin queries, Rack::Attack throttles per admin identity, a me and pendingCounts query so the UI can hide queues a role cannot access, a system_health probe surfacing four backend dependencies, Sentry + PostHog wired into the admin frontend with surface-tagged events, and a route-driven nav with breadcrumbs, environment badges, role badges, and PermissionGuard wrappers on destructive buttons.

It shipped on its own branch (gsd/v1.7-admin-evolution) and landed in master on 2026-04-18.

Goal

Take the v1.3 admin dashboard MVP and harden it into a delegatable platform: roles, pagination, rate limiting, self-health, observability, and role-aware UI — so support and moderation work can be handed off without expanding super-admin access.

Scope — What Shipped

Backend (Phases 26–28)

  • Pagination & searchpg_trgm GIN indexes on email, username, and first_name||last_name; six admin queries (users, searchUsers, adminActions, moderationQueue, aiEmployees, pendingArtifacts) converted to Relay connections with max_page_size: 200 clamping; Admin::UserSearch rewritten on similarity().
  • Rate limitingrack-attack ~> 6.7 integrated with admin-identity throttles (120 queries/min, 30 mutations/min per admin.public_id), using GraphqlOperationInspector.classify to distinguish queries from mutations and AdminIdentityResolver.call for identity. 429 response shape { error, retry_after }.
  • CachingAdminStatsService.call wrapped in Rails.cache.fetch with key admin_stats:v1:{days_back} and 10-minute TTL.
  • Rolesauthorize_admin_role!(role) helper added to the Authorization concern; me query returns currentUser; pendingCounts returns role-scoped queue counts (returns nil for queues the caller's role cannot see, so the UI hides the badge).
  • System healthSystemHealthService dispatches probes against DB, Redis, Sidekiq, and LiteLLM (each swallows errors → false, never raises); SystemHealthType exposes booleans + deploy_revision/deploy_timestamp metadata; system_health GraphQL field gated by require_admin!.
  • GDPR railsgdpr_requests table introduced with role-scoped intake/processing flow; pending_counts.gdpr_count probes table_exists? and returns 0 until the table lands.

Frontend (Phases 26–31)

  • Apollo migration to connectionsadmin_queries.ts, ai_workforce_queries.ts, dashboard_queries.ts, user_queries.ts rewritten to edges/node/cursor/pageInfo; all 5 admin views (Dashboard, UsersView, AIWorkforceView, ModerationView, ReviewQueueView) updated to map edges.map(e => e.node).
  • Observability@sentry/vue@^10.45.0 and posthog-js@^1.364.7 installed; initAdminSentry (50% traces, 100% replay on error, surface:admin tag) and initAdminPostHog (surface:admin super-property) wired into main.ts; App.vue watches [isSignedIn, user] to set/clear identity on Sentry + PostHog; ErrorBoundary.vue captures uncaught errors and shows a fallback card with copyable Sentry event ID.
  • Apollo error link — added with Sentry forwarding and PII scrubbing on outgoing payloads.
  • Role-aware shelluseRoles composable exposes reactive roles, hasRole, canPerform, isSuperAdmin, isLoading; ACTION_REQUIREMENTS maps 10 canonical actions to required roles; RoleBadge, EnvironmentBadge, AppBreadcrumbs, PermissionGuard (slot wrapper that disables children with pointer-events-none + tooltip when canPerform is false), and UnauthorizedView shipped.
  • Router guardrouter.beforeEach runs apolloClient.query(ME_QUERY) cache-first; redirects to /unauthorized?attempted=<fullPath> on role mismatch; falls back to /sign-in on query failure (expired Clerk session).
  • Self-health view/system/self-health route renders a 10-probe grid (5 frontend probes: build SHA, deploy timestamp, Sentry, PostHog, Clerk session; 5 backend probes: DB, Redis, Sidekiq, LiteLLM, GraphQL latency client-measured) with refresh button, PASS/WARN/FAIL pills, and a footer showing truncated public_id + role pills.
  • Action telemetryAdmin::Logged interaction emits admin_action_logged PostHog events via PosthogNotifier.

UI Polish

  • Topbar layout: AppBreadcrumbs | spacer | EnvironmentBadge | RoleBadge | UserButton.
  • Sidebar nav filtered by useRoles so a moderator sees only permitted items.
  • Unread count badges on moderation + review queue with 99+ cap and zero-hide; PENDING_COUNTS_QUERY polled every 30s, stopped on unmount.
  • Destructive buttons (Edit Roles, Save Roles, Approve Artifact, Reject Artifact, Hide Content, Dismiss Flag) wrapped in PermissionGuard with role-specific tooltips.

Phases

PhaseNameStatusHighlights
26Pagination, indexes, rate limiting, observability scaffoldingShippedpg_trgm, Relay connections, Rack::Attack, Sentry+PostHog wired
27Roles + role-aware shellShippedauthorize_admin_role!, me, pendingCounts, useRoles, PermissionGuard
28GDPR intakeShippedgdpr_requests table, role-scoped queue, frontend intake
29System health probeShippedSystemHealthService + /system/self-health 10-probe grid
30Apollo error link + admin telemetryShippedPII-scrubbed errors, admin_action_logged events
31Final polish + cross-cutsShippedbreadcrumbs, environment badges, polished filters

Key Decisions

  • Roles via a separate admin_roles table, not a column on User. Lets a single user hold multiple roles and lets us add new roles without migrations to users. User#has_role? proxies through the join.
  • pendingCounts returns nil for inaccessible queues, not 0. The UI hides the badge entirely; rendering 0 for a queue the caller cannot see is misleading.
  • PostHog surface:admin super-property. Lets PostHog separate admin behavior from user behavior in the same project without two PostHog instances.
  • Frontend role guard is cache-first, server is authoritative. useRoles short-circuits the UI; authorize_admin_role! on the backend is the only thing that actually denies an action.
  • Self-health probes swallow exceptions. A failing probe returns false; the page renders for the operator instead of crashing on an upstream outage — exactly the moment the page is most needed.

Outcomes

Support and moderation work can now be delegated to non-engineers. Admin sessions are throttled per identity, paginated, and observable in Sentry + PostHog. The self-health view is the operator's first stop on every alert page. The dashboard ships under feature parity for v1.3's super-admin surface while opening four roles' worth of new delegatable surface.

Tech Debt

  • GDPR processing UI ships in v1.7 with a manual operator workflow — automated export pipelines are deferred to a future milestone.
  • Sentry tracesSampleRate is 50% in admin (vs. 10% in app) — appropriate for low-traffic admin sessions but flagged for cost review.
  • 4df5a6c0 — Gsd/v1.7 admin evolution (#329)
  • 4fc9d6df — fix(admin_action_type): realign with v1.7 model (unblocks Dashboard + demo actions queries) (#336)

Last updated: 2026-05-22

Loading…