06 — Screen Specifications (Audit Module)
- 1. Audit Log List
/settings/audit - 2. Audit Filter Bar
- 3. Audit Entry Detail
/settings/audit/:id - 4. Export Sheet
/settings/audit/export(planned) - 5. Entry Timeline
(proposed) - Global micro-interaction & motion notes (all screens)
Production-grade specs: chrome, states, components, micro-interactions, a11y, responsive, dark/light, keyboard, motion. Tokens:
Sp= 00-shared/02 §3,Ty= typography §2,El= elevation §5,R= radius §4,Mo= motion §6. All components reference 00-shared/03. Data contract in 12_API_Mapping.md.
1. Audit Log List /settings/audit
Chrome: AppBar (title "Audit log", subtitle count from total when loaded); body =
filter bar + result area; horizontal padding 16 (phone) / 24 (tablet+). No FAB (read-only).
Layout (phone portrait):
AppBar "Audit log" [count badge]
AuditFilterBar [Action ▼] [Entity ▼] [Actor] [Clear]
List (AuditEntryRow cards):
Card: ActionChip "StudentUpdated" · 10:32
Actor: A. Kumar (u_8f2a…) · entity: student_9f1
context: correlationId 9a1… (mono bodySmall)
⋮
Footer: "Showing 50 of 1,234" [Load more]
Layout (desktop ≥ 1200 px) — AuditTable (primary target):
| Time | Action | Actor | Entity | Context |
|---|---|---|---|---|
| 02 Aug 2026, 10:32:14 | StudentUpdated | A. Kumar | student_9f1… | corr 9a1… |
| 02 Aug 2026, 10:30:02 | UserLoggedIn | A. Kumar | — | corr 8c2… |
- Sticky header row (
e-2); hover row highlight (m-fast); row tap → detail. - Column widths: Time 180, Action 200, Actor 200, Entity 200, Context flexible (mono).
- Desktop shows both local-time and UTC in the Time column tooltip (server sends ISO UTC
occurredAt,audit-log.schema.ts:42-43; display per client locale — no server tz field).
States:
- loading:
AppSkeleton(list)rows (≤ 200 ms target); count hidden. - success+empty (
total === 0):AppEmptyStateiconreceipt_long, "No activity recorded", subtitle "Actions that happen in StudyLyon will appear here." - success+filtered-empty: "No activity matches these filters" +
TextButton"Clear filters". - error:
AppErrorState.code— 401 → session-expiry flow (00-shared/06 §3.6); 429 → countdown + retry button (api tier,rate-limit.constants.ts:6); 5xx → generic + requestId. - offline:
AppOfflineBanner+ last-good cache; Load more disabled with tooltip. - permission: route hidden without
audit.read(OQ-5); direct link → 403 screen.
Pagination (server contract — 12_API_Mapping):
- Default
limit=50(audit.controller.ts:20); client "Load more" incrementspageby 1. - Total from response
data.total(notmeta.totalItems— shape deviation, OQ-2). - Reached end (
items.length === total) → footer "End of audit trail" (bodySmall). - No infinite scroll auto-trigger on desktop (table semantics); phone may auto-load (C2 — per-design decision).
Realtime (WS, 03 §5): on WsBridge event with matching topic, cubit prepends the
entry (dedupe by _id), shows AppSnackbar "1 new entry" (tap → scroll to top + flash row
primaryContainer); if filters active and entry doesn't match → snackbar "New activity —
refresh filters". WS payloads never rendered verbatim (OQ-8).
Motion: rows m-entrance stagger 40 ms on first load; appends slide-in m-base;
filter changes cross-fade m-slow (no pop).
A11y: table exposes column headers (00-shared/09 §7); row semantics
"StudentUpdated, by A. Kumar, 10:32 — button"; live region on count + realtime banner;
focus to filter bar on /; focus ring primary.
Dark/light: token-only (surface, surfaceContainerLow cards, outlineVariant
dividers); both pass contrast.
2. Audit Filter Bar
Chrome: horizontal Wrap of controls (or bottom sheet on phone — shared 05 §5):
| Control | Component | Behaviour |
|---|---|---|
| Action | AppDropdown searchable | values = distinct action strings from loaded + cached pages (enum grows with new events — free list) |
| Entity type | AppDropdown searchable | values = distinct entityType; helper note when set: "Entity type appears only on entries that carried it (currently rare)" (OQ-1) |
| Actor | AppDropdown searchable OR AppTextField (id) | exact string match (audit.controller.ts:24); names (proposed) resolved from users module |
| Clear | TextButton | resets all → unfiltered page-1 reload |
State: active filters rendered as AppChips above results (removable individually);
any change → changeFilter → page=1 → reload. Esc clears all.
(planned): date-range presets (Today / 7d / 30d / Custom) + free-text search — need
backend from/to/q params (OQ; AUDITING.md:86).
3. Audit Entry Detail /settings/audit/:id
Chrome: AppBar "Audit entry" + menu (Copy JSON, Export (planned)); body in
SingleChildScrollView.
Header block:
[AuditActionChip: StudentUpdated] actorType chip: user
Actor: A. Kumar (u_8f2a1c…) [mono, id copyable]
Time: 02 Aug 2026, 10:32:14 (2026-08-02T05:02:14.000Z · UTC) [tooltip]
Entity: student_9f1… (when present; mono)
Trace: correlationId 9a1c… [copy icon]
Body — AuditDiffView (the diff):
- Input:
before+aftermaps (audit-log.schema.ts:45-49; masked server-side,audit.service.ts:20). - Client computes union of keys: changed (in both, different value), added (after only), removed (before only).
- Rows, one per key: key (
monolabelMedium) | badge (changedtertiary, addedsuccesssecondary, removederror) | before value (struck through,error-tint) → after value (success-tint). Scalar values inline; nested maps/arrays collapse to "Object (3 keys)" / "Array (5 items)" → tap expandsJsonTreeView(indent 16,mono). - Empty map →
AppEmptyState compact"No snapshot captured for this action." afterfallback: whenbeforeabsent andafter === payload(audit.handler.ts:42-43), header note "Snapshot after action" and render as flat JSON tree — most entries today (emitters send payload withoutbefore, e.g.users.service.ts:70-75).- No values rendered if neither: single line + note.
- All values are display-only:
SelectableText(desktop Ctrl+C); no editing affordances.
Immutability communication: footer bodySmall onSurfaceVariant: "This record is
append-only and cannot be edited or deleted." (derived from audit-log.repository.ts:6 +
PLAN 19.3, PLAN.md:191).
Not-found state (deep link, no :id endpoint, OQ-4): AppEmptyState "Entry not found —
it may be beyond the loaded pages." + "Back to list". Client retries match by loading next
pages up to a cap (proposed).
States: none loading (in-memory); motion m-base route; expand/collapse m-fast.
4. Export Sheet /settings/audit/export (planned)
AppBottomSheet (2/3 height): format radio (CSV, PDF), scope card (active filters + "all
pages"), timezone dropdown (client-side date formatting), CTA Export — disabled with
AppBanner info "Export is planned — the API does not offer it yet" (AUDITING.md:87 has no
endpoint). Enabled when backend lands; per blueprint the export itself becomes an audited
action.
5. Entry Timeline (proposed)
Not bound to a screen today; would be a tab on detail: reverse-chronology of entries sharing
entityId/correlationId. Requires backend filter support (OQ-1) — speced as
(proposed), no code.
Global micro-interaction & motion notes (all screens)
Mo-m-baseroute transitions; cardsEl-e-1; sticky table headerEl-e-2.- Snackbar: success < 3 s, error ≥ 4 s (
00-shared/03 A). - Keyboard:
/filter focus,Escclears filters/closes sheets, arrows+Enter table nav (00-shared/08 §2). - Safe areas: bottom nav-height respected in authed shell; table scrolls horizontally on
< 1200 px (min-width 720 px,
Scrollbarvisible). - Dynamic type 200%: table degrades to stacked cards automatically at any text scale (no fixed-height rows).