Skip to main content

Admin Panel v2 Live

Operations command center for KA26. Rebuilt 2026-04-27 from a top-nav stats dashboard into a full operational toolkit: side-nav layout, ⌘K command palette, universal entity drawer, segmented broadcasts, trust & safety queue, audit log, real impersonation, suspend cascade with force-logout, affected-user notifications, and live orders monitor.

Today the admin is gated by seller.role === "admin". RBAC for multi-admin teams is stubbed (requireAdminRole(["super_admin", "finance"]) exists but is a no-op pass-through) — when team admins land we just enforce the role check inside that helper without rewriting any endpoints.

1. Architectural moves (the v2 rebuild)

MoveWhyWhere
Side nav (collapsible) replaces top navTop nav had 10+ items and no room for badges or future sectionssrc/components/admin/AdminShell.tsx
⌘K command paletteKeyboard-first jump to any entity (sellers/orders/products/...)src/components/admin/CommandPalette.tsx + /api/admin/search
Universal entity drawerClick any entity name → slide-in case file with quick actions, no navigation tax. Stacks (open multiple).src/components/admin/EntityDrawer.tsx + /api/admin/entities/[type]/[id]
Single chokepoint for destructive actionsEvery suspend / hide / delete / dismiss / impersonate goes through one route → mandatory reason → audit log → notify affected user/api/admin/entities/[type]/[id]/actions
Operations homepage replaces stats dashboard"What needs me right now?" answer instead of chartssrc/app/admin/page.tsx
Audit log surfaces every destructive actionMulti-admin accountability + customer service appeals + regulatory CYAAdminAction model + /admin/audit-log page
Cmd-K, Cmd-K, Cmd-KOperations is keyboard workmounted globally

2. Pages

PathPurpose
/adminOperations homepage — KPIs + 4 swim-lanes (verifications, moderation, stuck orders, recent admin activity). Auto-refreshes every 15s.
/admin/sellersLegacy sellers list (preserved from old admin)
/admin/audit-logFilterable AdminAction viewer with before/after JSON diff
/admin/moderationT&S queue (j/k keyboard nav)
/admin/communicationsCompose broadcast (5-step wizard + sticky review pane)
/admin/communications/templatesTemplate CRUD
/admin/communications/auditSend history (last 50 broadcasts)
/admin/orders-monitorLive orders, auto-refresh 10s
/admin/observabilitySystem health dashboard (see System Health)
/admin/verificationPending seller verifications
/admin/stores, /admin/consumers, /admin/doctors, /admin/delivery-partners, /admin/creatorsPer-segment lists
/admin/websiteWebsite contact + job apps

3. Database models added (v2)

ModelPurposeCritical fields
AdminActionAppend-only audit logadminId, adminEmail, category, action, entityType?, entityId?, reason?, beforeState?, afterState?, ipAddress?, userAgent?, rolledBack
BroadcastTemplateReusable pre-composed messagesname @unique, type, title, body, audience (JSON), channels[], usageCount
ScheduledBroadcastOne row per sendtype, title, body, audience (JSON), channels[], status, scheduledFor?, sentAt?, recipientCount, deliveredCount, failedCount, clickCount, contentHash
BroadcastDeliveryPer-recipient auditbroadcastId, recipientType, recipientId, status, pushSent, notifWritten, ackAt?, clickedAt?
ModerationTicketT&S queue itemsentityType, entityId, source (auto_flag / user_report / admin_review), reason?, visionScores?, status, reporterType?, reporterId?, reviewedBy?, resolution?, adminActionId?
SavedFilterPer-admin per-page list viewsadminId, page, name, filterJson, pinned
ImpersonationSessionShort-lived "view as user" tokensadminId, targetType, targetId, token @unique, mode, reason, expiresAt, endedAt?

Plus tokenInvalidatedAt DateTime? on Seller, Consumer, Doctor for force-logout.

4. The single rule for destructive actions

All destructive admin actions flow through one route:

POST /api/admin/entities/{type}/{id}/actions
{ "actionKey": "suspend", "reason": "Sold counterfeit goods" }
  • Reason is mandatory (≥3 chars). Enforced server-side, surfaced in the ConfirmModal UI.
  • High-stakes verbs (suspend, delete, reject) require typing the verb verbatim ("SUSPEND") to confirm — prevents accidental clicks.
  • Every action writes an AdminAction row with before/after JSON snapshot + IP + user-agent + admin id.
  • Affected user notified: when admin suspends/deletes/hides, the affected user gets a SellerNotification / Notification / DoctorNotification row + push with the admin's reason.

Suspend cascade (the headline action)

"Suspend seller" performs in one transaction:

  1. Seller.status = "disabled"
  2. Seller.tokenInvalidatedAt = new Date() (force logout from every device)
  3. Store.updateMany({ where: { sellerId }, isActive: false })
  4. Product.updateMany({ where: { sellerId }, isActive: false })
  5. Write AdminAction row
  6. notifyAffected("seller", id, …) — push + write SellerNotification with admin's reason

Reactivate reverses 1+3+4 but does NOT re-bump tokenInvalidatedAt — the seller must re-login. Safer default than re-enabling tokens issued during the disabled period.

5. Communications studio

Three notification types, each with different runtime behavior:

TypePush?DB notification row?Use for
ephemeralyesNO"Quote of the day", holiday wishes, one-time engagement
persistentyesyesFraud warnings, policy updates, important announcements
ack_required(planned)yes + must-ack flagLegally-binding policy changes (UI disabled today)

Linear 5-step compose flow

  1. Audience — segments (Sellers / Consumers / Doctors) + filters (city, active-within-N-days, seller-status, min-orders)
  2. Notification type — ephemeral / persistent / ack_required
  3. Channels — which apps to push to (defaults to audience, override to push to a subset)
  4. Compose — title (120) + body (500) + optional CTA label + deep link
  5. When — immediate or schedule

Sticky right-side review pane shows: live recipient count + per-app pill breakdown + push preview rendered in each app's brand color (consumer blue, seller emerald, doctor purple). The Send button label tells you exactly what's missing when disabled — "Pick an audience" / "Write a title + body" / "No matching users" — instead of staying greyed out with no explanation.

Throttle + quiet hours

  • 5-minute throttle on identical content. contentHash = sha256(title + body + sorted(channels)). A second send of the same payload within 5 minutes returns 409 duplicate_recent_send. Prevents accidental double-sends.
  • Quiet hours (10pm-7am IST) block ephemeral pushes unless overrideQuietHours: true is passed. Persistent and ack_required ignore the guard (they go to the bell whether the device buzzes or not).

6. Token revocation + impersonation

Force logout

getAuthenticatedSeller() and resolveConsumerId() decode the JWT's iat claim and reject if iat * 1000 < user.tokenInvalidatedAt.getTime(). Legacy tokens without iat are grandfathered (the user just needs to re-login once to get a token with iat).

Bumping the timestamp is how every "suspend cascade" instantly kills the seller's existing sessions across web + mobile.

Real impersonation

Two-step flow because the admin's own session must stay alive:

  1. Admin clicks "Impersonate" in the entity drawer
  2. Server creates an ImpersonationSession with a single-use opaque token (30-min TTL, mandatory reason, default mode: "read_only")
  3. Drawer opens /api/admin/impersonate/{token} in a new tab
  4. That endpoint redeems the token, mints a JWT for the target user with extra claims:
    • impersonatedBy: <adminId>
    • impersonationMode: "read_only"
  5. Sets the appropriate cookie (ka26_consumer_token / seller_token / ka26_doctor_token)
  6. Redirects to the target app's home

ImpersonationBanner is mounted in the root layout. It reads /api/auth/who-am-i, and if the active cookie has impersonatedBy, renders a sticky red ribbon at the top of every page in the impersonated session with an "End session" button (which hits /api/admin/impersonate/end to clear the cookies).

Today's caveat: the read_only mode is informational. The banner says it, the JWT carries it, but the per-app middleware that should reject mutations from impersonated cookies isn't wired yet. Listed as Phase E follow-up.

7. APIs

EndpointMethodPurpose
/api/admin/operations/summaryGETDrives Operations homepage + nav badges
/api/admin/search?q=GETCmd-K entity search
/api/admin/entities/{type}/{id}GETCase-file payload for the EntityDrawer
/api/admin/entities/{type}/{id}/actionsPOSTSingle chokepoint for destructive actions
/api/admin/audit-logGETList AdminAction rows (filterable)
/api/admin/broadcastsGET, POSTSend broadcast / list last 50
/api/admin/broadcasts/audience-countGETLive recipient count for compose UI
/api/admin/broadcasts/templatesGET, POST, DELETETemplate CRUD
/api/admin/moderationGET, POSTList tickets (admin) / create ticket (consumer-side <ReportButton/>)
/api/admin/orders-monitorGETLive orders (KPIs + 100 most recent)
/api/admin/impersonate/{token}GETRedeem token → issues 30-min cookie for target user
/api/admin/impersonate/endPOSTClear all 3 user-app cookies
/api/auth/who-am-iGETDrives the ImpersonationBanner

8. Consumer "Report this" button

The moderation queue's user-report input. Drop-in component:

<ReportButton entityType="product" entityId={product.id} />

Opens a modal with category dropdown (Inappropriate, Misleading, Spam/scam, Counterfeit, Pricing fraud, Wrong category, Other) + free-text. POSTs to /api/admin/moderation → ticket lands in T&S queue with source=user_report, reporter linked back via reporterType/reporterId.

Wired today on the consumer product detail page. Other entities (reviews, reels, profile) can be added by dropping the same component in.

9. Tests

  • tests/admin-v2.test.ts — 63 contract tests covering schema, API exports, UI sections, helpers, security (token-revocation cascade, audit-log mandatory reason, impersonation TTL), notification-type contract.
  • tests/admin-dashboard.test.ts — 139 tests covering legacy admin pages + APIs.

10. Helpers (src/lib/)

FileExports
admin-actions.tsrequireAdmin() / requireAdminRole(roles) / logAdminAction() / requestForensics()
broadcast-engine.tsresolveAudience() / contentHash() / findRecentDuplicate() / isQuietHourIST()

11. Unified entity action vocabulary (v2.3, 2026-04-27)

Pre-v2.3, each list page had its own action verbs:

PagePre-v2.3 verbs
SellersDisable, Edit, Delete (icon)
ConsumersView, Suspend, Ban
DoctorsDeactivate, Delete
Delivery PartnersSuspend, Delete
CreatorsView, Suspend

5 dialects, no muscle-memory transfer. v2.3 unified all of them through one shared component <AdminEntityActions> and one verb set.

Standard verbs (every entity, same position)

VerbMeaningAvailable
ViewOpen the EntityDrawer case fileAll
EditModify entity detailsSellers (limit), Stores, Products
SuspendSoft-disable, reversible. Bumps tokenInvalidatedAt + hides content + notifies user with reason. Requires typing SUSPEND to confirm.All
ReactivateReverse Suspend.All
BanPermanent intent. Same DB change as Suspend but flagged in audit + user notification. Requires typing BAN.Sellers, Consumers, Doctors
UnbanReverse Ban.Sellers, Consumers
DeleteSoft-delete (status flip + audit row). Requires typing DELETE. Hard delete is gated separately.All
Impersonate30-min read-only session as the user.Sellers, Consumers, Doctors

Component contract

src/components/admin/AdminEntityActions.tsx accepts:

  • entityType: "seller" | "consumer" | "doctor" | "delivery_partner"
  • entityId: number
  • status?: lowercase status — drives which verbs render (e.g. Suspend hides when already suspended)
  • label?: human-readable name shown in confirm dialog
  • onChanged?: called after a successful destructive action (parent list re-fetches)

Single chokepoint API

POST /api/admin/entities/[type]/[id]/actions handles every entity type. The pre-v2.3 per-entity admin routes (/api/admin/consumers/[id] etc) are preserved for backwards compat but now also write to AdminAction + call notifyAffected.

Cascade preview

Each high-stakes verb shows a preview in the ConfirmModal so the admin sees what will happen before confirming:

Suspend will:

  • Hide all stores + products
  • Force-logout from every device
  • Notify seller with your reason

Plus a required reason textarea (≥3 chars) AND a typing gate ("Type SUSPEND to confirm").

Tests

tests/admin-entity-actions.test.ts — 45 cases pinning the component, the wiring on all 5 list pages, the unified API verb coverage per entity, and the notification + audit + token-revocation contract.

13. Known gotchas / current limits

  • Read-only impersonation is informational — banner says it, JWT carries the claim, but mutations from impersonated cookies still execute. Phase E follow-up.
  • Cloud Vision auto-flagging not wiredModerationTicket.visionScores is in the schema and the queue UI handles source=auto_flag, but no upload-side hook enqueues tickets yet. The queue's only real input today is consumer reports + admin-initiated.
  • RBAC stubrequireAdminRole() exists but accepts any admin. Today every admin is super_admin.
  • Saved filters — model exists, no per-page UI yet.
  • Bulk multi-select on lists — not yet.
  • RollbackAdminAction.rolledBack column exists, no UI to actually rollback yet (24h window is the design intent).