Skip to main content

Frontend architecture

The frontend is a Next.js 14 App Router app in TypeScript, in frontend/src. It is a thin, fast client over the backend API: no server-side data layer of its own, just pages and components that call /api and render.

Stack

ConcernChoice
FrameworkNext.js 14 (App Router), React 18
LanguageTypeScript
StylingTailwind CSS with a project design system in app/globals.css
AnimationFramer Motion
Data fetchingTanStack Query; large lists virtualized with TanStack Virtual
Iconslucide-react
Build/runnext dev (port 3000 locally), next start in production (standalone output)

Routing map

RouteFilePurpose
/app/page.tsxEntry; routes the user into the app or to login.
/dashboardapp/dashboard/page.tsxThe team's claims, per-claim usage, and headline stat cards (claims, items, processing speed); create a claim.
/claims/[id]app/claims/[id]/page.tsxA claim: its rooms, details, submission history (components/SubmissionsCard.tsx: date, items, photos, claim key, submission id, processing time), and submit. Send for Estimation disables once every item is submitted; audio-only items show a neutral "no photo to send" note instead of a warning.
/rooms/[id]app/rooms/[id]/page.tsxA room: upload media, list jobs, review room inventory.
/jobs/[id]app/jobs/[id]/page.tsxA job: processing progress and its inventory.
/settingsapp/settings/page.tsxProfile (time zone preference), auto-approval rules, AI prompts, integrations.
/changelogapp/changelog/page.tsxCustomer changelog, rendered from src/content/changelog/customer.md. The sidebar item shows a dot while there are unseen entries (localStorage).
/updatesapp/updates/page.tsxInternal release log (admins only), rendered from src/content/changelog/internal.md; same unseen-dot behavior. Note the gate is UI-only: the markdown ships in the client bundle.
/adminapp/admin/page.tsxInternal admin dashboard (admins only): system-wide health across all teams. Overview tab (live stats, range-scoped credits and AI cost, last 10 jobs, claims, jobs, team leaderboard), Teams tab (per-team component visibility with global defaults; training ships hidden), Activity Log tab (all teams, team filter), and Media tab (browse and play every upload).
/logsapp/logs/page.tsxPipeline Logs (admins only). Jobs tab: every job system-wide with detailed logs, filters, grouping, plus per-job Mark failed (dismiss a stuck job with a refund) and Delete (hard-delete with confirm). API Requests tab (components/ApiRequestsPanel.tsx): OpenRouter calls grouped by job with per-job totals (requests, cost, time), searchable by job/claim/filename and filterable by company/user, expanding to per-call request/response JSON with an open-in-OpenRouter link.
/training, /training/missions/[key]app/training/*The training hub and mission runner. Hidden (nav and page) when the admin switches the training component off for the team. See training.
/verify/[code]app/verify/[code]/page.tsxPublic certificate verification (no sign-in).
/help, /help/[slug]app/help/*The in-app Help Center.
/login, /logout, /auth/callbackapp/*The SSO flow.
error.tsx, not-found.tsxapp/*Error and 404 pages.

A client-side SSE proxy route also exists at app/api/jobs/[id]/stream/route.ts to relay the backend stream during local development.

Dates and times format through lib/datetime.ts, which shows every timestamp in the user's chosen time zone (Settings > Profile, stored on the user row) or the browser's local zone by default. Do not call toLocaleString with a hardcoded zone.

Changelogs are markdown files in src/content/changelog/ imported as raw strings (a webpack rule in next.config.js), parsed by lib/changelog.ts (## YYYY-MM-DD - Title + bullets), and rendered by components/ChangelogTimeline.tsx. Updating them on every product commit is a hard rule in CLAUDE.md. The footer (components/Footer.tsx) shows the team's credit balance and a Buy credits button (on prepaid-billing instances, moved here from the old top bar), the copyright line, and the build's commit hash (linking to /changelog) and date from NEXT_PUBLIC_COMMIT_SHA / NEXT_PUBLIC_COMMIT_TIME, which docker-compose bakes in at build time. Video playback goes through components/MediaFrame.tsx, which reserves a 16:9 box with a logo placeholder and spinner so pages never shift while media loads.

The app shell

app/layout.tsx wraps everything in three context providers and the shell:

  • components/PostHogProvider.tsx initialises product analytics and wires page views and user identity. It is a no-op when no PostHog key is configured (the default), so it is invisible until analytics is turned on.
  • components/AppShell.tsx is the frame: the responsive sidebar and the content column. There is no desktop top bar — the team's credit balance now lives in the footer (Footer takes a billing prop), and the content column's left margin follows the sidebar's collapsed width. On mobile a slim top bar carries only the hamburger + logo.
  • components/Sidebar.tsx is the left nav with the Adjust Square logo, an environment badge (hidden in production, amber for stage, violet for local), and the signed-in user with a sign-out button. On desktop the rail is collapsible to an icon strip via a toggle above the user chip; the choice persists in the cv_nav_collapsed cookie through a shared useSyncExternalStore store (lib/navCollapsed.ts) that both the sidebar (its width) and AppShell (the content margin) read, so a toggle re-renders them together. Collapse applies at the xl breakpoint only; the mobile drawer is always full width. It also renders an Admin tab, but only when useAuth().user?.is_admin is true (the allowlisted operators). Hiding it is cosmetic; the /admin page and its API enforce access on their own (see authentication).
  • The environment comes from NEXT_PUBLIC_APP_ENV via getAppEnv() in layout.tsx.

The API client

All backend calls go through lib/api.ts. There is no axios or generated client; it is hand-written fetch/XMLHttpRequest wrappers.

  • Auth header: actorHeaders() reads the token from localStorage (cv_auth_token) and adds Authorization: Bearer <token> to every request.
  • Base URL: everything is relative to /api, which nginx (prod) or the Next proxy (dev) routes to the backend.
  • Uploads: video/photo/audio uploads use XMLHttpRequest so a real upload-progress percentage can drive the UI.
  • Errors: failed calls try to surface the backend's detail message so users see something meaningful.
  • Streaming: subscribeToJobProgress opens an EventSource to the job stream and dispatches progress, log, and metrics callbacks.
// lib/api.ts: every call attaches the token
function actorHeaders(): Record<string, string> {
const token = getAuthToken() // localStorage 'cv_auth_token'
return token ? { Authorization: `Bearer ${token}` } : {}
}

Authentication on the client

lib/auth.tsx provides AuthProvider/useAuth. On mount it restores a saved session from localStorage, and if none exists (and the user did not just log out) it probes /api/auth/me in case a same-subdomain AdjustSquare cookie is present. The login redirect, token exchange at /auth/callback, and logout are covered in authentication.

Real-time job progress

When a job is processing, the page subscribes to the SSE stream and updates components/ProcessingStatus.tsx live: an animated progress bar, the current stage text, and a scrolling log. When the stream sends complete, the UI swaps to the inventory.

Component map

AreaComponents
Shell & navAppShell, Sidebar, ModalShell, ModalPortal
Claims & roomsCreateClaimModal, CreateRoomModal, RoomUpload
UploadRoomUpload (one drop area for any media mix), ConfirmUploadsModal, GroupPhotosModal
Trainingapp/training (hub + mission runner), app/verify/[code] (public certificate page), TrainingTour (spotlight tours), TrainingTracker (floating mission task list), TrainingBadgeIcon
ProcessingProcessingStatus, JobCompletionChime
ReviewInventoryTable, ItemModal, ReviewMode, TranscriptPanel, VideoPlayer
SubmitSubmitClaimModal
SettingsPromptsCard, automation UI
HelpersStyledSelect, ConfirmModal, HelpArticleBody

Helper libraries: lib/media.ts (upload classification: extension lists, classifyFile, folder traversal for drag-and-drop), lib/confidence.ts (confidence buckets and colors), lib/sourceTag.ts (video/photo/audio badges), lib/automationSettings.tsx (auto-approval context), lib/chime.ts (the completion sound), hooks/useEscapeToClose.ts.

The design system

The visual language lives in frontend/src/app/globals.css as a set of component classes (.btn-primary, .card, .as-input, .as-table, .pill-*, .eyebrow, motion primitives). The handbook you are reading reuses the same fonts (Inter Tight, JetBrains Mono, Instrument Serif), the #0B80F0 blue, and the slate palette so the two surfaces feel like one product.

Building UI? Read the conventions

The project has firm UI and motion rules (use the design-system classes, never a native <select>, animate everything in and out with Framer Motion). They are summarized in engineering conventions and live in full in the repo's CLAUDE.md.

The Help Center

In-app help is content-driven. Articles are TypeScript modules in frontend/src/content/help/ (one file per article, registered in articles.ts), rendered by app/help/[slug]/page.tsx through components/HelpArticleBody.tsx. The house style: short verb-first headings, terse steps, screenshots, and a path that always starts from the Dashboard. This is distinct from the handbook (this Docusaurus site), which documents how the system works rather than how to perform a task in the UI.