M6-02: Console app and serving by the server (ADR 0028)¶
Status: ready | Depends on: M4-07, M4-09, M6-01 | Milestone: M6
Goal¶
After this ticket apps/console is a Flutter Web app for administration, statistics and the terminal only (ADR 0028, ADR 0070; 01 5.5), built with the JavaScript build, and the server serves it from its own image under /console/ on the same origin as the API. The console has the shell of 01 5.5 (navigation on the left: Live, Statistics, Hosts and terminal, People, Groups and grants, Devices, Agents and costs, Apps and webhooks, Audit, Import, Instance and branding, Status; each entry a placeholder until its ticket fills it), the login following E2 of 01 5.6 against the console form of M6-01 (provider buttons from getDiscovery, OIDC with a redirect to /console/login/callback in the same tab, passkey through WebAuthn in the browser, password with the one-time code), the step-up that 08 7.5 requires directly after login (passkey, otherwise a new OIDC login or password with TOTP), E7 after logout or expiry, and no device screens (no E3, E4, E5, no device pairing). Its HTTP layer uses the generated api_client with the session cookie, the CSRF header of M6-01 on every state-changing call and no DPoP; it holds no token. The console never depends on sync_engine or local_store (architecture test), has no route for channels, threads or messages (route test), turns on semantics at startup (ADR 0028 "Confirmation"), and shows and checks its version, which always equals the API version of the server that serves it. Why: administration must be reachable without an app (B5), with the same packages as the desktop (design_system, api_client, auth, admin_ui) and without chat in the browser (ADR 0028).
Read first¶
docs/adr/0028-web-target-only-for-the-admin-and-statistics-console.md(all; "Confirmation": architecture test, route test, semantics test),docs/adr/0070-terminal-in-the-web-console.md(item 5).docs/spec/01-product.md: 5.5 "Web console" (navigation and the note "No chat in the browser"), 5.6 (E2 and E7 in the console; "The web console uses only E2 and E7: it has no device confirmation, but requires a step-up after the login"), 8 (screen readers in the console: NVDA and VoiceOver), 9 (languages), 12 (branding).docs/spec/15-client-architecture.md: 2 (row Web: current Chromium, Firefox and Safari; JavaScript build), 3 and 3.1 (rule 3:apps/consolewithoutsync_engineandlocal_store), 8 (console step-up by WebAuthn passkey), 9 (row Web console: cookie session, no tokens in the browser), 10 (row Web: REST, fetch on opening), 11 (paths;/admin/...), 20.docs/spec/08-identity.md: 6.2 (console step-up methods), 7.5 (BFF session).docs/spec/02-architecture.md: 3.1 (row "Web console": static files), 6 "Code layout" (places with the product name).docs/spec/19-security.md: 3 (SEC-HDR-1 to SEC-HDR-3; the CSP itself comes with M6-03).docs/api/openapi.yaml:getDiscovery, the console forms of the login operations and the operations M6-01 added (getConsoleSession,stepUpWithPassword,beginStepUpOidcReauth,completeStepUpOidcReauth,endViewAs),createStepUpChallenge,completeStepUpWithPasskey,logout,getInstance.docs/DECISIONS.md: D-W-100 and its extension to M5 and M6 (D-W-120), D-W-114, D-W-115; section 10 (format ofD-M6-02-<n>rows) and the rowsD-M6-01-<n>.- Briefs (merged names win, D-W-100):
M6-01-console-session.md(cookie, CSRF headerX-Console-Request, console login forms, pre-step-up gate, operations),M4-01-workspace-arch.md(members,rules.yaml, platform folders: "M6 for web"),M4-02-dart-api-client.md(client factory),M4-04-design-system-theme.md,M4-06-l10n.md,M4-07-core-components.md,M4-09-auth-dpop.md(LoginServiceand its ports; the console uses the parts without DPoP), the brief of M4-18 (the E2 and E7 flows and texts of the app; the console builds its own screens inapps/consolefromdesign_systemcomponents, because one app never imports another, 15 3.1 rule 2),M1-23-server-image.md(the image build andmake image-test),M1-18-http-chain.md(security headers, unmatched handler). - Code on the base branch:
apps/console(skeleton of M4-01),packages/*,services/server/internal/platform/httpserver,services/server/Dockerfile,infra/compose/.
This ticket touches no table, no action and no event.
Fixed requirements that apply¶
Owner's fixed build rules (apply verbatim):
- No MVP. New from scratch and directly in the final state; stages are only the order.
- Strictly modular: clear module and service boundaries, dependencies only through defined interfaces, no cross access.
- Documentation in code: every public module, class, function and interface with a docstring/doc comment (purpose, parameters, return value, errors, example where useful); comments explain the why, not the what. API documentation is generated from code (OpenAPI from the routes, dartdoc, Python API docs in MkDocs).
- Documentation in the repo: README per module/service, ADR for every architecture decision, CHANGELOG per release, keep the documentation site current. A change without matching documentation is not done.
- Quality gates (SonarQube, linters, type checking, security scanners, test coverage) block the merge.
Rules for every brief:
- Test first: write the failing test, run it and keep the red output for the report, then make it green, then clean up.
- No en dash and no em dash in any file you write; the
text-rulespre-commit hook rejects them, and the ARB test of M4-06 rejects them in interface texts. Use a hyphen or rephrase. - White label: examples and test data use
example.comand the people Alex, Sam and Robin; no company, host or personal names, no paths under a home directory. Interface texts come frompackages/l10n; the console showsbranding.product_nameand the logo from the instance, never a literal. - Limits only through named configuration keys; a new server key (Design note 4) goes into
docs/operations/configuration.mdand its chapter first; code comments name the key, never the number. - Specification first: a change to behaviour, API or configuration updates
docs/spec/ordocs/api/in the same change. The core contract is not edited by this ticket.
Product requirements implemented here:
- Flutter Web only for administration, statistics and terminal; no chat, no route for channels, threads or messages; links into threads open the app through the deep link (01 5.5; ADR 0028).
- No
sync_engine, nolocal_store, no gateway connection, no offline storage, no push in the console (15 3.1 rule 3; ADR 0028). - No token in the browser: the console authenticates only with the cookie of M6-01 and sends the CSRF header on every state-changing request (08 7.5).
- Login E2 and E7 only; step-up directly after login before any administration screen (08 7.5).
- The console is served by the server under
/console/from the server image; its version equals the API version of that server. - Semantics are turned on at startup (ADR 0028), the language of the semantics tree follows the app language, and the console works in the current versions of Chromium, Firefox and Safari (15 2).
Scope¶
Create:
apps/console/web/withflutter create --platforms=web(no service worker for offline use: set the build so the Flutter service worker is not registered; Design note 3),index.htmlwith the loader configuration that M6-03 extends (asset URLs on the own origin).apps/console/lib/:main.dart(semantics on, theme fromdesign_system, router),shell/(navigation of 01 5.5 with placeholder pages),login/(E2 and E7 in the console form, the OIDC callback route, the passkey adapter over WebAuthn throughpackage:webinterop, the step-up screen),session/(the console session state fromgetConsoleSession, logout, expiry handling with the clearing cookie),http/(theapi_clientfactory for the web: cookie by the browser,X-Console-Request: 1on state-changing requests, no DPoP),version/(Design note 5) (Design notes 1 and 2).services/server/internal/platform/httpserver/console.go(or a packageinternal/platform/consoleassets): serving the console build under/console/with the SPA fallback toindex.htmlfor unknown paths below/console/(never for/api/), the right content types,Cache-Control(hashed assets long,index.htmlno-store), and the headers SEC-HDR-1 and SEC-HDR-3 except the CSP, which M6-03 adds (Design note 4).- Tests (Tests).
Change:
services/server/Dockerfile.dockerignoreanddocs/spec/18-release.md7 rule 7 (spec first): the build context also admitsapps/console, every memberpubspec.yamlof the pub workspace (the root resolution needs them all), thelib/of the packages the console needs, the rootpubspec.yaml,pubspec.lockandanalysis_options.yaml, without tests and test data; the Flutter stage runs on$BUILDPLATFORM(18 7 rule 2), because its output is platform independent; the Flutter builder image is pinned by digest like every base image (18 7 rule 5, SEC-SC-4).services/server/Dockerfile: a build stage that builds the console (flutter build web --releasewith the pinned Flutter) and copies the output into the image at the path the server serves;make imageandmake image-teststay green and the image test asserts that/console/answers withindex.html(Design note 4).services/serverconfiguration:console.assets_dir(path of the built console; empty disables the console with404under/console/), first indocs/operations/configuration.mdand the chapter that owns the console serving (02 3.1 or 16), then in the schema (make config-reference,make config-doc-check).tools/arch-check/rules.yamlonly if the console's members need an entry M4-01 did not foresee;make archmust fail for an import ofsync_engineorlocal_storeinapps/console(M4-01 already holds it; add a test case for the real console if none exists).packages/l10nARB files (console navigation and login texts).apps/console/README.md(build, serve, test),docs/spec/15-client-architecture.md11 (console paths under/console/),docs/spec/02-architecture.md3.1 (row "Web console": served by the api process under/console/), root and serverCHANGELOG.mdunder[Unreleased],docs/DECISIONS.mdsection 10 rowsD-M6-02-1toD-M6-02-7.
Out of scope¶
- The CSP, nonce, Trusted Types, COOP and COEP and the headless browser test (M6-03).
- Administration screens (M6-04, M6-07 to M6-09), statistics (M6-06), live views (M6-16), terminal (M6-18).
- Chat in the browser (never, ADR 0028).
- Changes to the core contract.
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M6-02-console-app; one writer per worktree. - Conventional Commits with the component as scope (for example
feat(console): ...,feat(httpserver): ...,build(server): ...) and a DCO sign-off on every commit (git commit -s); squash into meaningful commits before the merge (docs/quality/push-discipline.md). - Never push to
main, never tag and never create remote repositories; the only remote steps are the branch push and the pull request ofAGENTS.md. - Do not modify files outside the scope above, except the CHANGELOG, README and
docs/DECISIONS.mdentries named in this brief. - Follow the host rules supplied by the launcher (ports, machine rules).
- Tests bind only
127.0.0.1and port 0; PostgreSQL only throughpgtest; browser tests in a headless Chromium on the loopback. Temporary directories only viamktemp -d, removed at the end. Stop only processes you started, by PID. - Never read or print secrets. No network calls except package downloads for pinned tools and pub packages; the console loads nothing from a foreign origin (fonts and CanvasKit are bundled; M6-03 enforces it).
- The Flutter SDK at the pinned version must be on
PATH; without it the gates fail by design and the ticket stops withVerdict: BLOCKED <tool> missing. - Never lower a threshold, disable a gate, delete a test, raise
repo.max_file_kbor extendquality/licenses/allowed.txt; never exclude hand-written code from a lint, format, coverage or doc gate. - No permission logic in the client (15 1 principle 1).
- If the core contract or
docs/api/lacks something this ticket needs, stop withVerdict: BLOCKED <reason>instead of building a deviation (P-11). - This brief was written before milestone M1 closed (D-W-100, extended to M5 and M6 by D-W-120). Where the merged code of an earlier ticket names a package, type, function, file, provider or make target differently from this brief, follow the code and list each difference in the report; where it behaves differently from the specification, stop with
Verdict: BLOCKED <reason>.
Design notes¶
-
Routes (01 5.5; ADR 0028; Ruling for docs/DECISIONS.md).
/console/login,/console/login/callback,/console/step-up,/console/step-up/callback,/console/logged-out, and below/console/one route per navigation entry (live,stats,hosts,people,groups,devices,agents,apps,audit,import,instance,status). A route test enumerates the router's configuration and fails on any path containingchannel,thread,message,space(content) or/t/,/c/,/s/; a link to a thread renders "Open in the app" with the deep link<scheme>://t/{id}from the instance's branding (the scheme comes fromgetDiscoveryif it carries one; otherwise the console shows the thread ID with a copy action and says to open it in the app; record which). -
Session and HTTP (08 7.5; M6-01). After a reload the console calls
getConsoleSession:401goes to/console/login; a session withoutstep_up_atgoes to/console/step-up. Theapi_clientfactory for the web uses the browser's fetch (cookies of the same origin are sent automatically), addsX-Console-Request: 1to every method exceptGET,HEAD,OPTIONS, and never addsAuthorizationorDPoP. A401leads to E7 or to the login by the problem type of the response (unauthenticatedor a revoked or expired session to E7 with the reason,step_up_requiredto the step-up screen); the browser cannot readSet-Cookie, so the console never branches on the cookie. The passkey adapter callsnavigator.credentials.getwithuserVerification: requiredfor the step-up options ofcreateStepUpChallenge(M1-40 format) and for the passkey login; the OIDC adapter navigates the tab toauthorization_urland handles/console/login/callback(login) and/console/step-up/callback(the OIDC re-login of the step-up); because the redirect unloads the app, it keeps theclient_verifierin the tab'ssessionStorageunder a key bound to the flow, written right before the redirect and deleted on the callback or on an error, never inlocalStorage, a cookie or the URL (M6-01 Design note 1). -
Web build (15 2; Ruling for docs/DECISIONS.md). JavaScript build with the CanvasKit renderer files bundled into the build output (no CDN); the Flutter service worker is not used (no offline cache of an administration surface; a stale cached console could mismatch the API version);
SemanticsBinding.instance.ensureSemantics()at startup; the HTMLlangattribute follows the app language. -
Serving (02 3.1; SEC-HDR-1, SEC-HDR-3; Ruling for docs/DECISIONS.md). The api process serves files from
console.assets_dirunder/console/through the HTTP chain (rate limits, security headers, unmatched handling): exact files with their content type, everything else below/console/falls back toindex.html;/consolewithout the slash redirects to/console/; paths with..or encoded separators are rejected before the file system.index.htmlgetsCache-Control: no-store; hashed assets get a longmax-agewithimmutable; other filesno-cache. Headers now: SEC-HDR-1,Cross-Origin-Opener-Policy: same-origin,X-Frame-Options: DENY,Permissions-Policyof SEC-HDR-3; COEP and the CSP come with M6-03 (record that SEC-HDR-3 is complete only after M6-03). The Dockerfile builds the console in its own stage and copiesbuild/webto the path the image's default configuration names inconsole.assets_dir. -
Version (PLAN row M6-02; Ruling for docs/DECISIONS.md). The build writes
version.jsonwith thespec_versionofdocs/api/openapi.yamlthat itsapi_clientwas generated from (and the server version fromservices/server/VERSIONfor display); the console reads it at start and compares thespec_versionwithDiscovery.spec_versionofgetDiscovery(the membersDiscoveryandInstanceInforeally carry); a mismatch (a cached oldindex.htmldespiteno-store, a proxy) shows "The console was updated, reload" and blocks actions. A Go test asserts that the servedversion.jsoncarries thespec_versionthe server reports ingetDiscovery. -
Accessibility (01 8; ADR 0028). Semantics on, focus order per column, visible focus ring, keyboard navigation of the navigation rail; the login and step-up screens follow the onboarding rules of 01 5.6 (errors name the cause and the next step).
-
Rulings for
docs/DECISIONS.md(D-M6-02-<n>): routes and the route test (note 1); cookie-only HTTP with the CSRF header and the passkey and OIDC adapters (note 2); no service worker (note 3); serving under/console/with caching rules andconsole.assets_dir(note 4); headers completed by M6-03 (note 4); the version check (note 5); the deep link fallback (note 1).
Tests (write them first)¶
Dart (apps/console/test/, flutter test, plus flutter test --platform chrome for the web-only adapters):
routes_test.dart: no route for channels, threads or messages (ADR 0028 "Confirmation"); every navigation entry has a route.semantics_test.dart: semantics are enabled aftermain()(ADR 0028 "Confirmation").http_test.dart: state-changing requests carryX-Console-Request: 1; no request carriesAuthorizationorDPoP;401leads to E7.login_test.dart: E2 shows only the methods from a fakegetDiscovery; OIDC navigates and completes on the callback with theclient_verifierfromsessionStorageafter a full reload of the app (M6-01 Design note 1), and the entry is deleted afterwards and on an error; the OIDC re-login of the step-up does the same on/console/step-up/callback; passkey with a fakenavigator.credentials; password with the one-time code; after login the step-up screen comes first; E7 shows the reason; goldens of E2, step-up and E7 in light and dark, text scaling 1 and 2, de and en.version_test.dart: equal versions pass; a mismatch blocks actions with the reload notice.
Architecture: make arch fails on a test fixture where apps/console imports sync_engine or local_store (the case of M4-01 against the real console member).
Go (services/server): TestConsoleServing (files with types, SPA fallback only below /console/, /api/ untouched, path traversal rejected, cache headers, SEC-HDR-1 and the listed SEC-HDR-3 headers present, 404 with empty console.assets_dir), TestConsoleVersionJSON.
Image: make image-test asserts /console/ serves index.html from the image.
Commands that must pass, from the repository root:
cd apps/console && flutter analyze --fatal-infos && flutter test --coverage && flutter build web --releasecd services/server && go test -race -count=1 ./internal/platform/... ./internal/wire/...make arch,make lint,make fmt-check,make coverage,make config-doc-check,make image,make image-testmake check
Acceptance criteria¶
- Architecture test:
consoledoes not depend onsync_engineandlocal_store; the route test finds no content routes; semantics are on at startup. - The server serves the console under
/console/from the image; the console version equals the API version and a mismatch is shown. - Login following E2 and E7 with the cookie session, step-up directly after login, no device screens, no token in the browser.
-
console.assets_dir, 15 11, 02 3.1, the README, the CHANGELOGs and the rulings are updated. -
make checkis green.
Definition of done¶
- Code and tests as specified; doc comments on every public Dart and Go symbol; comments explain why.
apps/console/README.mdwritten in English.- Rulings of Design note 7 recorded in
docs/DECISIONS.mdsection 10. - Root and server
CHANGELOG.mdentries under 'Unreleased'. - An ADR only if you take an architecture decision beyond this brief (MADR 4 as in
docs/adr/0000-template-madr-4.md, headings and text in English, next free number,make adr-index). make checkgreen.- An independent code review over the diff, then the red team gate (
docs/quality/red-team-gate.md, verdictJAorNEIN); onNEINfix with a test per finding and have the delta checked again. Record both verdicts. - Land the change as
AGENTS.md("How work lands onmain") describes: a pull request from this branch, squash-merged only when review, red team andmake check,ci-okand the review threads are green; thengit worktree remove.
Report¶
Write to {{REPORT_PATH}}:
- First line exactly
Verdict: DONEorVerdict: BLOCKED <reason>. - Commits (SHA and subject), changed files with line counts, test results with counts (the red run before the change and the green run after), the summary of
make check. - Review and red team verdicts and the findings fixed.
- The size of the web build and of the image layer it adds; the browsers the web-only tests ran in.
- What is not proven (the CSP comes with M6-03).
- Follow-ups that could not be done in this scope, each with its reason, every difference to the names of this brief (D-W-100), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.