Skip to content

M4-26 Administration: people, groups and devices

No Date Ruling Reason
D-M4-26-0 2026-10-18 Base int/m1-wave40 at 35d6a9c, branch feat/M4-26-admin-people in its own worktree; the branch is not merged locally and not pushed, the PR lane lands it. The rows stand in a section of the ticket because the base has no section 10 in this file. The base's actions.yaml and the API operations the brief names equal those of origin/main in step_up, owner and shape, so no contract change was brought in. Instructions of the launcher, which override the brief's landing step; merged layout wins over the brief (D-W-100, D-W-114).
D-M4-26-1 2026-10-18 AdminRepository (REST over api_client, ApiAdminRepository) is the only data source of the administration screens; the screens take it through their constructor. They take no StepUpService: as in M4-25 the step-up runs in the StepUpInterceptor of the client the repository wraps, and every changing call passes its purpose text (stepUpPurposeExtra). The console passes its own repository (M6-04). Failures reuse SelfActionFailed of M4-25. Search (q) and the state filter (status) are request parameters of listUsers; the group filter has none and filters the loaded page by the members of the group, with a visible note. Brief Design note 1; the merged step-up path of M4-09 and M4-25 needs no service in the screens (D-W-100).
D-M4-26-2 2026-10-18 Every changing action shows one ConfirmChange dialog that says what the change does (form dialogs such as "Add person", "Create group" and "Add member" are that dialog with their fields), then sends the call; the step-up dialog appears only when the server answers 401 step_up_required. Destructive actions (disable, revoke, unlink, delete, remove, reject, end a session) use the danger banner and button and ask once. A 403 of a list shows the server's reason instead of the section; a refused change hides its control. 01 5.2 and 6.7, SR5, fixed requirements 9 to 11.
D-M4-26-3 2026-10-18 The fake server reads docs/spec/actions.yaml (ActionCatalog, walking up to the repository root) and the x-authz-action of each operation; the administration handlers ask for the step-up exactly for step_up: true (and fresh). server.stepUpOf(operationId) and server.actionOf(operationId) expose the table, so the enumeration test (headless in apps/client/test/admin/admin_step_up_test.dart, through the screens in integration_test/admin_step_up_test.dart) holds the rule without a copy of 05. createUser and createEnrollmentCode answer a replay of the same Idempotency-Key with 409 conflict, reason: one_time_secret; revokeSession also accepts an owner for another person's session. Brief Design note 3; 06 7; 05 session.revoke (self or owner).
D-M4-26-4 2026-10-18 CodeText, groupCode and spellCode moved from apps/client/lib/onboarding/code_text.dart to packages/admin_ui/lib/src/common/code_text.dart (the app re-exports them), with keepGroups for an enrollment code en_... whose own hyphens separate the groups. EnrollmentCodeView is a dialog that holds the only copy of the code; closing it discards the code. Brief Design note 4: packages never import from apps/ (15 3.1 rule 2).
D-M4-26-5 2026-10-18 The waiting-device queue and the shell's count are assembled from listUsers, listUserDevices (state pending) and listUserSessions (origin from auth_method of the device's session); disabled and deleted people are skipped. The queue leaves out "Key: hardware, attested" of A1. The shell lists only entries with a screen (people, groups, devices); later tickets set hasScreen. The desktop hosts the shell at /admin, /admin/<section> and /admin/<section>/<id>; this ticket adds no entry point in the sidebar, the path is reachable by link. Brief requirement 15 (the API has no instance-wide list and no hardware field); the sidebar is outside the brief's scope.
D-M4-26-6 2026-10-18 Fixes after the code review: the pickers of "Add member" and "Add owner" load every page of active people (allPeople follows the cursor of listUsers) instead of the first page or the list's filtered page; a membership end typed as a day holds through that day in the person's time zone (expires_at is the start of the next day there, L10nFormat.dayStart), and the member row shows that last day. Code review: two reproduced Majors (people after the first page could not be added; a membership ended at the start of the typed day in UTC). 07 2.1.
D-M4-26-7 2026-10-20 Port onto main from the integration squash of int/m1-wave42, which holds the same five own commits of the ticket as its branch (that branch starts at the wave 40 head with M4-25 and M4-19); they sit on top of the M4-19 port. No integration fix of waves 42 or 43 is needed (INT-W42 concerns the server, INT-W43/1 only arises together with M4-20); the CHANGELOG keeps only the entry of this ticket; admin_step_up_test.dart gets its macOS and Windows lines in apps/client/platform-tests.json (D-M4-18-19). The PR lane ports only the ticket and the integration fixes it needs.
D-M4-26-8 2026-10-20 The goldens admin_people, admin_person and admin_waiting_devices are recorded again after D-M4-25-10 (the no-break space before AM and PM). They show English clock times, which changed by one character; the CI goldens of admin_ui pass in a Linux amd64 container with Flutter 3.47.7.
D-M4-26-9 2026-10-20 contract_documents.g.dart also carries docs/spec/actions.yaml (make contract-documents, held by make codegen-check); ActionCatalog.read falls back to that copy (ActionCatalog.embedded) when no repository is above the working directory, as ContractSchemas does for the API documents (D-M4-18-17). In the macOS platform job admin_step_up_test.dart failed at the start of the fake server: the app sandbox cannot read the repository, so ActionCatalog.load found no docs/spec/actions.yaml. Test: test/fake_server/action_catalog_test.dart.