medium priority low complexity frontend pending frontend specialist Tier 2

Acceptance Criteria

A banner appears at the top of the admin screen when DateTime.now() minus FeatureFlagCache.lastSyncAt exceeds the configured TTL
The banner displays 'Data may be outdated — last synced X minutes ago' with X updated every 60 seconds
The banner includes a 'Refresh now' button
Tapping 'Refresh now' triggers FeatureFlagRepository.forceRefresh() and shows a loading spinner on the button for the duration of the call
On successful refresh, the banner is dismissed and the flag list updates to reflect the new data
On failed refresh, the banner remains visible and a snackbar displays the error message; the button returns to its default state
The banner does not appear when the cache is fresh (lastSyncAt within TTL)
The banner is dismissed automatically if a background refresh succeeds and updates the cache timestamp
The banner has sufficient color contrast (yellow/amber warning color with dark text) meeting WCAG 2.2 AA
The banner is announced to screen readers as a status region when it appears

Technical Requirements

frameworks
Flutter
Riverpod
apis
FeatureFlagCache.lastSyncAt: DateTime
FeatureFlagCache.ttlSeconds: int
FeatureFlagRepository.forceRefresh(): Future<void>
data models
FeatureFlagCache (lastSyncAt, ttlSeconds, cachedFlags)
performance requirements
Staleness check must be computed reactively from a Riverpod provider; no polling timers in widget build
Timer to update 'X minutes ago' text must be a single periodic Timer.periodic at 60s interval, cancelled on widget dispose
security requirements
Force refresh must be gated behind admin role; anonymous refresh calls must be rejected by repository
ui components
StaleCacheBanner (MaterialBanner or custom warning bar)
RefreshButton with loading state
Semantic status region wrapper for screen reader announcement

Execution Context

Execution Tier
Tier 2

Tier 2 - 518 tasks

Can start after Tier 1 completes

Implementation Notes

Derive isStale from a computed Riverpod provider that watches FeatureFlagCacheProvider and returns a bool based on timestamp delta. This ensures the banner reactively appears/disappears without widget-level polling. For the 'X minutes ago' display, use a StatefulWidget with a single Timer.periodic(60s) that calls setState to recompute the elapsed label—do not create multiple timers. The banner should use MaterialBanner for native accessibility support (it handles live region semantics on Android).

For iOS VoiceOver, add a Semantics wrapper with liveRegion: true. Confirm the amber warning color token exists in the design system before hardcoding; if absent, add a --color-warning-bg and --color-warning-text token to styles.css.

Testing Requirements

Widget tests: (1) banner is absent when cache is within TTL; (2) banner appears when cache exceeds TTL; (3) 'X minutes ago' text reflects correct elapsed time; (4) tapping refresh calls forceRefresh exactly once; (5) on success, banner is removed and flag list provider is invalidated; (6) on failure, banner remains and error snackbar is shown; (7) button is disabled during in-flight refresh to prevent double calls. Use fake async (fakeAsync from flutter_test) to simulate TTL expiry without real time delays.

Component
Feature Flag Admin Screen
ui medium
Epic Risks (3)
high impact low prob security

The feature flag admin screen allows persisting changes to organization_configs. If the role guard is implemented only client-side (checking role state in Riverpod), a user who manipulates their local role state could toggle flags for their organization without proper server-side authorization, potentially exposing features prematurely.

Mitigation & Contingency

Mitigation: Implement server-side authorization for flag update operations using Supabase RLS UPDATE policies that check the user's role in the memberships table. The client-side guard is UX only; the database enforces the actual restriction.

Contingency: If an unauthorized update is detected, audit the RLS policies and add a Supabase Edge Function as an authorization middleware for flag toggle operations, rejecting requests from non-admin role JWTs.

medium impact medium prob scope

Developers on other feature teams may use FeatureGate incorrectly — for example, wrapping business logic rather than UI, or using it before flag initialization completes — leading to features that are visible but non-functional or cause runtime errors when flags are queried in a loading state.

Mitigation & Contingency

Mitigation: Add assert statements in FeatureGate's build method that throw in debug mode if the provider is still in a loading state. Write developer documentation with a clear usage contract: FeatureGate is UI-only; logic gating must use the provider's isEnabled method directly. Include lint examples in the codebase.

Contingency: If misuse is found in code reviews, add a custom Dart lint rule via custom_lint that flags FeatureGate usage outside of the widget tree, and conduct a codebase audit to find existing violations.

medium impact low prob technical

If the audit log is stored in the same organization_configs table without pagination or archival strategy, high-frequency flag changes during pilot testing could produce an unbounded number of rows, degrading query performance on the admin screen.

Mitigation & Contingency

Mitigation: Store audit log entries in a separate feature_flag_audit_log table with an index on (organization_id, changed_at DESC). Implement cursor-based pagination in the repository and limit the initial load to 50 entries.

Contingency: If table size becomes a performance concern, add a Supabase scheduled function to archive entries older than 90 days to cold storage, and add a database index on changed_at for range queries.