Skip to content

Case Study: Admin Dashboard

Background

Parcelog — logistics SaaS. Customer-facing app was fine; internal ops was a graveyard of SQL bookmarks and one Metabase board. Support (6 people) and ops (4) needed an admin: users, orgs, shipments, carriers, rate tables, refunds, audit log, feature flags, impersonation, webhook deliveries, API keys, and "stuck job" replay. 12 screens, all CRUD-ish with a few dangerous actions.

Stack: React, TanStack Query + Table, Fastify admin API mounted at /admin/v1, same Postgres. Team: two product engineers, one week calendar target for v1 admin, agents welcome.

Initial vague prompt

text
Build an admin dashboard with pages for users, orgs, shipments, carriers,
rates, refunds, audit log, feature flags, impersonation, webhooks, API
keys, and job replay. Use React and TanStack. Make it consistent.

The agent built all twelve in one session. Consistency was aspirational: four different patterns for "delete," three for pagination, impersonation as a GET link, refunds without idempotency keys.

Why it failed

Support dogfooding day:

ScreenProblem
RefundsDouble-click created two refunds — no idempotency
ImpersonationOpened in same tab via GET; prefetch/crawlers risk; no audit row
Users listClient-side filter of 10k rows; page froze
Feature flagsToggle called PATCH then showed success before server ack; refresh reverted
API keysPlaintext key shown only once — good — but key hashed with MD5 in agent code

"Make it consistent" without an exemplar is an invitation to average together every admin dashboard in the training distribution. You get twelve almosts.

The improved spec

They deleted nine screens, kept a stripped users list as a cautionary artifact, and invested in one exemplar: Orgs.

markdown
## Exemplar — Admin Orgs screen

### UX rules (apply to all future admin screens)
- List: server pagination, sort, q= search
- Mutations: TanStack mutation, disable submit until settled, toast on error
- Destructive actions: ConfirmDialog with typed org slug
- Dangerous GETs forbidden — POST with CSRF token on admin origin
- Every mutation writes audit_log via server (not client)

### API
- GET /admin/v1/orgs?cursor=&q=&sort=
- POST /admin/v1/orgs/:id/suspend — idempotency-key header required
- Response shapes in packages/admin-api-types

### Implementation map
- src/admin/pages/OrgsPage.tsx — list + filters
- src/admin/pages/OrgDetailPage.tsx — detail + actions
- src/admin/components/ConfirmDialog.tsx — shared
- src/admin/lib/adminQuery.ts — query key factory

After Orgs was reviewed and merged, the prompt for the remaining screens was not "build admin," it was "clone the Orgs pattern for Shipments — same components, same query factory, same confirm dialog."

Agent workflow

Critical rule in CLAUDE.md:

text
Admin screens must copy patterns from src/admin/pages/OrgsPage.tsx.
If a screen needs a new shared component, stop and ask — do not invent
a one-off pattern. Prefer regenerating a page over patching inconsistency.

When Carriers drifted (local state instead of query cache), they deleted CarriersPage.tsx and regenerated from the exemplar prompt. Patching drift took longer and left scars; regenerate was twenty minutes.

Mass-production prompt (abridged) used for screens 2–9:

text
Read CLAUDE.md and src/admin/pages/OrgsPage.tsx + OrgDetailPage.tsx.
Implement Shipments admin list+detail with the same query factory,
ConfirmDialog, pagination, and mutation disable patterns. Map fields
from specs/admin-shipments.md. Do not add new shared components.
Do not implement refund or replay actions on this screen.

Screens 10–12 (impersonation, refunds, job replay) each got a dedicated danger spec — not the clone prompt. Mixing "clone Orgs" with "also add impersonation" is how GET-impersonation sneaks back in.

Human intervention points

  1. Exemplar review — treated like a public API design review, not a normal feature PR. Included a support lead clicking through Orgs for 20 minutes.
  2. Conventions written into CLAUDE.md before mass production.
  3. Dangerous actions (impersonation, refunds, job replay) — human-written specs with audit requirements; agent implements.
  4. Spot-check three screens deeply rather than skim twelve — pagination, double-submit, audit rows. They picked Users (volume), Refunds (money), Impersonation (authz).
  5. Hashing/crypto — human replaced MD5 API key hashing with scrypt; agent was banned from inventing crypto. Added to CLAUDE.md after the incident.

Mistakes made

  • Mass-producing before the exemplar. Cost: ~1.5 days of throwaway UI + support trust hit ("is admin safe?").
  • Patching Carriers instead of regenerating (first instinct). Cost: 3 hours of whack-a-mole; regenerate fixed it cleanly.
  • Letting the agent design impersonation. GET-based impersonation is a classic footgun; only a human threat pass caught it. Fix: POST /admin/v1/impersonation + audit row + 1h TTL cookie.
  • Shared component invented on screen 7 (Flags). Slightly different ConfirmDialog. They later forced Flags to use the shared one; two hours of CSS and prop realignment. The "stop and ask" rule existed — the session ignored it because the human wasn't watching.
  • Cursor pagination inconsistency on Webhooks. Agent used offset pagination "because the table was small." At 200k delivery rows it melted. Regenerated against Orgs cursor pattern — another argument for regenerate-over-patch and for including scale assumptions in the exemplar spec.

Final outcome

Twelve screens in nine engineer-days after the restart (two on exemplar + conventions, seven on mass production and dangerous-action specs). Support's median time-to-resolve for "stuck shipment" dropped from ~40 minutes (Slack + SQL) to ~6 minutes. Idempotent refunds and audited impersonation shipped without incident in the first month. Admin API: ~2.4k LOC; admin UI: ~5.1k LOC; ~60% agent-typed after the restart (near 95% on the nine clone screens, near 40% on danger screens where humans wrote more).

Artifact that mattered most: OrgsPage.tsx + 40 lines in CLAUDE.md, not the twelve screens themselves. When they added screen 13 (Surcharge rules) a month later, a junior + agent finished it in half a day by cloning Orgs — the process transferred.

Lessons

  1. One exemplar screen with frozen conventions beats twelve screens prompted in parallel for consistency.
  2. When a generated screen drifts, regenerate from the exemplar prompt — don't patch toward consistency.
  3. Dangerous admin actions need their own specs (audit, POST-only, idempotency); CRUD exemplars don't encode them for free.
  4. Put "stop and ask before new shared patterns" in CLAUDE.md — and watch the sessions; agents ignore process when the human leaves.
  5. Crypto and impersonation are human-designed; agents implement against a fixed decision.
  6. Put scale assumptions (cursor vs offset, server filters) in the exemplar — agents otherwise optimize for the seed dataset.

A field manual for AI-native software engineering.