Lewati ke isi

Admin Architecture

Purpose

Deep-dive into apps/admin — the Next.js 14 enterprise dashboard used by counselors, marketing staff, and admins. Start here if your task touches pages, layout, components, client-side realtime, or the NextAuth session flow.

Prerequisites

Tech stack

  • Next.js 14 — App Router, React Server Components
  • React 18
  • Tailwind CSS — with design tokens in tailwind.config.ts
  • shadcn/ui (new-york style) — primary component library
  • Recharts — analytics charts
  • NextAuth (JWE) — auth
  • Socket.io client — realtime events
  • SWR (where used) or polling — data fetching

Process and hosting

  • Dev port: 3103 (npm run dev:admin)
  • Prod: systemd service huph-admin.service, port 47293
  • Nginx: admin.huph.val.id proxies to 127.0.0.1:47293
  • Restart prod: sudo systemctl restart huph-admin

Enterprise shell pattern

The root authenticated layout uses a fixed-shell pattern: sidebar and header stay put while content scrolls. This is implemented in src/components/layout/authenticated-layout.tsx:

┌──────────────────────────────────────────────┐
│ AppHeader              (fixed, never scrolls) │
├────────┬─────────────────────────────────────┤
│ Sidebar│  Main content area                   │
│ (fixed)│  (overflow-y-auto, scrolls)          │
│        │                                      │
│        │                                      │
└────────┴─────────────────────────────────────┘

Critical rule (from feedback_enterprise_shell_pattern): the root must use h-screen overflow-hidden, NOT min-h-screen. The column wrapper is overflow-hidden and the main pane is flex-1 overflow-y-auto overflow-x-hidden.

Using min-h-screen re-breaks the scroll — the body grows as content grows, pushing sidebar and header off-screen. This was a known bug, root-caused and fixed 2026-04-08. DO NOT regress.

Research-backed: Gmail, Slack, Linear, Notion, Intercom, and Salesforce Lightning all use this flex-based fixed shell.

Key pages

Path Purpose
/ Overview — today's stats, pending escalations
/conversations 3-pane inbox (list + detail + chat thread)
/conversations/[id] Single conversation detail
/conversations/inbox Alternative inbox view (older UX)
/conversations/pipeline Conversations grouped by lead stage
/leads-v2 Lead list with cluster + stage filters, 30s polling
/leads-v2/[id] Lead detail with Reassign + status actions
/analytics-v2 13 charts dashboard
/knowledge/* KB documents, sources, eval, gaps, FAQ
/follow-up Follow-up queue, rules, stats tabs
/counselor-dashboard Per-counselor view (Phase 1.6)
/settings/* Chatbot settings, escalation rules, users, clusters

Auth flow

  1. User opens admin.huph.val.id
  2. nginx forwards to Next.js (port 47293)
  3. Middleware checks NextAuth session via JWE cookie
  4. Unauthenticated → redirect to /signin
  5. Signin form posts credentials to /api/auth/callback/credentials
  6. NextAuth verifies against admin_users table (bcrypt hash)
  7. JWE session cookie issued, user redirected to requested page

Session cookie expires after 24 hours.

API proxy pattern

All admin API calls go through Next.js Route Handlers at src/app/api/*. These proxy to apps/api via the shared serverFetch helper in src/lib/serverFetch.ts:

Browser → Next.js /api/leads-v2 (Route Handler)
          → serverFetch (injects X-Internal-Secret + X-Forwarded-Session JWE)
          → apps/api /api/v1/leads/v2 (gated by middleware)

Critical: serverFetch is the ONLY way admin should talk to the API. Direct fetch() bypasses auth injection and will 401 under API_AUTH_MODE=enforce. 21 proxy routes were migrated to serverFetch in commit e3ec319.

Realtime via Socket.io

src/lib/socket.tsx provides a SocketProvider React context that wraps the authenticated layout. Pages subscribe via the useSocketEvent hook in src/hooks/useSocketEvent.ts:

useSocketEvent('message.new', (payload) => {
  // payload includes conversation_id, cluster_id, etc
  if (payload.conversation_id === currentConvId) {
    addMessageToThread(payload);
  }
});

Socket connection authenticated with NextAuth JWE (via X-Forwarded-Session header on the upgrade request).

Room scoping: counselor in cluster A never receives events for cluster B leads. Enforced server-side in apps/api/src/realtime/rooms.ts resolveRoomsForUser.

Health indicator: ConnectionStatus component shows Connected/Connecting/Offline in the header. Offline typically means NEXTAUTH_SECRET missing from API container env.

RBAC (Phase 1.5)

Cluster-based access control implemented at the admin proxy + Prisma route layer. Rules:

  • marketing_counselor with cluster_id = X → sees only cluster X leads and conversations
  • marketing_staff and marketing_admin → see all (global)
  • admin → see all, can reassign clusters

Filter applied in: - /admin/leads-v2 API proxy (list endpoint) - /admin/analytics-v2 API proxy - /admin/counselor-dashboard API proxy - Prisma routes (where used)

Spec: docs/superpowers/specs/2026-04-08-rbac-phase15-design.md.

Shared components

Component library in src/components/:

  • ui/ — shadcn/ui primitives (Button, Card, Dialog, Tabs, etc.)
  • shared/StatCard, ChartCard, EmptyState, ConnectionStatus, Skeleton, ConfirmDialog
  • layout/AppHeader, AppSidebar, authenticated-layout
  • leads/LeadCard, LeadFilterChips, ClusterBadge
  • conversations/ThreadList, ChatThread, ReplyBox, FeedbackActions, CorrectionPanel

No test infrastructure

The admin app has no Jest/Vitest/Playwright setup. Verification happens via npx tsc --noEmit, npm run build, and manual browser smoke. See the feedback_admin_no_test_infra memory and the running-tests page for the full rationale.

Gotchas

  1. Enterprise shell must use h-screen, not min-h-screen. See the pattern description above — reverting re-breaks sidebar and header scroll.
  2. apps/webchat-enterprise used to conflict on port 3103. Deleted in Apr 8 cleanup. If you see port conflicts, check for a stray process, not the old webchat.
  3. serverFetch is mandatory for API calls. Direct fetch() skips auth injection and breaks under API_AUTH_MODE=enforce.
  4. Dev port 3103 vs prod 47293. Don't hardcode — use env vars.
  5. **module-level throws breaknext build.** The originalserverFetch.tshad a top-levelthrow` if env missing, which fired during Next.js static analysis. Move env checks inside function bodies.
  6. Dark mode is defined but not activated. CSS vars exist in tailwind.config.ts but the theme toggle is not wired up. Low priority follow-up.
  7. Realtime depends on NEXTAUTH_SECRET in API container env. Missing secret → Socket.io clients can't decode JWE → Offline forever. Add to docker-compose.yml + root .env.
  8. CorrectionPanel + FeedbackActions labels are in Indonesian, the rest of admin is mostly English. Minor inconsistency, needs product decision.

See also

  • Architecture overview
  • API — the backend admin talks to
  • Running tests — admin no-test rationale
  • Admin redesign spec: docs/superpowers/specs/2026-03-21-enterprise-admin-redesign-design.md