| D-M6-02-0 |
2026-10-16 |
Base int/m1-wave33 at 2f845d8, branch feat/M6-02-console-app in its own worktree; not merged locally and not pushed, the PR lane ports it (the brief's landing steps follow the task). Section 5.6 of chapter 01 (screens E1 to E7) and the rule R-PR-14, which main holds since its baseline and this base lacks, enter first as their own commit. Names that differ on this base and are followed: the chapters are 01-product.md, 02-architecture.md, 08-identity.md, 15-client-architecture.md, 16-operations.md, 18-release.md, 19-security.md; console.go of httpserver already holds the CSRF guard of M6-01, so the serving is console_assets.go; the configuration key lives in chapter 16 beside the other process keys. |
The task names the base and the lane; merged names win (D-W-100). |
| D-M6-02-1 |
2026-10-16 |
Routes: /console/login, /console/login/callback, /console/step-up, /console/step-up/callback, /console/logged-out and one path per navigation entry (live, stats, hosts, people, groups, devices, agents, apps, audit, import, instance, status). The enum ConsolePage is the whole router configuration (an own RouterDelegate over the state of the session instead of go_router, which the workspace does not carry); routes_test.dart enumerates it and fails on channel, thread, message, space, /t/, /c/, /s/. The enum value of the import page is dataImport, because tools/arch-check reads a line that starts with import as a directive. URLs use the path strategy below <base href="/console/">, so the OIDC callbacks reach the app. |
Brief note 1; ADR 0028 "Confirmation"; no new dependency for a table of seventeen paths. |
| D-M6-02-2 |
2026-10-16 |
HTTP: createConsoleApiClient builds the generated client over the browser adapter of Dio (cookies of the same origin go along), adds X-Console-Request: 1 to every method except GET, HEAD and OPTIONS, removes Authorization and DPoP from every request and retries no read. A 401 during a session leads to E7 with the problem code as reason, step_up_required back to the step-up; a 401 of the start or of a login form changes no state. Passkey: navigator.credentials.get with userVerification: required whatever the options say; the JSON conversion is plain Dart (webauthn_json.dart), the browser part is browser_web.dart. OIDC: the client_verifier lives in sessionStorage under bothy.console.oidc.<login|step_up>.<login_attempt_id>, written right before the tab leaves and deleted on every callback, successful or not. A passkey login without login name sends no login_name (discoverable credentials, as the contract allows). The console uses the generated login methods with client: console and PkcePair of auth, no other part of auth. |
Brief note 2; M6-01 Design note 1 and D-M6-01-11. |
| D-M6-02-3 |
2026-10-16 |
Web build: flutter build web --release --base-href /console/ --no-web-resources-cdn (JavaScript build, CanvasKit from the build output). The own web/flutter_bootstrap.js calls _flutter.loader.load({}) without serviceWorkerSettings, so no service worker is registered; Flutter still writes flutter_service_worker.js, which nothing loads. runConsole calls SemanticsBinding.instance.ensureSemantics() before runApp, and the app sets the lang attribute of the document to the app language; index.html starts with lang="de", the default locale. The E7 sentence about deleted device data is left out: the console keeps no data in the browser. |
Brief note 3; a cached console could mismatch the API. |
| D-M6-02-4 |
2026-10-16 |
Serving: console.assets_dir (empty default: no console, 404 problem under /console/; a set directory without a regular index.html ends the start). httpserver.ConsoleAssets opens it as an os.Root and mounts GET /console/ and GET /console (301 to /console/) on the public mux; exact files get their type from a fixed table (no MIME database in the image), every other path below /console/ gets index.html; encoded slashes or backslashes, backslashes, NUL and dot segments are refused before any file call, and a name the root refuses (a symbolic link out of the directory) is 404, never index.html. index.html has Cache-Control: no-store, a name with a hash of at least eight hex digits between dots public, max-age=31536000, immutable, every other file no-cache; Flutter fingerprints no file today, so in practice all assets are revalidated. Revalidation goes by a strong content tag (ETag, the first 16 bytes of SHA-256, hashed once per file version) and never by Last-Modified: a rollback to an image built earlier serves older files, which a modification time would answer with 304 (finding of the code review). The console routes run steps 1 to 4 of the chain (SEC-HDR-1, trace, client IP, setup gate, 404 and 405 problems) but no per-route stage: they are no operation of the contract, have no action and no rate-limit class, and a page load of a few dozen assets must not draw on rl.public_auth. The image copies build/web to /usr/share/bothy/console, owned by root and read-only, and sets BOTHY_CONSOLE__ASSETS_DIR to it. |
Brief note 4; a static file needs no authorization, and the core contract has no class for it. |
| D-M6-02-5 |
2026-10-16 |
Headers of the console now: SEC-HDR-1 from the chain, Cross-Origin-Opener-Policy: same-origin, X-Frame-Options: DENY and the Permissions-Policy of SEC-HDR-3. The CSP and Cross-Origin-Embedder-Policy: require-corp come with M6-03; SEC-HDR-3 is complete only after M6-03. |
Brief note 4. |
| D-M6-02-6 |
2026-10-16 |
Version: apps/console/tool/version_json.sh writes version.json after the build with spec_version (info.version of docs/api/openapi.yaml, from which the api_client is generated) and server_version (services/server/VERSION); the Dockerfile runs it, and TestConsoleVersionJSON runs the same script and holds the served spec_version equal to routes.SpecVersion, which getDiscovery reports. The console reads /console/version.json at start and compares its spec_version with Discovery.spec_version; a missing or different value shows "The console was updated" with only a reload button, and no further request runs. |
Brief note 5. |
| D-M6-02-7 |
2026-10-16 |
Deep links: Discovery names no URL scheme of the apps, so a thread in the console shows its ID with a copy action and the note to open it in the app (ThreadLink); there is no "Open in the app" button until discovery carries a scheme. |
Brief note 1 ("record which"). |
| D-M6-02-8 |
2026-10-16 |
Builder: no Flutter image exists for the pinned 3.47.7, so the console stage runs on the pinned builder image of the server (golang:1.27.3-trixie by digest since the toolchain update of #200), installs unzip and xz-utils, clones Flutter at the tag with --depth 1 and checks the revision abaf9c5, and builds with flutter pub get --enforce-lockfile at the workspace root. make image-test asserts the console in the exported file system (index.html with the base href, main.dart.js, flutter_bootstrap.js, CanvasKit, version.json with the server's API version, root ownership) and the environment variable; starting the server in the image needs a database and is not part of the image test, the answer of /console/ itself is TestConsoleServing. |
Brief "Change" (builder pinned by digest, SEC-SC-4); the image test has no database. |
| D-M6-02-9 |
2026-10-16 |
Tests of the web-only adapters (test/web/, @TestOn('browser')) run with flutter test --platform chrome test/web; make test runs flutter test on the VM, which skips them. test/web/flutter_test_config.dart replaces the golden setup of test/, which reads fonts through dart:io. |
Brief "Tests"; the gate runs on the VM. |
| D-M6-02-10 |
2026-10-21 |
Port onto main on top of M6-01: the ticket range (2f845d8..e7cdad5) as one commit. The spec additions of the German chapters go into the English chapters 02, 15 and 16 (the row of console.assets_dir with the column "Place" of 16); section 5.6 of 01 is already on main. The README of the console keeps the screen note of main and takes the session, build and test sections of the ticket; the new message keys are added to the ARB files of main and the localizations regenerated. References to the German chapter files in the ticket's files name the English files. |
The PR lane ports only the ticket's own commits; the English spec on main wins. |
| D-M6-02-11 |
2026-10-21 |
The console's CI goldens are compared with DsGoldenFileComparator of design_system, like admin_ui and the client. |
Two CI goldens recorded on macOS differed on the Linux runner in 36 anti-aliased edge pixels; PL/5 is the shared tolerance for that, and the platform goldens stay exact. |
| D-M6-02-12 |
2026-10-22 |
Land onto current main: the ported ticket change of D-M6-02-10 and D-M6-02-11 applied on top of main after M6-01 landed (#197). On main the server Dockerfile gained the smoke stages of M1-73 and the files directory of M3-03, and mountAPI gained the host control connection of M2-43: the console stage stays before the final stage, the final stage copies the console beside the files directory, the build context admits both the smoke probe and the console inputs, and MountConsole runs before the gateway and the host control connection. |
Only the ticket's own change moves; the additions of main stay as they are. Since #236 the changelog entries and these rulings are fragments (changelog.d/M6-02.md, services/server/changelog.d/M6-02.md, this file). |
| D-M6-02-13 |
2026-10-22 |
Found by the SonarQube analysis of the pull request: apps/console/web/flutter_bootstrap.js is the loader template of Flutter, whose lines {{flutter_js}} and {{flutter_build_config}} the build replaces; the analysis read them as JavaScript expression statements (S905, reliability) and failed the quality gate. The file is added to sonar.exclusions and to the sorted list of tools/ci/tests/test_server_lint_config.py. The complexity notes on the login views and the WebAuthn conversion and the tag plus digest pin of the console stage (the same form as the build stage, which test_dockerfile_pins.py requires) stay as they are. |
The built flutter_bootstrap.js is generated output; the template is no source code the analysis can parse. |