high priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

On screen entry, a single async call to PostSessionReportRepository.getByActivityId(activityId) determines draft state — no redundant calls
Banner is rendered at the top of the report screen (above form fields, below AppBar) when `report.status == 'draft'`
Banner is NOT rendered when no report exists, when the report status is 'submitted', or while the draft status is loading
Banner displays: a draft icon, the text 'You have an unsaved draft for this session', and a 'Continue draft' button/link
'Continue draft' action populates the form fields with the draft's saved values — all previously entered field values are restored
Banner is dismissible via an accessible close (×) button; after dismissal the banner is hidden for the current screen session (does not need to persist across app restarts)
Dismissing the banner does NOT delete the draft — it only hides the banner UI; the draft data remains in the repository
Loading state: while draft status is being fetched, the banner area shows a low-profile skeleton (not a full-screen spinner) so the rest of the form is still visible
Error state: if the draft fetch fails, the banner is silently hidden (not shown) — the user can still fill in a fresh report; error is logged but not surfaced as a modal
WCAG 2.2 AA: banner background-to-text contrast ratio ≥ 4.5:1; close button has a minimum touch target of 44×44 dp; banner role is announced by screen readers as a status region (`Semantics(liveRegion: true)` or equivalent)
Widget test: banner renders with correct text when draft exists; banner is absent when no draft; dismiss button hides the banner; 'Continue draft' triggers field population callback
The banner uses only design token classes (colors, spacing, typography) — no hardcoded hex values or pixel sizes

Technical Requirements

frameworks
Flutter
BLoC or Riverpod (for draft state management)
flutter_test
apis
PostSessionReportRepository.getByActivityId(activityId)
data models
PostSessionReport (status, field_values, activity_id, updated_at)
performance requirements
Draft status fetch completes within the first frame or shows skeleton — no visible layout shift after data loads
Banner appears within 200 ms of screen entry on a standard mobile connection
security requirements
Draft field values restored via 'Continue draft' must be sanitised before populating form fields — apply the same XSS-equivalent escaping used elsewhere in the app for text display
ui components
DraftIndicatorBanner widget (icon, message, continue button, dismiss button)
DraftBannerSkeleton (loading placeholder)
DraftState (sealed class: loading, hasDraft(PostSessionReport), noDraft, error)

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Implement DraftIndicatorBanner as a standalone stateful widget (or a BLoC-consumer widget) that accepts `activityId` as a parameter and manages its own loading/dismiss state internally — this keeps the parent report screen clean. Use a `ValueNotifier` for the dismissed state (simpler than a BLoC for this UI-only state). For the 'Continue draft' action, expose an `onContinueDraft(PostSessionReport draft)` callback so the parent screen can apply the draft values to its form fields — this separates concerns cleanly. Use the app's existing design token constants for the banner background color (choose amber/yellow from the token system to signal 'attention, not error'), icon, and text style.

Ensure the Semantics widget wraps the entire banner with `container: true` and a `label` describing its purpose for screen readers — this satisfies the WCAG 4.1.3 Status Messages criterion relevant to Norwegian accessibility law (which the app must comply with given its public-sector context).

Testing Requirements

Write widget tests using `WidgetTester` that cover: (1) banner visible when draft exists, (2) banner absent when no draft, (3) banner absent after dismiss, (4) 'Continue draft' calls the onContinue callback with the draft report object, (5) skeleton shown during loading state, (6) banner absent on error state. Mock PostSessionReportRepository with mockito. Add a golden test for the banner in its draft-present state to catch visual regressions. Verify accessibility with `tester.getSemantics()` to assert the live region and button labels are correctly set.

Component
Post-Session Report Screen
ui high
Epic Risks (2)
medium impact high prob technical

End-to-end integration tests that span Flutter UI → Supabase → RLS → storage are inherently flaky in CI due to network timing, test database state, and Supabase cold-start latency. Flaky tests erode confidence and slow the release pipeline.

Mitigation & Contingency

Mitigation: Use a dedicated Supabase test project with seeded org and user fixtures. Wrap all E2E tests in retry logic with a fixed seed and tear-down hooks. Keep E2E tests in a separate test suite that runs on-demand rather than on every PR, with unit and widget tests as the primary CI gate.

Contingency: If E2E tests remain unreliable, replace the Supabase calls in integration tests with a verified fake (in-memory repository implementations) and promote the real Supabase tests to a nightly scheduled run rather than blocking PR merges.

high impact medium prob security

Health status, course interest, and assistive device fields contain personal health data. If any logger, analytics event, or crash reporter captures field values — through automated error serialisation or developer-added debug logs — the feature could violate GDPR and Blindeforbundet's data processor agreement.

Mitigation & Contingency

Mitigation: Audit all log statements in the report feature's code paths before the epic is marked done. Apply a PII-safe logging wrapper that strips field values from any serialised form state before it reaches the logger. Add a CI lint rule that flags direct logger calls within report-related files.

Contingency: If PII is found in logs post-launch, immediately disable the affected logging call and rotate any credentials that were exposed. Notify the data protection officer and document the incident per GDPR Article 33 requirements.