Skip to content

M4-29 Administration: audit view

No Date Ruling Reason
D-M4-29-0 2026-10-18 Base int/m1-wave42 at c5a86a6, branch feat/M4-29-admin-audit in its own worktree; the branch is not merged locally and not pushed, the PR lane lands it. The audit operations of docs/api/openapi.yaml and audit.read, audit.verify of actions.yaml equal those of origin/main, so no contract change was brought in. The base keeps the German chapter files, so the A6 line changes in docs/spec/01-produkt.md (the brief names 01-product.md of origin/main). 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-29-1 2026-10-18 The action filter offers the action IDs of the loaded entries as suggestions of a text field that also takes any typed ID; Enter applies the typed text, a picked suggestion applies at once. Every filter maps one to one to a query parameter of listAuditEvents; days are typed as YYYY-MM-DD in the person's time zone, "to" is the start of the day after. An action shows its translated label where l10n has one (the administration actions), else its ID; a resource shows its kind and ID, because AuditEvent carries no name. Brief Design note 1: the catalogue is not shipped to the client as a list of its own.
D-M4-29-2 2026-10-18 "Check chain" sends verifyAuditChain without a range while no filter is set (the server checks from the last checkpoint); with a filter it sends the seq span of the listed entries as from_seq and to_seq, because the request takes numbers, not times. Brief Design note 2; AuditVerifyRequest has only from_seq and to_seq.
D-M4-29-3 2026-10-18 "Export" writes the bytes of exportAuditEvents unchanged; "Export as CSV" converts the same stream line by line into the columns occurred_at, actor_principal_id, action, decision, resource_kind, resource_id, deny_reason, correlation_id, seq, hash, prev_hash (header with these field names, RFC 4180 quoting, CRLF), and a cell that starts with =, +, - or @ gets a leading apostrophe. Both need the filter's time range. The save dialog is a port of the host (AuditSaveTarget, a StreamConsumer of the chosen file), because admin_ui also serves the console on the web and the sandboxed desktop app may write only to a file the person picked in the platform's panel; a host without it shows no export buttons. The desktop app wires no panel in this ticket (its files are outside the brief's scope), so the export is reachable once a host passes one. A stream that breaks off says that the file is incomplete. Brief Design note 3; ADR 0021 (offline verification needs the hash links of the NDJSON).
D-M4-29-4 2026-10-18 A streamed response hides its problem body from the StepUpInterceptor (Dio passes the raw body), so exportAudit first reads one entry of the same range through listAuditEvents (same action audit.read, same purpose text): the step-up and a 403 arrive on a JSON answer, and the stream follows within auth.step_up_max_age. A refused stream itself is read into an ApiProblem too. Found by the fake-server test: the bytes request answered a bare 401 without the step-up.
D-M4-29-5 2026-10-18 AdminRepository includes AuditRepository (ApiAdminRepository delegates to ApiAuditRepository), so the shell hands one repository to every screen and the app needs no change; the audit screens use only the audit calls and allPeople for the person picker. The audit section of the shell gets the body's height instead of the scroll view, so its list is virtualized. Brief Scope "Registration of the Audit entry in the admin shell"; fixed rule 2 (the audit code stays in lib/src/audit/).
D-M4-29-6 2026-10-18 Fixes after the code review: a failed or cancelled load keeps the filter bar (only a 403 or 404 shows the server's reason alone), so a filter the server rejects (400 for a resource ID that is no UUID) can be changed; a failed "Load more" keeps the loaded entries and retries the same page; a new filter clears the result of the last chain check. Code review finding: the view lost its filters while the filter still applied to the list, the chain check and the export.
D-M4-29-7 2026-10-20 Port onto main from the integration squash of int/m1-wave45: the seven own commits of the ticket on top of the M4-27 port (the ticket branch starts at the wave 42 head, which holds M4-26). The conflicts with M4-27 are unions as in INT-W45/0 without the parts of M4-20: AdminRepository implements AccessRepository, AuditRepository, the shell routes grants and audit, the fake server registers both handler sets and the contract test calls both, the ARB files take only the keys the ticket adds, with the localizations generated again, and admin_shell.png is recorded again. The A6 addition of the German chapter goes into the English docs/spec/01-product.md. After D-M4-25-10 the golden audit_list is recorded again and audit_list_test.dart expects U+00A0 before AM. The PR lane ports only the ticket and the integration fixes it needs; the goldens of admin_ui pass in a Linux amd64 container with Flutter 3.47.7.
D-M4-29-8 2026-10-23 exportAudit takes the problem of a refused export with ApiProblem.of instead of reading the streamed body itself: since M4-22 createApiClient decodes that body first (BinaryProblemInterceptor), so the own reader found a decoded body, gave up and reported only http_<status>. A console test pins that a download whose step-up ended between the check read and the export runs the step-up and repeats instead of ending the session. The console E2E flow A6 was logged out with http_401 twice before M4-22 (the 3 s step-up of the E2E stack ended between the check read and the export); with M4-22 the export's refusal lost its reason (a 403 with a reason became http_403), reproduced by api_audit_repository_test.dart once its client has the interceptors of createApiClient.