src/app/(console)/) is the client-facing product: Home, Loops, Issues, Remediation, Sources, and Settings, all wrapped by ConsoleLayout (src/app/(console)/layout.tsx) behind the cl_tenant_session cookie. Every data call in this surface goes through the same session-gated proxy documented in Proxy and sessions; this page maps each call to the component that makes it and what the UI does with the result.
Sign-in surfaces
A
401 from POST /accept-invite does not mean the invitation itself failed. The backend accepts the invitation (creates the account, marks it accepted) and only then attempts to auto-login the new user — and that auto-login step requires the tenant to already be live. The frontend’s error handling reflects this exactly: “Your account was created, but this workspace isn’t live yet” rather than a generic failure message.ConsoleLayout itself (a server component) is the auth gate for every route under (console): it reads cl_tenant_session, fetches ${BACKEND_URL}/me directly with that cookie attached, and redirects to /login if either the cookie is missing or the fetch fails. On success it builds the console’s Session object (src/lib/session.ts) from the returned TenantIdentity:
mapPermissionsToWorkspaceRole() (src/lib/api/tenantAuth.ts) turns the backend’s real permission strings into one of four display roles, checked in this exact priority order: users.manage → Risk Admin, sources.manage → Loop Owner, reviews.write → Analyst, else → Auditor. Two gates in src/lib/session.ts derive from that role by rank (Auditor < Analyst < Loop Owner < Risk Admin):
canReview(session)— rank ≥Analyst. Gates every remediation mutation (assign owner, change status, mark a step done, adopt a suggested plan, submit a loop decision).canOperate(session)— rank ≥Loop Owner. Gates source creation/upload inSourcesClient.
The insights snapshot
Every report-backed view (Home, Loops, Issues, Remediation) is built on one shared hook:useConsoleData() (src/lib/data/useConsoleData.ts), which wraps a single query:
The response carries all nine insight collections in one coherent read — five deterministic, model-derived collections (
MODEL_COLLECTIONS: summary, themes, issues, severity, lens) and four of the five LLM-derived collections the frontend’s LLM_COLLECTIONS constant lists (fishbone, theme_summaries, narratives, recommendations) — plus dataset_version, model_version, model_ready, all_ready, and share_onboarding_outputs. The backend chooses a single (dataset_version, model_version) pair before returning any collection, specifically so one render can never mix results from two different pipeline runs.
LLM_COLLECTIONS itself has a fifth entry, cause_remediation, but services/api/insights_api.py treats it as _EXPORT_ONLY_COLLECTIONS: the snapshot endpoint serves _CLIENT_COLLECTIONS = _ALL_COLLECTIONS - _EXPORT_ONLY_COLLECTIONS, so cause_remediation never appears in GET /insights/snapshot — it’s only populated in the XLSX export (see Export). The snapshot’s nine collections are the five model collections plus the other four LLM collections.
useConsoleData() runs the (expensive, 9-collection) adaptation inside TanStack Query’s select option rather than in each consuming component’s render body, specifically so the ~10 call sites across the console share one memoized result instead of each re-running the adapter on every render. It also polls every 4 seconds — but only while at least one collection is still "generating":
The four states a view can be in
useConsoleData() exposes a four-way discriminated result (ConsoleDataResult in src/lib/data/useConsoleData.ts), and every report-backed view branches on it in the same order:
locked is a real-tenant-only state: a tenant onboarded with “share onboarding outputs” turned off (see Admin portal wiring’s Activation step), with nothing ingested yet, gets <LockView> — a soft “dashboards populate automatically” message pointing at /sources — instead of a self-service empty state, because this tenant was deliberately onboarded so the client never triggers the pipeline themselves. error is kept distinct from a plain empty report specifically so a genuine fetch/parse failure (after TanStack’s retries are exhausted) never gets silently presented as “no report yet.”
ready, in more detail
A ready result can still have work in flight: the five MODEL_COLLECTIONS being ready is enough to unblock the view, but the four LLM_COLLECTIONS (fishbone, theme summaries, narratives, recommendations) can still be "generating". useConsoleData() surfaces that as a pending: CollectionName[] array alongside data, and InsightsBanner (src/components/shell/InsightsBanner.tsx, mounted once in ConsoleLayout under the pagebar) renders a live status line for it:
consoleData.narrative (the LLM-written portfolio narrative, once generated) is consumed only by HomeView, which prefers it over the deterministic, template-built hero line whenever it’s present.
Which view reads which collection
Every collection name (
CollectionName) is a union of MODEL_COLLECTIONS and LLM_COLLECTIONS; ConsoleData itself is the adapted shape (src/lib/data/types.ts, built by adaptClientInsights()) that all four report-backed views actually consume, not the raw wire response.
Remediation: CAPs and reviews
The remediation domain is entirely tenant-scoped —src/lib/api/hooks-tenant-remediation.ts is explicit that this is “the sole production remediation boundary.” Every hook wraps useQuery/useMutation directly.
RemediationView merges live tenant CAPs with the report’s suggested fix plans (data.causes) itself — a loop with no adopted CAP shows every suggested cause-group with a “Suggested” chip and an “Adopt as plan” action; adopting one replaces only that suggestion (matched by problem statement), leaving every other suggested fix for the same loop listed alongside it. Pagebar’s “Action queue” count is every cap whose status is proposed, blocked, or completed — the three states that represent a plan currently awaiting a human decision.
Export
The export is the authenticated tenant’s own coherent snapshot — same “one dataset/model version pair” guarantee as
/insights/snapshot — rendered server-side as an XLSX workbook (issues, loops, causes, and fix plans as of the last completed pipeline run).
Sources
SourcesView (src/components/views/SourcesView.tsx) is a thin permission wrapper around SourcesClient (src/components/views/SourcesClient.tsx), passing canOperate(session) down as canManageSources.
Both calls run inside one
handleUpload() function with local uploading/uploadError state rather than useMutation, since the sequence (conditionally create a source, then upload to it) doesn’t map cleanly onto a single mutation key.
Members
SettingsView’s Members tab (src/components/views/SettingsView.tsx) is read-only in the MVP: