critical priority medium complexity integration pending integration specialist Tier 5

Acceptance Criteria

fetchSummary(mentorId, year, {bool forceRefresh = false}) first checks SummaryOfflineCache; if a cached entry exists and forceRefresh is false, it returns it immediately via a Stream or the result object
After returning the cached value, the method asynchronously fetches from Supabase and, on success, updates the cache and emits the fresh value to the caller
When Supabase fetch succeeds, the cache is updated with the fresh AnnualSummary
When the network call fails and a cached entry exists, the method returns the stale entry wrapped in a SummaryResult.stale(data) value object with isStale = true
When the network call fails and no cached entry exists, the method returns SummaryResult.error(exception) — it does not throw
forceRefresh = true bypasses the initial cache check and always waits for the Supabase response before returning, then updates the cache
The staleness flag is surfaced to the UI layer via the result type — it is the UI's responsibility to show a 'Last updated: X' indicator
SummaryOfflineCache.put() is called exactly once per successful network fetch, not on cache hits
The strategy is implemented inside the repository and does not leak into the BLoC or UI layer

Technical Requirements

frameworks
Flutter
Riverpod
supabase_flutter
Hive
apis
Supabase PostgreSQL REST API — annual_summaries table
data models
annual_summary
performance requirements
Cached response must be returned within 5 ms of the call (pure synchronous cache read)
Network fetch must not block the cached result delivery — use Future chaining or Stream
forceRefresh=true path must show a loading indicator in the UI within 100 ms
security requirements
Stale data must never be shown without a visible staleness indicator in the UI — repository must enforce the flag
forceRefresh must not be triggerable by unauthenticated users — guard at BLoC/use-case layer

Execution Context

Execution Tier
Tier 5

Tier 5 - 253 tasks

Can start after Tier 4 completes

Implementation Notes

Model the return type as a sealed class: SummaryResult with subtypes SummaryFresh(AnnualSummary), SummarySale(AnnualSummary), and SummaryError(Exception). Return a Stream from fetchSummary so the UI can react to both the cached value and the subsequent network update in one subscription. Alternatively, use a two-emit pattern with a StreamController. The background fetch should be fire-and-forget with its own try/catch — its failure should only set isStale, not propagate as an unhandled exception.

Keep the cache-then-network logic in a private _fetchWithCache() method so fetchSummary remains clean. Document the Stream contract clearly: always 1 or 2 emissions, never more.

Testing Requirements

Unit tests: (1) cache hit returns immediately without calling Supabase client, (2) cache hit still triggers background network fetch and updates cache, (3) network failure with cache hit returns stale result with isStale=true, (4) network failure with no cache returns error result, (5) forceRefresh=true calls Supabase regardless of cache state, (6) successful network response updates cache via put(), (7) SummaryResult sealed class has correct subtypes for fresh/stale/error. Test the async background update with a completer-based mock to verify cache is updated after the background call completes.

Component
Annual Summary Repository
data medium
Epic Risks (3)
medium impact medium prob dependency

Rive animation files may not be available at implementation time, blocking the wrapped-animation-controller from being fully tested. If asset delivery is delayed, the controller cannot be validated for memory-leak-free disposal.

Mitigation & Contingency

Mitigation: Implement the animation controller with stub/placeholder AnimationController instances first so the lifecycle and disposal logic can be unit-tested independently of Rive assets. Define a named animation registry interface early so UI components can reference animations by name without coupling to specific Rive files.

Contingency: If Rive assets are not delivered before Epic 3 begins, replace Rive animations with Flutter implicit animations (AnimatedOpacity, ScaleTransition) as a drop-in and schedule Rive integration as a follow-on task once assets arrive.

high impact medium prob technical

The annual_summaries Supabase RPC aggregating 12 months of activity records per mentor may exceed acceptable query latency (>2s) for mentors with high activity volumes such as the HLF mentor with 380 registrations cited in workshop notes.

Mitigation & Contingency

Mitigation: Design the RPC to materialise summary results into the annual_summaries table via a scheduled edge function rather than computing on demand. The repository reads pre-computed rows, keeping query latency constant regardless of activity volume.

Contingency: If on-demand queries are required for real-time period switching, add a PostgreSQL partial index on (mentor_id, activity_date) and implement a client-side loading skeleton so slow queries degrade gracefully rather than blocking the UI.

medium impact low prob technical

iOS requires photo library permission before saving a screenshot to the gallery. If the permission prompt is triggered at an unexpected point in the share flow, the UX breaks and users may deny permission permanently, making gallery save unavailable.

Mitigation & Contingency

Mitigation: Trigger the permission request only when the user explicitly chooses 'Save to gallery' in the share overlay, not on screen load. Implement a pre-prompt explanation screen following Apple HIG so users understand why the permission is needed before the system dialog appears.

Contingency: If permission is denied, gracefully fall back to clipboard copy and system share sheet options which do not require photo library access, and surface a non-blocking snackbar explaining the limitation.