15 — Flutter Implementation Guide (Audit Module)
- 1. Folder structure
- 2. Dependencies
- 3. AuditRepository (read-only)
- 4. Diff rendering (core algorithm)
- 5. Table layout on desktop (≥ 1200 px)
- 6. Timeline UI
(proposed) - 7. Navigation
- 8. Localization keys
- 9. Theme
- 10. Testing
- 11. Performance
- 12. Proposals flagged to the team
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
}
queryomits empty filter params (audit.controller.ts:26-29); never sendssort/q(unsupported).watchEventssubscribes 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-througherrortint → aftersecondarytint; nested values →JsonTreeView(lazy). afteralone (before == null): show all keys asaftersnapshot with header "Snapshot after action" (audit.handler.ts:42-43).- Truncate values > 120 chars with expand;
SelectableTextfor copy.
5. Table layout on desktop (≥ 1200 px)
AuditTable: customTableinsideSingleChildScrollView(horizontal)(min-width 720), verticalListView.builder(virtualized —00-shared/11 §13).- Sticky header via
SliverPersistentHeader(orTablewith 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× → stackedAuditEntryRowcards instead of table (no fixed-height rows).
6. Timeline UI (proposed)
- Vertical
CustomPaintrail + dots; entries grouped by entity/correlationId from loaded set; needs backendentityIdfilter to be meaningful (OQ-1) — gated off today.
7. Navigation
GoRoute /settings/audit→AuditListPage;GoRoute /settings/audit/:id→AuditDetailPage(guard:permissionGuard('audit.read'),00-shared/11 §6).- Deep link entry miss → lookup cap
(proposed)(OQ-4) → empty state. - RBAC
/settings/access-auditreusesAuditListPagewith 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 aliasessuccess/error/tertiary(11 §1);monofor ids/JSON.
10. Testing
- Unit:
computeDiffpermutations; 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;
constconstructors; lazy JSON rendering (collapsed by default);RepaintBoundaryaround 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 §1on mid-range device (deep pages, 10k+ rows).
12. Proposals flagged to the team
- Backend: add
entityType/entityId/beforeto emitters +?entityId=filter (OQ-1) → enables real resource filtering + timeline. - Backend: return
meta(shared pagination shape) fromAuditService.query(OQ-2). - Backend: add
GET /audit-logs/:id, date-rangefrom/to,q, export endpoint (OQ-4,AUDITING.md:86-87). - Backend: enforce
audit.read(Phase-5,docs/IMPLEMENTATION_PLAN.md:241); resolve 90-day TTL vs 7-year retention (OQ-6). - Realtime: mask payloads in
WsBridgebroadcast or filter client-side (OQ-8).