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

13 — State Management (Dashboard Module)

Cubit-based state per 00-shared/06 (Bloc/Cubit baseline, cache §3.3, auth/session §3.6). Sources: dashboard.service.ts (payload), redis-cache.service.ts (server cache), ws-bridge.service.ts + ws.gateway.ts (realtime), Dashboard.md (TTL/events).


1. State model

sealed class DashboardState {
  const DashboardState();
}

class DashboardInitial extends DashboardState {}

class DashboardLoading extends DashboardState {}        // first load, skeletons

class DashboardLoaded extends DashboardState {
  final Overview overview;          // dashboard.service.ts:50-70 shape
  final DateTime fetchedAt;         // freshness tracking
  final bool fromCache;             // client cache hit?
  final Duration cacheAge;          // 0 when fresh network
}

class DashboardError extends DashboardState {
  final AppError error;             // UNAUTHENTICATED / INTERNAL / RATE_LIMITED
  final Overview? lastGood;         // keep-last-good on refresh failure
}

class DashboardOffline extends DashboardState {
  final Overview lastGood;          // ≥10 min old → offline banner
}

Overview mirrors dashboard.service.ts:50-70 exactly — no client-side derivation (09_User_Behaviour.md B9).

2. Cubits

CubitOwnsSource
OverviewCubitpayload, freshness, refresh triggersE1 (12_API_Mapping.md)
PeriodCubit (planned)Today/7d/30d selection08_Form_Specifications.md §2
WidgetsCubit (planned)widget order/visibility for customize screenE4/E5
WsSubscriptionCubit (or service)WS connect state, event filter, debounce§5 below

Single OverviewCubit per dashboard route; created in the screen's BlocProvider, disposed on route pop (poll timer dies with it).

3. Fetch flow (OverviewCubit)

flowchart TD
    A[open /dashboard] --> B{cache hit? age<60s}
    B -->|yes| C[emit Loaded fromCache=true cacheAge=age]
    B -->|no| D[emit Loading only if no lastGood]
    D --> E[GET /dashboard/overview]
    E -->|200| F[write client cache TTL 60s]
    F --> G[emit Loaded fromCache=false]
    E -->|401| H[session refresh → replay; fail = session expiry]
    E -->|429| I[emit Error RATE_LIMITED, keep lastGood, backoff poll]
    E -->|5xx| J[emit Error INTERNAL keep lastGood]
    C --> K[poll timer 60s → B]
  • Single-flight: one in-flight request; a new trigger while in-flight sets a pending flag and re-runs once on completion (coalescing) (10_Interaction_Specification.md §2).
  • refresh({force}): force skips the cache check (pull-to-refresh, WS).
  • Client cache key mirrors server: sl:{tenant}:dashboard:overview TTL 60 s (00-shared/06 §3.3); volatile — never written on error responses.

4. Polling

  • Timer.periodic(60s) started on Loaded, cancelled in close().
  • Tick → refresh() non-forced (cache age check skips the request when fresh — 10_Interaction_Specification.md §3).
  • App-background: WidgetsBindingObserver pauses timer; resume runs an immediate staleness check.

5. WebSocket refresh path

sequenceDiagram
    participant S as Socket (WsGateway)
    participant C as OverviewCubit
    participant R as REST /dashboard/overview
    Note over S: auto-joined tenant:{id} (ws.gateway.ts:50)
    S->>C: event {eventType, occurredAt, payload} (ws-bridge.service.ts:17-21)
    C->>C: filter: AttendanceMarked|ResultPublished|PaymentRecorded|InvoiceGenerated (Dashboard.md:34)
    C->>C: debounce 2s (coalesce bursts)
    C->>R: refresh(force: true)
    R-->>C: fresh Overview
    C->>C: emit Loaded; delta ≥1% → animate + live-region
  • Unknown/other events ignored (bridge forwards every domain event — ws-bridge.service.ts:16-22).
  • WS drop → poll timer continues; after 2 missed polls → AppOfflineBanner.
  • Server-pushed KPI payloads (forward-looking).

6. Period selectors (planned)

  • PeriodCubit state {period: today|7d|30d}; change → refetch /dashboard/attendance|finance?period= only when server supports it (E2/E3 (planned)); until then the selector renders disabled (08_Form_Specifications.md §2).
  • Each period keeps its own cache entry sl:{tenant}:dashboard:{scope}:{period} (TTL 60 s (planned)).

7. Widgets state (planned)

  • WidgetsCubit: {widgets: List<WidgetConfig>, dirty: bool, savingIds: Set}; reorder/toggle → local optimistic + dirty; Save → per-widget PATCH (E5) with rollback; conflict (409) → reload from E4.
  • Layout is server-authoritative (07_Component_Library.md §7).

8. Cache & staleness summary

LayerKeyTTLWrites on
Server (target (planned))sl:{tenantId}:dashboard:overview60 s (Dashboard.md:41)rebuild worker + event invalidation (Dashboard.md:50,:34)
Client volatilesl:{tenant}:dashboard:overview60 ssuccessful 200 only
Client renderlast-good always kept; StaleBanner past 60 s, offline ≥10 min

9. Error mapping (from 00-shared/06 §5)

ErrorUI
401silent refresh → replay; fail → session expiry screen
403never reachable (module hidden) — dashboard.read gate
429backoff: skip next poll cycle; AppSnackbar once
404not expected on overview; treat as 5xx
5xxkeep lastGood; snackbar ≥ 4 s; no skeleton flash