Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

15 — Flutter Implementation Guide (Audit Module)

How to build the Audit feature in the Flutter client on top of 00-shared/11. Forward-looking spec; no client repo exists yet. Read-only feature: no write repositories, no optimistic updates, no forms beyond filters.


1. Folder structure

features/audit/
├── domain/
│   ├── models/
│   │   ├── audit_entry.dart        # schema mirror (12_API_Mapping): id, tenantId,
│   │   │                           # actorId, actorType, action, entityType?, entityId?,
│   │   │                           # occurredAt(DateTime), before?, after?, correlationId?,
│   │   │                           # metadata?, ipAddress?, device?, browser?, userAgent?
│   │   └── audit_filter.dart       # action?, entityType?, actorId? (query params)
│   ├── diff/
│   │   └── field_diff.dart         # FieldDiff {key, status: changed|added|removed, before?, after?}
│   └── exceptions/audit_exceptions.dart
├── data/
│   ├── dto/audit_entry_dto.dart    # envelope.data.data[] + envelope.data.total mapper (OQ-2)
│   └── repositories/audit_repository.dart
└── presentation/
    ├── cubit/
    │   ├── audit_cubit.dart        # list + filters + pagination + WS append (13 §1)
    │   └── audit_detail_cubit.dart # in-memory detail + deep-link lookup
    ├── pages/
    │   ├── audit_list_page.dart
    │   ├── audit_detail_page.dart
    │   └── audit_export_sheet.dart   # (planned) gated
    └── widgets/
        ├── audit_table.dart
        ├── audit_entry_row.dart
        ├── audit_action_chip.dart
        ├── audit_filter_bar.dart
        ├── audit_diff_view.dart
        └── json_tree_view.dart

2. Dependencies

flutter_bloc, dio (AppDio), go_router, get_it, intl (dates), flutter_secure_storage (tokens — nothing extra module-specific). No new deps; the diff and JSON tree are hand-rolled (07 §C) to avoid a diff-library dependency (ponytail: compute union of keys — ~30 lines).

3. AuditRepository (read-only)

class AuditRepository {
  // throws ApiException(code, status) (00-shared/11 §5)
  Future<AuditPage> query(AuditFilter f, {required int page, int limit = 50});
  // GET /audit-logs?page=&limit=&action=&entityType=&actorId=  (audit.controller.ts:19-25)
  // AuditPage { List<AuditEntry> items; int total; }            // from envelope.data
  Stream<DomainEventMessage> watchEvents();                      // WsClient topic stream
}
  • query omits empty filter params (audit.controller.ts:26-29); never sends sort/q (unsupported).
  • watchEvents subscribes to the tenant room events (ws.gateway.ts:50, ws-bridge.service.ts:16-22); AuditCubit converts + dedupes.

4. Diff rendering (core algorithm)

List<FieldDiff> computeDiff(Map<String,dynamic>? before, Map<String,dynamic>? after) {
  final keys = {...?before?.keys, ...?after?.keys};
  return [for (final k in keys)
    FieldDiff(
      key: k,
      status: before == null ? FieldStatus.added
        : after == null ? FieldStatus.removed
        : !deepEquals(before[k], after[k]) ? FieldStatus.changed
        : FieldStatus.unchanged,   // filtered out before rendering
      before: before?[k], after: after?[k],
    )];
}
  • Render: status badge + icon (11 §4), before struck-through error tint → after secondary tint; nested values → JsonTreeView (lazy).
  • after alone (before == null): show all keys as after snapshot with header "Snapshot after action" (audit.handler.ts:42-43).
  • Truncate values > 120 chars with expand; SelectableText for copy.

5. Table layout on desktop (≥ 1200 px)

  • AuditTable: custom Table inside SingleChildScrollView(horizontal) (min-width 720), vertical ListView.builder (virtualized — 00-shared/11 §13).
  • Sticky header via SliverPersistentHeader (or Table with separate header widget).
  • Columns: Time · Action · Actor · Entity · Context (mono); tap row → detail; hover tint; arrow-key row selection (FocusTraversalGroup).
  • Responsive degrade: LayoutBuilder — width < 840 px or text scale > 1.5× → stacked AuditEntryRow cards instead of table (no fixed-height rows).

6. Timeline UI (proposed)

  • Vertical CustomPaint rail + dots; entries grouped by entity/correlationId from loaded set; needs backend entityId filter to be meaningful (OQ-1) — gated off today.

7. Navigation

  • GoRoute /settings/auditAuditListPage; GoRoute /settings/audit/:idAuditDetailPage (guard: permissionGuard('audit.read'), 00-shared/11 §6).
  • Deep link entry miss → lookup cap (proposed) (OQ-4) → empty state.
  • RBAC /settings/access-audit reuses AuditListPage with preset filter (route-level, design-docs/rbac/04).

8. Localization keys

audit.list.title, audit.list.count ("Showing {shown} of {total}"), audit.list.load_more, audit.list.empty, audit.list.filtered_empty, audit.filter.action|entity|actor|clear, audit.filter.entity_hint, audit.detail.actor|time|entity|trace, audit.diff.changed|added|removed|none, audit.detail.immutable, audit.export.title|planned_note, audit.realtime.new, audit.copy.copied. Raw action/entityType strings never translated.

9. Theme

  • AppTheme.light()/dark() unchanged; audit adds no tokens — diff colors use semantic aliases success/error/tertiary (11 §1); mono for ids/JSON.

10. Testing

  • Unit: computeDiff permutations; pagination math; filter→query param mapping; WS dedupe/filter-matching; DTO mapper for the {data,total} shape.
  • Widget: list loading/error/empty/filtered-empty; detail states; banner; table↔card switch; export sheet disabled state.
  • Golden: AuditTable, AuditEntryRow, AuditDiffView, JsonTreeView (light/dark × 3 sizes).
  • Integration: login → open audit → filter → detail → realtime append (mock WS).
  • E2E (P0): org_admin audit journey + immutability probe (PATCH/DELETE → 404).

11. Performance

  • Virtualized table/cards; const constructors; lazy JSON rendering (collapsed by default); RepaintBoundary around table; cache filter option lists; debounce actor text field 300 ms; no rebuild of full list on filter change (cross-fade only).
  • Profiled against 00-shared/10 §1 on mid-range device (deep pages, 10k+ rows).

12. Proposals flagged to the team

  1. Backend: add entityType/entityId/before to emitters + ?entityId= filter (OQ-1) → enables real resource filtering + timeline.
  2. Backend: return meta (shared pagination shape) from AuditService.query (OQ-2).
  3. Backend: add GET /audit-logs/:id, date-range from/to, q, export endpoint (OQ-4, AUDITING.md:86-87).
  4. Backend: enforce audit.read (Phase-5, docs/IMPLEMENTATION_PLAN.md:241); resolve 90-day TTL vs 7-year retention (OQ-6).
  5. Realtime: mask payloads in WsBridge broadcast or filter client-side (OQ-8).