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
| Concern | Choice |
|---|---|
| Framework | Next.js 14 (App Router), React 18 |
| Language | TypeScript |
| Styling | Tailwind CSS with a project design system in app/globals.css |
| Animation | Framer Motion |
| Data fetching | TanStack Query; large lists virtualized with TanStack Virtual |
| Icons | lucide-react |
| Build/run | next dev (port 3000 locally), next start in production (standalone output) |
Routing map
| Route | File | Purpose |
|---|---|---|
/ | app/page.tsx | Entry; routes the user into the app or to login. |
/dashboard | app/dashboard/page.tsx | The team's claims, per-claim usage, and headline stat cards (claims, items, processing speed); create a claim. |
/claims/[id] | app/claims/[id]/page.tsx | A 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.tsx | A room: upload media, list jobs, review room inventory. |
/jobs/[id] | app/jobs/[id]/page.tsx | A job: processing progress and its inventory. |
/settings | app/settings/page.tsx | Profile (time zone preference), auto-approval rules, AI prompts, integrations. |
/changelog | app/changelog/page.tsx | Customer changelog, rendered from src/content/changelog/customer.md. The sidebar item shows a dot while there are unseen entries (localStorage). |
/updates | app/updates/page.tsx | Internal 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. |
/admin | app/admin/page.tsx | Internal 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). |
/logs | app/logs/page.tsx | Pipeline 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.tsx | Public certificate verification (no sign-in). |
/help, /help/[slug] | app/help/* | The in-app Help Center. |
/login, /logout, /auth/callback | app/* | The SSO flow. |
error.tsx, not-found.tsx | app/* | 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.tsxinitialises 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.tsxis 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 (Footertakes abillingprop), 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.tsxis 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 thecv_nav_collapsedcookie through a shareduseSyncExternalStorestore (lib/navCollapsed.ts) that both the sidebar (its width) andAppShell(the content margin) read, so a toggle re-renders them together. Collapse applies at thexlbreakpoint only; the mobile drawer is always full width. It also renders an Admin tab, but only whenuseAuth().user?.is_adminis true (the allowlisted operators). Hiding it is cosmetic; the/adminpage and its API enforce access on their own (see authentication).- The environment comes from
NEXT_PUBLIC_APP_ENVviagetAppEnv()inlayout.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 fromlocalStorage(cv_auth_token) and addsAuthorization: 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
XMLHttpRequestso a real upload-progress percentage can drive the UI. - Errors: failed calls try to surface the backend's
detailmessage so users see something meaningful. - Streaming:
subscribeToJobProgressopens anEventSourceto the job stream and dispatchesprogress,log, andmetricscallbacks.
// 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
| Area | Components |
|---|---|
| Shell & nav | AppShell, Sidebar, ModalShell, ModalPortal |
| Claims & rooms | CreateClaimModal, CreateRoomModal, RoomUpload |
| Upload | RoomUpload (one drop area for any media mix), ConfirmUploadsModal, GroupPhotosModal |
| Training | app/training (hub + mission runner), app/verify/[code] (public certificate page), TrainingTour (spotlight tours), TrainingTracker (floating mission task list), TrainingBadgeIcon |
| Processing | ProcessingStatus, JobCompletionChime |
| Review | InventoryTable, ItemModal, ReviewMode, TranscriptPanel, VideoPlayer |
| Submit | SubmitClaimModal |
| Settings | PromptsCard, automation UI |
| Helpers | StyledSelect, 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.
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.