AdminCustomersPage: founder view of all teams + tier/promo actions#45
Merged
Conversation
New /app/admin/customers console for platform admins. Gated on the new is_platform_admin flag on AuthMeResponse — non-admin users redirect to root (404-equivalent) instead of seeing a 403, and the sidebar link is suppressed entirely. Page surfaces: - Filter pills (All / Anonymous / Free / Hobby / Pro / Team) + search - Sortable table: Email · Tier · MRR · Storage · Deploys · Last active · Signed up - Row click opens CustomerDetailDrawer (right-side slide-in) - Drawer tabs: Overview / Resources / Activity / Promos - "Issue promo" modal — kind, value, applies_to, valid_for_days; surfaces the generated code with a Copy button - "Promote / demote tier" modal — requires typed PROMOTE / DEMOTE confirmation, computed from current → target tier rank Backend contract assumed (Track A): - GET /api/v1/admin/customers - GET /api/v1/admin/customers/:team_id - POST /api/v1/admin/customers/:team_id/tier - POST /api/v1/admin/customers/:team_id/promo 23 new tests covering route gating, sorting, filtering, drawer tabs, promo issuance, and tier-change confirmation. Full suite: 365 passed, 3 skipped (was 342 + 23 new). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
7 tasks
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
/app/admin/customerspage — gated onctx.me.is_platform_admin(new field onAuthMeResponse). Non-admin users redirect to root (404-equivalent, not 403), so the route's existence isn't leaked. Sidebar link is also conditional.CustomerDetailDrawer— a right-side slide-in with four tabs (Overview / Resources / Activity / Promos) so the operator never loses list context.PROMOTEorDEMOTEconfirmation, derived from the tier rank delta. Reason field is required and lands in the audit log.Backend contract assumed (Track A)
The dashboard talks to four new endpoints. Where Track A's shape differs from these assumptions, the adapter in
src/api/index.tsis the single place to fix:GET /api/v1/admin/customers?q=&tier=&sort_by=&limit=&offset=→{ ok, customers: AdminCustomerSummary[], total }GET /api/v1/admin/customers/:team_id→{ ok, team, users, resources, audit_log[], deploys, subscription, promos? }POST /api/v1/admin/customers/:team_id/tier→{ tier, reason }→{ ok, team }POST /api/v1/admin/customers/:team_id/promo→{ kind, value, applies_to, valid_for_days }→{ ok, code, expires_at }/auth/mecarries a newis_platform_admin?: booleanflag.Fields where I made an assumption Track A might not match exactly:
AdminCustomerSummary.mrr_monthly/mrr_yearly— assumed INR paise (×100), formatted viaIntl.NumberFormat('en-IN', { style: 'currency', currency: 'INR' }). If Track A returns rupees or USD cents, onlyformatINRCompactinAdminCustomersPage.tsxandformatINRin the drawer need to change.AdminCustomerSummary.name— marked optional because some old teams have no display name; the drawer falls back toteam.display_name/team.name/summary.namein that order.AdminCustomerDetailResponse.promosis optional — the page degrades to an empty list when the field isn't present.subscriptionmay benullfor unpaid teams (rendered as "none").sort_byquery param is sent but the page re-sorts locally for instant header-click feedback.Test plan
npm test— 365 passed / 3 skipped (was 342, +23 new inAdminCustomersPage.test.tsx)npm run build— clean; page lazy-chunked to~23 KB(gzip 6.4 KB)/auth/meresponse).🤖 Generated with Claude Code