15 - Flutter Implementation Guide (Health Module)
- 1. Folder layout
- 2. Repository (the critical mapping)
- 3. HealthCubit
- 4. Screen & widgets
- 5. Key implementation details
- 6. Tests
- 7. Analytics (proposed)
- 8. Roadmap items NOT built (flag in code)
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); extrasping(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 keysdown; do NOT throw. Only network errors/timeouts and non-health-shaped 5xx throwApiException(13§4). - 429 → surface
rateLimitedso the cubit pauses polling 60 s (rate-limit.constants.ts:4). checkedAtis 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+ 5StatusCards (grid, 2-col ≥ 700 dp) +LastCheckedLabel+ RetryAppButton.StatusCard(up/down/stale + detail; tap only whendown→IncidentSheetfrom cached snapshot - zero network,06S2).PendingJobsTile: labelemails n · in-app n · webhook-deliver n · dlq n; amber chip whendlq > 0(07§3). Parse strictly; unknown → neutral.- Monospace payload in
SelectableTextwith 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;/v2exists 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-97has none today), gate screen entry on it.
6. Tests
- Unit: model fromJson (200 envelope, extras
ping/pendingJobs,errorkeys), 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 (14S1).
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)- nohealth.*permission yet (permissions.constants.ts:1-97).