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 (Health Module)

Build order and concrete Flutter implementation notes for the Health module client, on top of 00-shared/11 (app architecture) and 00-shared/06 (state). Reminder: per PRD the native app is post-Phase 1 (PRODUCT_REQUIREMENTS_DOCUMENT.md:144); this guide is the forward-looking build plan.


1. Folder layout

lib/features/health/
  data/
    models/health_snapshot.dart      # 13 §3 (deps, reachable, rateLimited,
                                     #   pendingJobs, ping, checkedAt)
    repositories/health_repository.dart
  domain/
    entities/service_status.dart     # enum up | down | stale
  presentation/
    cubits/health_cubit.dart         # poll timer + fetch/retry (13 §2)
    screens/app_health_screen.dart
    widgets/ (status_card.dart, status_dot.dart, pending_jobs_tile.dart,
              last_checked_label.dart, overall_banner.dart, incident_sheet.dart)

2. Repository (the critical mapping)

  • fetch()GET /api/v1/health (health.controller.ts:27-37).
  • 200 → parse data (HealthCheckResult): info.<key>.status, error.<key> (health.controller.spec.ts:21-36); extras ping (redis-health.indicator.ts:16), pendingJobs (bullmq-health.indicator.ts:30).
  • 503 → parse the error envelope (http-exception.filter.ts:73-81) as a down snapshot - reachable: false, all keys down; do NOT throw. Only network errors/timeouts and non-health-shaped 5xx throw ApiException (13 §4).
  • 429 → surface rateLimited so the cubit pauses polling 60 s (rate-limit.constants.ts:4).
  • checkedAt is client-clock (server sends none, 12 §6).

3. HealthCubit

class HealthCubit extends Cubit<HealthState> {
  HealthCubit(this._repo) : super(HealthState.initial());
  Timer? _poll;
  static const pollInterval = Duration(seconds: 30);

  Future<void> start() async {
    await fetch();                       // immediate on screen entry
    _poll = Timer.periodic(pollInterval, (_) => fetch(silent: true));
  }
  void onLifecyclePaused() => _poll?.cancel();       // 10 I7
  void onLifecycleResumed() { fetch(); start(); }
  Future<void> retry() async { if (state.inFlight) return; await fetch(); }
  // fetch(silent: true) does not animate unchanged cards; 429 → pause 60 s
  @override
  void close() { _poll?.cancel(); super.close(); }
}
  • One in-flight request at a time (guard in fetch).
  • On 429: cancel poll, Timer(60 s, () => fetch()) (13 §2).

4. Screen & widgets

  • AppHealthScreen: BlocBuilder<HealthCubit, HealthState>OverallBanner + 5 StatusCards (grid, 2-col ≥ 700 dp) + LastCheckedLabel + Retry AppButton.
  • StatusCard (up/down/stale + detail; tap only when downIncidentSheet from cached snapshot - zero network, 06 S2).
  • PendingJobsTile: label emails n · in-app n · webhook-deliver n · dlq n; amber chip when dlq > 0 (07 §3). Parse strictly; unknown → neutral.
  • Monospace payload in SelectableText with copy for the incident sheet.

5. Key implementation details

  • Poll hygiene: 30 s + manual retry must stay under the shared 30 req/min public cap (08 §1); never auto-retry 503 in a loop (09 §5).
  • Status enum: up/down/stale; stale = snapshot > 60 s (07 §4). Color never alone - text always (00-shared/09).
  • Version note: client should call /api/v1/health; /v2 exists and is identical today (health.controller.ts:28) - pick one and keep it.
  • No auth header needed - endpoint is @Public() (health.controller.ts:15); the dio client must not attach the JWT interceptor (keeps probes clean).
  • Permission gating (proposed): when health.read-style permission lands (permissions.constants.ts:1-97 has none today), gate screen entry on it.

6. Tests

  • Unit: model fromJson (200 envelope, extras ping/pendingJobs, error keys), status enum parsing, stale computation.
  • Repository: 503 error-envelope → down snapshot (NOT exception); 429 → rateLimited; network error → ApiException.
  • Cubit: initial fetch; poll tick silent refresh; 429 pause/resume; in-flight guard; lifecycle pause/resume refetch (13 §2).
  • Widget: card states up/down/stale; banner text per state; detail sheet renders from cache; dlq amber chip.
  • Integration (00-shared/10): against running API - all-up 200; force-stop Redis in test env → 503; verify no secret/connection-string strings in any payload (14 S1).

7. Analytics (proposed)

Wire health.screen.{open,poll_success,poll_failure}, health.retry.tap, health.card.detail_open from 05; no SDK selected yet (00-shared/10 §8).

8. Roadmap items NOT built (flag in code)

  • Liveness/readiness split screens/polling - wait for /live + /ready (planned), END_TO_END_USER_FLOWS.md:738-740.
  • Disk/memory cards - not wired server-side (planned), health.controller.ts:31-36.
  • Uptime/version display - payload absent (planned), 12 §6.
  • Authenticated admin health variant (proposed) - no health.* permission yet (permissions.constants.ts:1-97).