/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
(console)/ — tenant console
(console)/ — tenant console
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.admin/(portal)/ — staff onboarding portal
admin/(portal)/ — staff onboarding portal
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/ and accept-invite/
login/ and accept-invite/
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.api/backend/[...path]/ — the product API proxy
api/backend/[...path]/ — the product API proxy
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.api/workspaces/
api/workspaces/
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.ts—tenantLogin,acceptInvitation,tenantMe,tenantLogout, plusmapPermissionsToWorkspaceRole(backend permission strings → the console’sWorkspaceRoletype).adminAuth.ts—adminLogin,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.ts—fetchClientInsights(), the single call behind the tenant console’sGET /insights/snapshot; also exports theMODEL_COLLECTIONS/LLM_COLLECTIONSconstants describing the snapshot’s 10 sub-collections (5 model-derived, 5 LLM-generated).tenantSources.ts—listTenantSources,createTenantSource,uploadTenantSource.tenantMembers.ts—listTenantMembers.hooks-tenant-remediation.ts— TanStack Query hooks over the remediation API:useTenantCaps,useCreateTenantCap,useUpdateTenantCap,useCompleteTenantCapStep,useTenantReviews,useCreateTenantReview.remediationExport.ts—downloadExport()againstGET /remediation/export.xlsx.adminMetrics.ts— parses the staffGET /metricsPrometheus 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.
.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.
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 fore2e/**(Vitest’s default include glob would otherwise sweep up Playwright specs and fail them ontest.describe). Run withnpm test(ornpm run test:watch). - End-to-end — Playwright (
playwright.config.ts, specs ine2e/:auth.spec.ts,console.spec.ts,helpers.ts): single worker, no retries,baseURL http://localhost:3000, and awebServerblock that runsnpm run devand reuses an already-running dev server. The FastAPI product app must already be reachable athttp://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 requireCAUSELOOP_E2E_WORKSPACE,CAUSELOOP_E2E_EMAIL, andCAUSELOOP_E2E_PASSWORDfor a seeded tenant; without them, only the unauthenticated-redirect check runs and the rest are explicitly skipped. Run withnpm run e2e. - Static product-call gate:
scripts/audit-mvp-surface.mjs, run vianpm 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.