Define StatsSnapshot data model and AsyncValue state shape
epic-activity-statistics-dashboard-state-and-realtime-task-001 — Define the StatsSnapshot immutable data class that the Stats Async Notifier will expose via AsyncValue<StatsSnapshot>. This includes total activity counts, per-type breakdowns, peer mentor lists with individual totals, and the selected time window metadata. Ensure the model is serializable and supports equality for Riverpod state comparison.
Acceptance Criteria
Technical Requirements
Implementation Notes
Use freezed with json_serializable for immutability and serialization — this is consistent with the project's existing model pattern. Place StatsSnapshot in `lib/features/stats/domain/models/stats_snapshot.dart` and PeerMentorStatsSummary in the same file or a sibling file. The Map
Riverpod's AsyncValue
Testing Requirements
Unit tests using flutter_test. Test cases: (1) Two StatsSnapshot instances with identical data are equal (==) and have identical hashCodes. (2) StatsSnapshot.empty() returns an instance where totalActivityCount == 0 and peerMentorSummaries is an empty list. (3) A StatsSnapshot can be serialized to JSON and deserialized back to an equal instance.
(4) activityCountByType correctly handles an empty map without throwing. Run with `flutter test` — no mocking required for this pure data model.
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.