Skip to main content
The frontend repo (/Users/rushabhpatel/Desktop/causeloop.ai/frontend) is a single Next.js 15 / React 19 application (causeloop-console) that serves two distinct authenticated surfaces — the tenant console and the staff onboarding portal — behind one same-origin API proxy. Unlike the backend, this repo has no legacy/product split: it is entirely the product. Its own README states the governing rule plainly: “The production frontend uses one deliberately small backend contract: services.api.product_app:app” — the full research backend (services.api.main, Legacy Surfaces) is not a production dependency of this app.
npm run audit:mvp:strict (scripts/audit-mvp-surface.mjs) statically extracts every backend call this codebase makes from the TypeScript AST and fails if it finds a legacy/compatibility family, an unclassified call, or an import of the synthetic fixture/retired report adapter/provenance module from production code. Run it before trusting any change that touches src/lib/api/.

Directory map at a glance

src/app/ — routes

A route group (parentheses are excluded from the URL) covering /, /sources, /loops, /issues, /remediation, and /settings. layout.tsx is the auth gate for the whole group: it reads the TENANT_SESSION_COOKIE server-side, calls the backend’s GET /me directly (forwarding the cookie), and redirects to /login on any failure — there is no client-side auth check. On success it builds a Session (name, initials, WorkspaceRole mapped from the identity’s permissions, tenant id/name) and provides it via session-context.tsx’s SessionProvider/useSession() to every page and component below it. The same layout also mounts the app shell used by every console route: Rail, Pagebar, InsightsBanner, and the singleton overlays (Scrim, LoopDrawer, MoreModal, IssueModal, PlanModal, CommandPalette, Toaster) — these are mounted once here rather than per-view specifically so opening a drawer/modal works from any route.
Covers /admin (the tenant list, the group’s own index page), /admin/tenants/[tenantId], /admin/tenants/new, /admin/jobs, and /admin/control-plane — there is no separate /admin/tenants page. layout.tsx gates the group by calling the backend’s GET /admin/me with the ADMIN_SESSION_COOKIE, redirecting unauthenticated staff to /admin/login. AdminRail.tsx/AdminPagebar.tsx are this portal’s own shell components (parallel to, but separate from, the tenant console’s Rail/Pagebar). This is the staff-facing UI for the flow documented in Client onboarding pipeline.
login/page.tsx is one page with two tabs — client and staff — calling tenantLogin/adminLogin respectively depending on which tab is active; it also fetches /api/workspaces to populate a workspace picker for the client tab. accept-invite/page.tsx is where a tenant user invited by staff (via Mailpit locally, SES/Azure Email in cloud) lands to set a password and complete POST /accept-invite.
route.ts is the only path the browser uses to reach the backend — the browser never calls the API origin directly. It: requires an ADMIN_SESSION_COOKIE or TENANT_SESSION_COOKIE for every path except an explicit PUBLIC_PATHS allowlist (login, accept-invite, and password-reset routes plus the three /health* operations); strips every inbound x-causeloop-* header before forwarding (so a client cannot spoof a trust header the backend might read); forwards the real client IP from x-forwarded-for as x-causeloop-client-ip; and returns a stable 502 JSON body if the backend is unreachable rather than letting Next.js surface an opaque 500. BACKEND_URL (src/lib/backendUrl.ts) defaults to http://127.0.0.1:18000, matching the product Compose API port, and is overridden by CAUSELOOP_API_URL.
A small unauthenticated facade: it fetches the backend’s GET /workspaces server-side and reshapes the response into { clientId, company, industry, status } for the login page’s picker, returning { workspaces: [], degraded: true } on any backend failure rather than surfacing an error to a page that has to work for a user who isn’t signed in yet.

src/lib/ — data and client layer

1

src/lib/api/ — one function per backend call

client.ts is the shared fetch wrapper (apiGet/apiSend, both against the /api/backend prefix) with uniform JSON parsing and ApiError construction (errors.ts). Every other file in this directory is a thin, typed layer directly over one backend concern, and each exported function corresponds to exactly one backend operation — cite file + function when documenting endpoint-to-frontend wiring:
  • tenantAuth.tstenantLogin, acceptInvitation, tenantMe, tenantLogout, plus mapPermissionsToWorkspaceRole (backend permission strings → the console’s WorkspaceRole type).
  • adminAuth.tsadminLogin, adminLogout, adminMe.
  • adminOnboarding.ts — the full staff onboarding surface: listTenants, getTenant, createTenant, provisionTenant, uploadIngestFile, commitIngest, putTrainingConfig, trainModel, listCheckpoints, activateCheckpoint, inviteUser, listInvitations, goLive, getRetrainRecommendation, getDataPolicy/putDataPolicy, seedSources, impersonateTenant, and status pollers (getProvisionStatus, getTrainStatus).
  • clientInsights.tsfetchClientInsights(), the single call behind the tenant console’s GET /insights/snapshot; also exports the MODEL_COLLECTIONS/LLM_COLLECTIONS constants describing the snapshot’s 10 sub-collections (5 model-derived, 5 LLM-generated).
  • tenantSources.tslistTenantSources, createTenantSource, uploadTenantSource.
  • tenantMembers.tslistTenantMembers.
  • hooks-tenant-remediation.ts — TanStack Query hooks over the remediation API: useTenantCaps, useCreateTenantCap, useUpdateTenantCap, useCompleteTenantCapStep, useTenantReviews, useCreateTenantReview.
  • remediationExport.tsdownloadExport() against GET /remediation/export.xlsx.
  • adminMetrics.ts — parses the staff GET /metrics Prometheus text response (parsePrometheusText, toJobDashboardData) for the jobs dashboard.
2

src/lib/data/ — the dashboard data hook

useConsoleData.ts is the single hook every dashboard view calls; it wraps fetchClientInsights in a useQuery and reduces the result to one of five states — loading, locked (a tenant onboarded with “share onboarding outputs” off, before anything is visible), ready (with the adapted ConsoleData, any still-generating LLM collections, and the narrative text), error (with a refetch() the shared ErrorState component wires to a Retry button), and implicitly empty via the adapter. client-adapter.ts (adaptClientInsights) does the actual snapshot → ConsoleData shape conversion and is deliberately defined at module scope so TanStack Query can memoize it as a stable select function. types.ts defines the ConsoleData view-model types. __fixtures__/client-insights.fixture.ts is a test-only backend-snapshot fixture (the audit script’s directory walk explicitly skips any fixtures directory). Separately, demo-data.json in this same directory is the bundled synthetic report used by the selector unit tests; scripts/audit-mvp-surface.mjs explicitly flags any production import of demo-data.json or the retired data/provenance module.
3

src/lib/stores/ — client UI state

Zustand stores for state that is genuinely client-only: theme.ts (light/dark, persisted to localStorage under cl-theme, applied via document.documentElement.dataset.theme), ui.ts, lens.ts, toast.ts. Per the README’s data rule, browser storage here is used only for UI preferences — never as a second source of truth for tenant data.
4

src/lib/selectors/ — pure view derivations

home.ts, issues.ts, loops.ts, remediation.ts, hazardWave.ts — pure functions that derive chart/KPI-ready shapes from ConsoleData (e.g. homeKpis, commonCauses, scoreBins, boneAllocationForLoop, donutByBone, roiByLoop). These are unit-tested directly against src/lib/data/demo-data.json (a fixture dataset, not live data) rather than through component rendering.

src/components/ — component tree

  • shell/ — app chrome shared by (most of) the app: Rail.tsx, Pagebar.tsx, InsightsBanner.tsx, ThemeToggle.tsx/ThemeInit.tsx, Toaster.tsx, LockView.tsx (the “locked” console state), ErrorState.tsx, Skeletons.tsx, LensSeg.tsx.
  • ui/ — generic primitives: Btn.tsx, Modal.tsx, Drawer.tsx, Tabs.tsx, Seg.tsx, Chip.tsx, Badge.tsx, Switch.tsx, Avatar.tsx, Kpi.tsx, ListRow.tsx, BoardCard.tsx, Callout.tsx, Disclosure.tsx, Scrim.tsx, TooltipLayer.tsx, LiveDot.tsx, FakeInput.tsx.
  • charts/ — the console’s data visualizations, largely ported line-for-line from an earlier static HTML mockup (each file’s header comments cite the exact mockup function and line range it was ported from): Fishbone.tsx/BoneChart.tsx, Constellation.tsx, HazardWave.tsx, LoopHeat.tsx, BubbleMap.tsx, Donut.tsx, DriverBars.tsx, ScoreHist.tsx, Sparkline.tsx, EngineGraphic.tsx.
  • overlays/ — the singleton, route-independent overlays mounted once by the console layout: CommandPalette.tsx, LoopDrawer.tsx, MoreModal.tsx, IssueModal.tsx, PlanModal.tsx.
  • views/ — one component per console route’s main content: HomeView.tsx, IssuesView.tsx, LoopsView.tsx, RemediationView.tsx, SourcesView.tsx/SourcesClient.tsx, SettingsView.tsx, AggregationPanel.tsx.
  • onboarding/onboarding.css, shared by the staff portal’s onboarding flow pages.
Every visual component directory pairs .tsx files with a co-located .css file (charts.css, home.css, remediation.css, etc.) and, for anything with real logic, a co-located .test.tsx/.test.ts.

src/styles/ — the brand system

tokens.css is “Brand System v2.2,” explicitly ported verbatim from a static HTML mockup’s <style> block. It defines the entire color system as CSS custom properties on :root (base palette: --cl-navy, --cl-mag-400/700/300, --cl-peri-*, --cl-amber-*, --cl-red-*, --cl-green-*, then semantic tokens built from them: --bg, --ink, --body, --muted, --surface*, --line*, --accent-*, --shadow*) and a full parallel set under [data-theme="dark"]. The theme store (src/lib/stores/theme.ts) toggles that data-theme attribute; it never touches document/window at module-init time, so SSR and first client render always agree (no flash-of-wrong-theme handling is attempted or needed). atmosphere.css layers in background decoration; tokens.test.ts guards the token contract itself.
Do not reintroduce hardcoded hex colors in component CSS — the whole point of tokens.css is that every surface reads color, spacing, and shadow through these custom properties so both themes stay in sync automatically.

Test setup

  • Unit/component tests — Vitest (vitest.config.ts): jsdom environment, global test APIs, src/test/setup.ts (which just imports @testing-library/jest-dom/vitest), path alias @ -> src/, and an explicit exclude for e2e/** (Vitest’s default include glob would otherwise sweep up Playwright specs and fail them on test.describe). Run with npm test (or npm run test:watch).
  • End-to-end — Playwright (playwright.config.ts, specs in e2e/: auth.spec.ts, console.spec.ts, helpers.ts): single worker, no retries, baseURL http://localhost:3000, and a webServer block that runs npm run dev and reuses an already-running dev server. The FastAPI product app must already be reachable at http://127.0.0.1:18000 (i.e. the product Compose stack must be up) — Playwright only manages the Next.js dev server, not the backend. Authenticated specs require CAUSELOOP_E2E_WORKSPACE, CAUSELOOP_E2E_EMAIL, and CAUSELOOP_E2E_PASSWORD for a seeded tenant; without them, only the unauthenticated-redirect check runs and the rest are explicitly skipped. Run with npm run e2e.
  • Static product-call gate: scripts/audit-mvp-surface.mjs, run via npm run audit:mvp / npm run audit:mvp:strict.

package.json scripts

The full required-check sequence (from the repo’s own README) is npm run audit:mvp:strict && npm run typecheck && npm test -- --run && npm run build.