M4-25: Settings, devices and sessions on the desktop¶
Status: ready | Depends on: M4-18 | Milestone: M4
Goal¶
After this ticket the desktop app has the settings of 01 5.1 D8: Appearance (theme, density, motion, Enter sends, column widths reset), Language (language and time zone of the profile), Keyboard (Enter sends; the list of shortcuts itself is the overlay of M4-24 behind Cmd+/ or Ctrl+/), Devices and sessions (own devices with platform, name, last activity, hardware binding, own sessions; revoke a device or end a session; confirm a waiting device by typing its verification code; add a device by its pairing code), Privacy (crash reports and performance data) and About (version and licenses), with a Notifications entry that M4-33 fills. Personal settings are synchronized across devices through user_settings (04 14), and every device action that 05 marks with step-up (device.approve, device.revoke) asks for it with the step-up device key, because the server answers step_up_required (08 6); ending a session has no step-up in 05 and asks only for one confirmation. The device and session screens and the step-up dialog live in packages/admin_ui, because the console (M6) and the emergency actions on mobile (M5) reuse them. Why: confirming a waiting device and revoking a lost one are the security actions a person needs most (08 5, 10), and settings that differ between two devices of the same person look like a bug.
Read first¶
docs/PLAN.md: section 3; section 10 rows M4-18, M4-25, M4-26, M4-31, M4-33; section 11 (M5 emergency actions reuseadmin_ui); section 12 row M6-01 (console usesadmin_ui).docs/spec/01-product.md: 5.1 D8 "Settings", 5.6 E4 and E5 (the active-device side: "Settings, Devices and sessions, Confirm", "Add device"), 6.7 (destructive actions confirm once; step-up asks the device), 9 (language followsusers.locale), 11 "Personal settings" (keys, values, defaults; the server rejects unknown keys), 12 (crash reports only with a configured target), 13 (R-PR-07).docs/spec/08-identity.md: 5.2 (approval with the verification code; a wrong code revokes the waiting device), 5.3 (pairing approval with step-up,404ends the step-up), 5.4 (management, revoke reasons), 6 (step-up: 6.1 to 6.3), 7 and 8 (sessions), 10 "Lost device".docs/spec/04-sync-protocol.md14 (settings: last write wins per key, defaults not stored,deleted: true).docs/spec/15-client-architecture.md3 (admin_uiused by desktop, console and mobile emergency actions), 3.1 (admin_uidepends ondesign_system,auth,api_client,bothy_viz; not onsync_engineorlocal_store), 8 (step-up), 16 (crash reports), 17 (client metrics switch).docs/api/openapi.yaml:getMe,updateMyProfile(ProfileUpdate),listMySettings,putMySetting,deleteMySetting,listMyDevices,getDevice,approveDevice(DeviceApprove),approvePairing(PairingApprove),revokeDevice,listMySessions,revokeSession,createStepUpChallenge,completeStepUpWithDeviceKey; problem codesstep_up_requiredand its reasons.docs/DECISIONS.md: D-W-100, D-W-114; section 10.- Briefs:
docs/briefs/M4-18-desktop-shell.md(routes, providers, wiper, device key adapters),docs/briefs/M4-09-auth-dpop.md(StepUpService,StepUpPrompt,StepUpInterceptor),docs/briefs/M4-04-design-system-theme.md(resolveDsContext),docs/briefs/M4-06-l10n.md(resolveAppLocale),docs/briefs/M4-15-sync-outbox.md(setting executors); for the server sidedocs/briefs/M1-38-identity-device-approval.md,docs/briefs/M1-45-identity-revocation.md,docs/briefs/M1-61-sync-personal-data.md.
This ticket touches no API operation, no table of chapter 03, no action, no event and no configuration key.
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, never as literals in widgets. - No new personal setting key: only the keys of 01 11 (and
notify.*of 13 3.3 in M4-33); the server rejects others. - Step-up only through the
StepUpServiceof M4-09; no own challenge handling.
Product requirements implemented here:
- Settings of 01 11 with their values and defaults; defaults are not stored on the server; a changed value goes through the outbox as
putMySetting, a reset asdeleteMySetting; changes from another device apply per key (04 14). composer.enter_sendswithout a stored value istrueon desktop; a stored value applies on all devices (01 11).privacy.crash_reportsis offered only when the instance has a crash target;privacy.client_metricsdefaulttrue(01 11, 15 16, 15 17).- Language and time zone are profile fields (
updateMyProfile); the app language followsusers.locale(01 9). - Devices: list with platform, name, app version and last activity, this device marked (the
Deviceschema has no hardware or attestation field, so the list shows no hardware label; the report names the gap against 01 A1); revoke with one confirmation and step-up; a revoked own device ends in E7 on that device (08 10). - Confirm a waiting device: the person types the code shown on the waiting device, step-up,
approveDevice; a wrong code revokes the waiting device and is shown as such (08 5.2). - Add a device: the pairing code from the new device, step-up,
approvePairing; a code without an open pairing ends the step-up of this session and the next attempt asks again (08 5.3). - Sessions: list own sessions with device, method and last use; end one after one confirmation (
session.revokehas no step-up in 05); ending the current one logs out and wipes (R-SY-14). - No permission logic: actions appear according to the server's answers, and a
403shows its reason.
Scope¶
Create:
packages/admin_ui/lib/src/self/(my_devices_view.dart,device_tile.dart,confirm_device_dialog.dart,add_device_dialog.dart,my_sessions_view.dart,revoke_confirm.dart),packages/admin_ui/lib/src/step_up/step_up_dialog.dart(implementsStepUpPromptof M4-09: explains the action in the person's language before the system prompt),packages/admin_ui/lib/src/data/self_repository.dart(REST throughapi_client, no local database), exported fromlib/admin_ui.dart.apps/client/lib/settings/(settings_shell.dartwith the sections of D8,appearance_section.dart,language_section.dart,keyboard_section.dart,devices_section.darthosting theadmin_uiviews,privacy_section.dart,about_section.dart,notifications_entry.dartas the link M4-33 fills,settings_providers.dartoveruser_settingswith the defaults of 01 11).- Fake server handlers through the registry of M4-17 for the operations above that it lacks, including a second simulated device of Alex.
- Tests under
packages/admin_ui/test/self/,apps/client/test/settings/, goldens under bothtest/goldens/, integration tests underapps/client/integration_test/(Tests). - ARB keys for every new text in
packages/l10n.
Change:
packages/admin_ui/pubspec.yaml(api_client,auth,design_system,l10n),packages/admin_ui/README.md(what the package offers to the desktop app, the console and the mobile emergency actions).apps/client/lib/app/router.dart(/settings/<section>),apps/client/README.md.docs/DECISIONS.mdsection 10: rowsD-M4-25-<n>; rootCHANGELOG.mdunder[Unreleased].
Out of scope¶
- Notification settings (D9) and the channel menu (M4-33).
- Administration of other people's devices (A1 in M4-26).
- Export of own data (Privacy "Export", GDPR flows in M7).
- Crash report and metric sending themselves (M4-31); this ticket only stores the switches.
- App lock settings (M5-08).
Boundaries¶
- Work in a dedicated git worktree on the branch
feat/M4-25-settings-devices; one writer per worktree. - Conventional Commits with the package or component as scope (for example
feat(client): ...,feat(sync-engine): ...,build(gates): ...,docs(spec): ...) 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. Never publish a Dart package (publish_to: nonein everypubspec.yaml). - 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.1with port 0 and make no network call; fakes replace the server unless a section below names a container. Temporary directories only viamktemp -d(DartDirectory.systemTemp.createTemp, pytesttmp_path), 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 and what the gates of
make checkdo themselves. - The Flutter SDK at the version pinned in the root
pubspec.yamlmust be onPATH(and Go and Docker where a section names them); without them the gates fail by design (R-QA-02), and the ticket stops withVerdict: BLOCKED <tool> missinginstead of skipping. - 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. - The product name appears only at the places of 02 "Code layout" ("Places that carry the product name"); the app shows
branding.product_namefrom the instance, never a literal. - No permission logic in the client (15 1 principle 1): controls follow server responses, snapshots and events, never an own derivation from grants.
- If the core contract (chapters 00, 03, 04, 05) 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 M4 by D-W-114). 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¶
-
Settings model.
SettingsRepository(app side) readsuser_settingsfrom the local database through a provider and applies the defaults of 01 11 when a key is absent (composer.enter_sendsby platform). Writes go to the outbox (putMySetting,deleteMySettingfrom M4-15); the local row changes at once and the echo is ignored byupdated_by_device_id(04 14). Appearance changes rebuild the theme throughresolveDsContext(M4-04) at once. "Column widths: Reset" deleteslayout.column_widths. -
Profile. Language and time zone through
updateMyProfile; the language selector offers German and English; a change switches the app locale throughresolveAppLocale(M4-06). Time zones from the IANA list the platform offers. -
Device actions.
admin_uiviews take aSelfRepository(REST) and aStepUpService; every changing call (approveDevice,approvePairing,revokeDevice,revokeSession) runs through theStepUpInterceptorof M4-09, which showsStepUpDialogwith the purpose text and then the OS prompt of the step-up key when, and only when, the server answers401 step_up_required(05:device.approveanddevice.revokehave step-up,session.revokehas none; the screen adds none of its own).revokeDevicesends the required reason. Codes: the verification code field accepts digits only (DeviceApprove.verification_codepattern), the pairing code field normalizes like M4-09. A wrong verification code: the response200withstate = revokedis shown as "The waiting device was rejected because the code did not match" (08 5.2). A pairing404: "No open pairing with this code" and the next attempt asks for step-up again (08 5.3). Revoking this device or ending the current session calls the wiper of M4-18. -
Privacy switches.
privacy.crash_reportsis shown only when the instance or the build configuration names a crash target (the flag comes from M4-31 later; until then from the build defines of M4-18, absent means hidden);privacy.client_metricsis always shown. -
About. App version from the build, instance version (
spec_versionofgetInstance), licenses through Flutter'sLicenseRegistrypage. -
Rulings for docs/DECISIONS.md (
D-M4-25-<n>): settings defaults on the client (note 1); device and session screens inadmin_uiwith REST only (Scope, 15 3.1); step-up dialog inadmin_ui(note 3); visibility of the crash switch (note 4).
Tests (write them first)¶
Widget tests:
apps/client/test/settings/settings_test.dart: defaults of 01 11 when keys are absent; a change writes one outbox entry and applies at once; reset deletes the key; theme, density and motion switch the theme.packages/admin_ui/test/self/devices_test.dart: list with this device marked and no hardware label; revoke asks once and then for step-up; ending a session asks once and never for step-up; confirm with a correct code activates, with a wrong code shows the rejection; pairing with404shows the text and asks for step-up again next time; actions hidden when the server's answer does not allow them.packages/admin_ui/test/self/step_up_dialog_test.dart: the dialog explains the action; cancel returnsStepUpCancelled.- Goldens of the settings sections and the device views, light and dark, two densities, text scaling 1 and 2,
deanden.
Integration tests (apps/client/integration_test/, against FakeServer with two simulated devices of Alex):
settings_sync_test.dart: theme and Enter sends changed on one device appear on the other (settings in sync across two simulated devices); a reset appears too.devices_test.dart: the second device logs in and waits; the first confirms it with the code and step-up; then revokes it, and the second device ends in E7 with a wiped store; ending the own session logs out.
Commands that must pass, from the repository root:
cd packages/admin_ui && flutter analyze --fatal-infos && flutter test --coveragecd apps/client && flutter analyze --fatal-infos && flutter test --coverage- on macOS and on Windows:
make apps-platform; elsewhere it prints its SKIP line make arch,make lint,make fmt-check,make coveragemake check
Acceptance criteria¶
- Integration tests are green; settings stay in sync across two simulated devices.
- Devices and sessions: confirm with the verification code, add by pairing code, revoke with step-up and end a session without one, as 05, 08 5 and 10 say.
- The settings of 01 11 with their defaults; language and time zone through the profile.
-
admin_uiuses nosync_engineorlocal_store(make arch); rulings recorded. -
make checkis green.
Definition of done¶
- Code and tests as specified; doc comment on every public symbol (purpose, parameters, return value, errors, example where useful); comments explain why and cite 01, 04 or 08.
packages/admin_ui/README.mdandapps/client/README.md(English) as in Scope.- Rulings of Design note 6 in
docs/DECISIONS.mdsection 10. - Root
CHANGELOG.mdentry 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.
- Where the integration tests ran, and how the step-up prompt was exercised (fake signer in tests, real device key checked by hand or not).
- What is not proven (for example a platform test that ran only in CI, or a check done by hand).
- 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, D-W-114), and spec gaps found.
Then write {{DONE_PATH}} with exit=0 sha=<sha> or exit=1 <reason>.