Wire chapter scope into StatsAsyncNotifier provider
epic-activity-statistics-dashboard-state-and-realtime-task-003 — Integrate the Chapter Scope Resolver into the StatsAsyncNotifier provider so that coordinators see chapter-scoped aggregates while peer mentors see only their own stats. The notifier must read the resolved chapter scope from its provider family parameter or a watched provider, and pass it as a filter argument to the stats repository query. Validate that scope changes trigger a fresh fetch.
Acceptance Criteria
Technical Requirements
Execution Context
Tier 2 - 518 tasks
Can start after Tier 1 completes
Implementation Notes
ChapterScope should be a sealed class or enum with two variants: ChapterScope.chapter(String chapterId) and ChapterScope.self(String userId) — this avoids nullable/optional ambiguity. If ChapterScopeResolver is itself an AsyncNotifier, use ref.watch and handle the AsyncValue — do not call .requireValue which throws on loading/error states. Add a guard: if chapterScopeProvider is in AsyncLoading state, StatsAsyncNotifier should remain in AsyncLoading rather than fetching with an incomplete scope. Keep the scope wiring inside build() alongside the time window watch — both dependencies are declared in one place for clarity.
Testing Requirements
Unit tests using flutter_test and ProviderContainer. Mock both ChapterScopeResolver and StatsRepository. Test cases: (1) Coordinator role → chapterScopeResolverProvider resolves to ChapterScope.chapter(chapterId) → repository called with chapter filter → snapshot contains multiple peer mentors. (2) Peer mentor role → scope resolves to ChapterScope.self(userId) → repository called with self filter → snapshot contains only one peer mentor summary.
(3) Scope changes from chapter A to chapter B → build() re-runs → repository called with new chapter filter. (4) ChapterScopeResolver throws AuthorizationException → state becomes AsyncError
Supabase realtime channel subscriptions that are not properly disposed on screen close can accumulate in memory across navigation events, causing duplicate invalidation calls, ghost fetches, and eventual memory leaks on long sessions.
Mitigation & Contingency
Mitigation: Implement StatsCacheInvalidator as a Riverpod provider with an explicit ref.onDispose callback that cancels the realtime channel subscription. Write a widget test that navigates away and back multiple times and asserts that only one subscription is active at any given time.
Contingency: If subscription leaks are found in production, add a global subscription registry that enforces at-most-one subscription per channel key, and schedule a dispose sweep on app background events.
Debouncing rapid inserts may swallow the invalidation signal if the debounce window outlasts the Supabase realtime event delivery window, resulting in the dashboard showing stale totals after a bulk registration completes.
Mitigation & Contingency
Mitigation: Set the debounce window to 800ms (shorter than the typical Supabase realtime delivery latency of 1-2s for batched events) and ensure the leading-edge invalidation fires immediately while trailing duplicates are suppressed. Integration-test with a 20-record bulk insert.
Contingency: If debounce timing proves unreliable, replace debounce with a trailing-edge timer reset on each event and add a guaranteed invalidation 5 seconds after the last event regardless of subsequent events.