03 — User Journeys (Audit Module)
- 1. Browse the audit log
- 2. Filter by actor / action / resource
- 3. Search (free text) —
(planned) - 4. Inspect an entry — before/after diff
- 5. Realtime append (WS)
- 6. Export audit trail —
(planned)
End-to-end journeys for the Audit module computed from
audit.controller.ts+audit.service.ts+audit.handler.ts+ws-bridge.service.ts. Each journey: entry, intent, decision points, system responses, loading, failures, recovery, exit.(planned)/(proposed)marks per global rules. All requests:GET /api/v1/audit-logs, Bearer JWT, envelope per 00-shared/07.
1. Browse the audit log
entry: /settings/audit from Settings hub, or "Access audit" entry from RBAC
intent: see recent tenant activity
sequenceDiagram
actor U as Admin
participant L as AuditListPage
participant C as AuditCubit
participant R as AuditRepository
participant API as GET /audit-logs
U->>L: open audit log
L->>C: load(page=1, limit=50)
C->>R: query(filters={})
R->>API: ?page=1&limit=50
alt success
API-->>R: envelope.data = {data:[...], total:N} (audit.service.ts:41-50)
R-->>C: entries + total
C-->>L: render table (desktop) / cards (phone)
else 401
API-->>R: UNAUTHENTICATED
C-->>L: session-expiry flow (00-shared/06 §3.6)
else 429
API-->>R: RATE_LIMITED (api tier 100/min, rate-limit.constants.ts:6)
L-->>U: countdown chip, no auto-retry
else network
L-->>U: AppErrorState + Retry
end
L-->>U: scroll → Load more (next page) until total reached
- Loading:
AppSkeletonrows ≤ 200 ms → content; default page size 50 (audit.controller.ts:20). - Entry: row tap → detail (from in-memory entry — no detail endpoint, OQ-4).
- Exit: back to Settings; realtime WS appends visible on list (journey 5).
2. Filter by actor / action / resource
entry: filter bar on audit list
intent: narrow the trail to one actor, one action, or one resource type
sequenceDiagram
actor U as Admin
participant L as AuditListPage
participant C as AuditCubit
participant API as GET /audit-logs
U->>L: pick Action = "StudentUpdated"
L->>C: changeFilter(action: "StudentUpdated")
C->>C: reset page=1
C->>API: ?page=1&limit=50&action=StudentUpdated
API-->>C: data:[entries], total
Note over U,API: filters combine (AND): action + entityType + actorId (audit.controller.ts:26-29)
alt entityType filter chosen
Note over U,API: entityType matches only entries whose payload carried entityType — most emitters don't set it (OQ-1); show helper copy
end
U->>L: "Clear filters" → resets all three → reload unfiltered
- Filter controls: three
AppDropdowns (Action, Entity type, Actor) — all optional, AND-combined (audit.controller.ts:26-29). - Recovery: empty result →
AppEmptyState"No activity matches these filters" + Clear button. - State: filter selection resets pagination to page 1;
totalrefreshes.
3. Search (free text) — (planned)
entry: AppSearchBar on audit list
intent: find an entry by entity id, actor name, or event payload value
- Server today: no
qparam onGET /audit-logs(audit.controller.ts:19-25) — shared conventionsort/qunsupported (OQ-2 in00-shared;12_API_Mapping). (planned)client-side: filter current page by visible fields (action, entityType, actorId, id) as an interim; full server search requires a backendq(regex across payload) — flag to backend team.
4. Inspect an entry — before/after diff
sequenceDiagram
actor U as Admin
participant L as AuditListPage
participant D as AuditDetailView
U->>L: tap row (action=UserUpdated, actor=A.K., 10:32)
L-->>D: pass in-memory AuditEntry (no refetch — no :id endpoint, OQ-4)
D-->>U: header: action chip + actor + occurredAt (local tz) + correlationId
Note over D,U: body = AuditDiffView
alt emitter provided before/after
D-->>U: changed/added/removed rows (OQ-1: rare today)
else after only (common: after = payload, audit.handler.ts:43)
D-->>U: "Snapshot after action" — payload rendered as JSON tree (e.g. {userId, changes:[...]}, users.service.ts:70-75)
else neither
D-->>U: "No snapshot captured for this action" (empty diff)
end
U->>D: expand nested JSON value / copy JSON (SelectableText)
U->>U: back → list keeps scroll + filters
- Immutability cues: no edit/delete buttons anywhere on the detail view (append-only,
audit-log.repository.ts:6); footnote "This record cannot be modified." - Diff math is client-side: server stores full
before/aftermaps; the client computes changed/added/removed keys.
5. Realtime append (WS)
entry: audit list open while another user performs an action
intent: watch the trail update without manual refresh
sequenceDiagram
participant S as Source service (e.g. users)
participant B as EventBus
participant H as AuditHandler
participant DB as audit_logs
participant W as WsBridge
participant C as AuditCubit (connected)
S->>B: emit(UserUpdated, {tenantId, actorId, payload})
B-->>H: onAny(event) → write masked entry (audit.handler.ts:18-45)
B-->>W: onAny(event) → broadcast to tenant room (ws-bridge.service.ts:16-22)
W-->>C: WS topic "UserUpdated" {eventType, occurredAt, payload}
C-->>C: prepend entry (dedupe by _id/correlationId; respect active filters)
C-->>U: banner "1 new entry" → tap to expand
- WS auth: bearer token in handshake (
ws.gateway.ts:37-40); client joinstenant:{tenantId}automatically (ws.gateway.ts:50). - Filter-aware: realtime entries are appended only if they match the active filter set; otherwise the banner shows "New activity — refresh filters".
- Safety: broadcast payload is unmasked (OQ-8) — the client never renders broadcast payload fields verbatim; it uses the REST snapshot on expand.
6. Export audit trail — (planned)
entry: list overflow menu / detail menu → "Export"
intent: CSV/PDF of the filtered result set for compliance (AUDITING.md:87)
sequenceDiagram
actor U as Admin
participant L as AuditListPage
participant S as ExportSheet
U->>L: overflow → Export
L->>S: open ExportSheet (format, scope = current filters + pages, timezone)
S->>U: confirm → submit (planned endpoint; export itself is an audited action per AUDITING.md:87)
alt endpoint exists (future)
S-->>U: job queued (REPORT_GENERATE, queue.constants.ts:10) → status + download
else today
S-->>U: disabled state with note "(planned)" — no backend export endpoint
end
- Client scope today: none — the export surface is speced but gated until the backend lands (OQ/planned).
Abandonment & exit rules (all): back = restore previous scroll/filters; timeout = WS reconnect triggers silent refetch; permission denial (no audit.read) = route hidden + 403 screen (00-shared/05 §8); offline = last-good cache + AppOfflineBanner, WS buffered → refetch on reconnect.