high priority low complexity frontend pending frontend specialist Tier 1

Acceptance Criteria

ExpiryStatusIndicator widget accepts a required expiryStatus parameter of type CertificateExpiryStatus enum and an optional displayMode parameter (defaults to compact)
Compact mode renders a pill/badge with a short localized label (e.g., 'Active', 'Expiring Soon', 'Expired') using foreground color from CertificateExpiryTokens
Badge background and border use the correct token colors for the given state
Widget is wrapped in a Semantics node with a label that reads the full state description (e.g., 'Certificate status: expiring soon')
Widget renders correctly at system font scale factors 1.0, 1.5, and 2.0 without text overflow or layout clipping
Widget renders correctly in both light and dark ThemeData contexts
Widget is a stateless widget — no internal state management
Widget is exported from the component barrel file and usable with a single import
No hardcoded color literals in the widget file — all colors sourced from CertificateExpiryTokens

Technical Requirements

frameworks
Flutter
Dart
data models
CertificateExpiryStatus (enum)
CertificateExpiryTokens (design tokens from task-001)
performance requirements
Widget must be const-constructible when all parameters are known at compile time
No heavy computation in build() — token lookup is O(1)
ui components
ExpiryStatusIndicator (new StatelessWidget)
Semantics (Flutter built-in)
Container / DecoratedBox for badge shape

Execution Context

Execution Tier
Tier 1

Tier 1 - 540 tasks

Can start after Tier 0 completes

Implementation Notes

Define CertificateExpiryStatus as a Dart enum with values: active, expiringSoon, expired. Place it in lib/domain/enums/certificate_expiry_status.dart so it is accessible from both domain and UI layers without coupling. The widget itself belongs in lib/features/certificate_expiry/widgets/expiry_status_indicator.dart. Use a switch expression on the enum to select the token group — avoid if/else chains.

For the badge shape, use a Container with BoxDecoration(borderRadius: BorderRadius.circular(token), color: tokens.background, border: Border.all(color: tokens.border)). The Semantics label should be driven by a localized string map (or an extension method on the enum) rather than hardcoded English strings, to support future l10n. Compact mode should constrain height to match a standard list tile's leading widget zone (typically 20–24px height) so it embeds cleanly in ListTile without affecting row height.

Testing Requirements

Widget tests in test/widgets/expiry_status_indicator_test.dart. Use flutter_test pumpWidget with MaterialApp wrapper. Test cases: (1) each of the 3 states renders the correct label text, (2) each state applies the correct background color from tokens (use find.byType(Container) + decoration inspection or find.byWidgetPredicate), (3) Semantics node is present with the correct label for each state, (4) widget renders without overflow at textScaleFactor 2.0, (5) widget renders in dark mode ThemeData without assertion errors. Do not test visual pixel output — test widget tree structure and semantics.

Epic Risks (2)
medium impact medium prob technical

The persistent banner must remain visible across app sessions and only disappear when a specific backend condition is met (renewal or coordinator acknowledgement). If the BLoC state is not properly sourced from the notification record repository on every app launch, the banner may disappear prematurely or fail to reappear after a session restart.

Mitigation & Contingency

Mitigation: Drive the banner's visibility exclusively from a Supabase real-time subscription on the notification records table filtered by mentor_id and acknowledged_at IS NULL. Never persist banner visibility state locally. Write an integration test that restarts the BLoC and verifies the banner reappears from the database source.

Contingency: If real-time subscriptions introduce latency or connection reliability issues in offline-first scenarios, add a local cache flag that is only cleared when the repository confirms the acknowledgement write succeeded, with a cache TTL of 24 hours as a fallback.

high impact low prob security

The notification detail view must conditionally render coordinator-specific actions based on the authenticated user's role. Incorrect role resolution could expose the 'Acknowledge Lapse' action to peer mentors or hide it from coordinators, breaking the workflow and potentially allowing unauthorised state changes.

Mitigation & Contingency

Mitigation: Source the role check from the existing role_state_manager BLoC that is already authenticated against Supabase role claims. Do not rely on a local flag. The coordinator acknowledgement service backend also validates role server-side, providing defence in depth. Add widget tests that render the detail view with mentor and coordinator role fixtures and assert the presence or absence of coordinator actions.

Contingency: If a role resolution bug is found in production, immediately disable the acknowledge action via a feature flag and patch the role check in a hotfix release. The server-side validation in the coordinator acknowledgement service ensures no actual state change can occur even if the button is incorrectly rendered.