# The console's code [Architecture](architecture.md) is the backend. This is the other half: the Next.js application in `frontend/`, for somebody about to change it. **Stack.** Next.js 15 (App Router) · React 19 · TypeScript strict · Tailwind · `next-intl` · TanStack Query · Zustand · vitest and Testing Library · Playwright. Package manager and runner: **bun**. ## Where things live | Path | | |---|---| | `src/app/[locale]/(dashboard)/…` | The product. Routes are **locale-prefixed** | | `src/app/api/…` | Route handlers proxying to the backend | | `src/lib/` | Typed API clients, `query-keys.ts`, and the registries below | | `src/hooks/` | One per resource — `use-agents`, `use-permissions`, … | | `src/stores/` | Zustand, one per concern | | `src/components//` | UI by domain; primitives in `ui/`, empty and error states in `states/` | Server Components are the default. `"use client"` is for state, effects and handlers, not for habit. ## Console presentation Use `PageHeader` for compact titles and actions. Grid catalogs use `ListCard layout="grid"` to avoid a panel around cards; tables keep the panel. Action buttons use neutral `action` tokens, independently of the `primary` ink used by checked controls and badges. Keep focus and input edges visible. People and organizations use `EntityAvatar`: uploaded photos are cropped into circles, with stable colours and initials as the fallback. Agents retain their generated faces. Gallery descriptions reserve two lines; opening an item shows the full description. Async actions use `Button loading` to preserve their label and footprint while preventing repeat submissions. Keep that state active until every step, including uploads, finishes. Link-backed buttons retain a single child and block activation while unavailable. Controls share keyboard focus treatment and compact desktop sizing, with 44px touch targets where space permits. Overlays animate from their anchor; tooltips wrap within the viewport. Calendar headings follow the console language, and progress exposes its actual value and maximum to assistive technology. Motion follows the person's Motion preference. ## The browser never calls the backend Every request goes to `/api/*` on this app, which forwards to FastAPI with the access token taken from an **HttpOnly cookie**. That is what keeps the token out of JavaScript and the backend's URL out of the client bundle. One forwarder does it — `src/lib/platform-proxy.ts` — rather than a hand-rolled route file per endpoint repeating the same twelve lines. !!! warning "A response with no `Cache-Control` is not a response nobody caches" The proxy stamps `no-store` on anything the backend left unmarked. Every answer here depends on a cookie, a permission set and the organization header, and a list refetched after a write must reach the server. A hand-rolled route file owes the same header — the proxy is the only place that applies it for you. ## Data, and where state lives **All API access goes through a client in `src/lib/`, consumed via a hook.** No `fetch` in a component. Server data lives in the query layer; **stores hold UI and ephemeral state only**. Register every query key in `query-keys.ts` so invalidation after a write stays consistent. ## Chat history and streamed updates The console opens the newest transcript page and loads earlier messages on demand. Project chips read `/conversations/{id}/metadata`, which applies the same reader and tenant checks without loading messages. Repository reads omit the model's stored summary unless model-history reconstruction explicitly requests it. Chat sockets deliver bursts once per animation frame, with a 50 ms fallback for hidden tabs and a 256-frame queue bound. Adjacent text or reasoning deltas are combined; control, tool and part boundaries retain their order. Changing the conversation, account or organization discards buffered frames. Unchanged Markdown and tool output are reused. Scrolling follows measured content growth. Completed drawings mount near the viewport and unmount outside it, retaining their measured space; their scripts restart when they return. Streaming drawings remain live, and fullscreen suspends the inline copy. ## Permissions are a rendering decision `use-permissions.ts` gives the effective permission set for the active organization. **A control the caller may not use is not rendered** — not rendered and then 403. Two traps, both of which have shipped here: - **Which roles a picker offers is arithmetic, not a list.** `assignableRoles` mirrors the server's rule over the permission catalog: a role is offered only when the caller's own strictly outranks it. A hardcoded "every role but owner" is what offered an Admin the Admin option and 403'd after the email was typed. - **A page that names an organization in its URL *is* that organization.** The API client stamps `X-Organization-Id` from the *active* org, so a page acting on the org in its path while reading permissions for the active one decides Acme's members by your role in Globex. The adoption lives in `ActiveOrgGuard`, once. ## Every user-facing string goes through `next-intl` `make lint` enforces it in both directions: a readable string sitting in a component fails, and so does a key the catalog holds that no component reads. Three rules that catch people out: - **A count is an ICU `plural`, never a ternary.** `{n} file{n === 1 ? "" : "s"}` is a sentence only English builds that way. - **A noun the sentence agrees with is not a parameter.** `{matched} of {total} {noun}` renders `3 of 40 skills` under Polish. The noun goes inside the `plural` or `select`. - **The catalog holds copy, and only copy.** A false positive takes an `i18n-exempt` with a reason; it never takes a key. Answering one by moving a Tailwind class list into `en.json` is how translating a string once stripped a component of its styling. English is the source language and is merged under every locale, so a missing translation renders English rather than the key. !!! info "The product's own nouns stay English in every locale" agent, spec, capability, skill, embed, budget, run, prompt, provider, token, vault, workspace, sandbox, MCP. They name things a client also meets in the docs, in the API and in exported YAML — translating them in the UI and nowhere else makes two vocabularies for one product. Inflect them, do not replace them. ## Four registries, and no second source Each of these is one table that several parts of the UI read. Adding to the table is the whole change; adding a second source is the bug. | | Holds | Add by | |---|---|---| | `lib/tool-catalog.ts` | Icon, running caption, finished name and renderer for every tool the backend registers | A row keyed on the capability's tool id. A backend test compares the two in both directions | | `lib/brand-glyphs.generated.ts` | Every service, connector and provider mark, as raw path data | A row in `scripts/gen-brand-icons.ts`, then `bun run gen:brand-icons`. Never an icon package import | | `lib/dashboard/registry.ts` | The dashboard widgets and the permission each is gated on | Five edits, listed on the page below | | `lib/dialog-sizes.ts` | One width token and one shape token per dialog | Picking a token, never a bespoke height | ## Two things a new surface owes Both are registries with the same failure mode: a page added anywhere else is simply absent, nothing fails, and the feature ships invisible. **A walkthrough stop.** `lib/onboarding/tour.ts` is the passive walk a page's "?" replays, and `flows.ts` is the guided creation it offers at the end. A page with no stop renders **no "?" at all** — so a new page whose header has no help button has not been registered. Gate the step on the permission its control carries, mark it `optional` when the control needs data to exist, and anchor it on something bounded. **A dashboard widget**, if the feature produces state somebody would want at a glance. Five edits: the id and definition in `dashboard/registry.ts`, the component in `components/dashboard/widgets/`, a placement in `layouts.ts`, the id mirrored in `backend/app/schemas/dashboard_layout.py` — a test fails when those two drift — and copy in `en.json` and in each translation beside it. ## Verify From `frontend/`. At the repository root vitest finds no config, reports around 164 phantom failures and leaves a stray cache directory. ```bash bunx vitest run src/components/chat/usage-meter.test.tsx # while writing ``` Once, before pushing — from the repository root: ```bash make lint-frontend && make test-frontend-cov && make build-frontend ``` !!! danger "`test:coverage`, not `test:run`" The job CI runs measures coverage and fails under 100% lines, statements and functions or 97.5% branches. A suite where every test passes can still be red, and has been. A dead branch is easier to delete than to cover: a `?? ""` behind a check that already proved the value is one the gate is right to notice. **A spec that times out is usually the machine.** `testTimeout` is 15s and `asyncUtilTimeout` 5s, both measured rather than guessed. Neither is a reason to keep a spec that mounts more than its assertions read. ## Recap - The browser talks to **`/api/*` on this app**, never to FastAPI — one forwarder, and it stamps `no-store`. - Server data lives in the **query layer**; stores hold UI state only. - A control the caller may not use is **not rendered**, and role pickers are **computed, not listed**. - Copy goes through **`next-intl`**, counts are ICU plurals, and the catalog holds copy and only copy. - A new surface owes a **walkthrough stop**, and a widget if it has glanceable state — both are silent registries. [The backend →](architecture.md) · [Adding a feature →](adding_features.md) · [Testing →](testing.md) ## Evals in the console Evals reuse the console's `PageHeader`, `ListCard`, dialog sizes, tabs and `WizardSteps`. Data changes invalidate the registered query families through `/ws/events`; a manual verdict also refreshes suites, improvement sessions and environment guards. A read failure has a retry action instead of an empty-success state. Narrow layouts keep form fields at 16 px and leave bottom space for the Architect. Datasets retain their own sharing and resource grants and can be reused across agents. Grading model and instructions belong to the dataset; generation and improvement settings have their own Settings tab. Code-defined helpers keep internal run identities for attribution without appearing in the user-agent catalog. Cases and evals use the console's shared tables; case and review details open in side panels. The Architect does not receive a new dataset-writing capability in this milestone. Creating questions is an explicitly estimated, permission-checked action in the improvement flow; the corner assistant cannot silently start that paid work. Its button remains reachable beside the eval views. `e2e/improvement.mobile.spec.ts` crosses the real API and background worker. It checks dataset cases, the case editor, grading, review, the eval result, the improvement wizard and confirmed publication with an environment guard. Only the external model is a fixture. GitHub Actions retains light/dark screenshots at phone and desktop widths for the visual pass.