fix(polling): start timer when dashboardDomainReadyProvider already resolved - #962
Conversation
…esolved ref.listen() only fires on state changes — if dashboardDomainReadyProvider completed before the polling provider was first read, the listener never fires and the timer never starts. This caused Dashboard traffic cards and Statistics page to show "Waiting for data..." indefinitely. Add immediate state check in build() to start timer if conditions are already met. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Qodo reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
AustinChangLinksys
left a comment
There was a problem hiding this comment.
🤖 Automated Review — Round 1 · 34d7e43..954a39a (full)
Verdict: 💬 Self-review (comment only) — GitHub prohibits self-approval; this review is provided as a reference to catch blind spots.
| Where | Issue (one-liner) | |
|---|---|---|
| 🔴 | usp_system_monitor_notifier.dart:47–56 · usp_traffic_analysis_notifier.dart:47–56 |
Identical 10-line block copy-pasted into both notifiers; violates DRY |
usp_system_monitor_notifier.dart:54 · usp_traffic_analysis_notifier.dart:54 |
[both reviewers] Future.microtask lacks ref.mounted guard; dispose race can throw StateError |
|
usp_system_monitor_notifier.dart:41–45 · usp_traffic_analysis_notifier.dart:42–46 |
[both reviewers] Auth-guard asymmetry: new microtask path checks isAuthenticated, existing ref.listen path does not |
|
test/ |
No unit tests for the new "already-resolved" startup path | |
| 💡 | usp_system_monitor_notifier.dart:40 · usp_traffic_analysis_notifier.dart:41 |
defaultInterval is function-local; elevate to static const for test reuse |
| 💡 | usp_traffic_analysis_notifier.dart diff context |
Context line shows refreshInterval: refreshInterval but local file has defaultInterval; verify base-commit correctness |
| 💡 | Both files | Add comment noting that rare double-call (microtask + listen both fire) is safe because setRefreshInterval cancels-then-restarts |
Items marked [both reviewers] were independently flagged by two agents → higher confidence.
🔴 Critical — Duplicate Logic Details
UspSystemMonitorNotifier.build() lines 47–56 and UspTrafficAnalysisNotifier.build() lines 47–56 contain a byte-for-byte identical 10-line block (3-line comment + 7-line guard). Any future change to the guard condition (e.g. add logging, extend predicate) must be applied in both files in sync — a maintenance trap.
Fix: Extract a private helper in each notifier (can also combine the ref.mounted fix):
void _startTimerIfDomainAlreadyReady(Duration interval) {
final domainReady = ref.read(dashboardDomainReadyProvider);
final isAuthenticated = ref.read(appConnectionStateProvider) ==
AppConnectionState.authenticated;
if (domainReady is AsyncData && isAuthenticated) {
Future.microtask(() {
if (ref.mounted) setRefreshInterval(interval);
});
}
}Then in build(): _startTimerIfDomainAlreadyReady(defaultInterval);
⚠️ Warning Details
ref.mounted guard missing (both files, new line ~54)
Future.microtask callbacks are not lifecycle-managed by Riverpod. If the container is disposed before the microtask executes (test teardown, rapid navigation), setRefreshInterval will call state = … on a disposed notifier → StateError: Cannot use "state" after the notifier was disposed. Riverpod 2.x provides ref.mounted; a search of the codebase found 0 current usages of it.
Fix:
Future.microtask(() {
if (ref.mounted) setRefreshInterval(defaultInterval);
});Auth-guard asymmetry between ref.listen and new ref.read path
The new microtask fallback checks appConnectionStateProvider == authenticated before starting the timer. The pre-existing ref.listen(dashboardDomainReadyProvider, …) callback in both notifiers does not perform the same check — it calls setRefreshInterval(defaultInterval) unconditionally whenever the provider resolves.
Result: if the user logs out and then dashboardDomainReadyProvider fires (e.g. from a cached resolution), the timer will be started in the unauthenticated state via the ref.listen path. The downstream _fetchAndAppend() auth guard prevents actual USP requests, but the timer runs needlessly.
Fix: Unify both paths — either add the auth check to the ref.listen callback, or remove it from the microtask path with a comment explaining that the downstream guard is sufficient.
No unit tests for new startup path
No test files exist for UspSystemMonitorNotifier or UspTrafficAnalysisNotifier in test/. The project has well-established notifier test patterns (see usp_dmz_notifier_test.dart, usp_wifi_settings_notifier_test.dart) that handle Future.microtask via await Future.delayed(Duration.zero).
Minimum coverage needed:
- Provider already-resolved + authenticated → timer starts after microtask pump
- Provider starts
AsyncLoading→ transitions toAsyncDataviaref.listen→ timer starts - Provider already-resolved + unauthenticated → timer does NOT start
✅ What looks good
- Core fix is correct and necessary.
ref.listendoes not replay the current value on subscription — theref.readfallback is the right approach and matches the pattern used elsewhere in the codebase. Future.microtaskdeferral is appropriate. CallingsetRefreshIntervalsynchronously insidebuild()would mutate state during the build phase; the microtask correctly defers to after build completes.- All PrivacyGUI architecture rules satisfied: layer dependency direction ✅, L1 non-autoDispose ✅,
ref.read(notwatch) for cross-provider reads ✅, mutation lock not applicable, no codegen model misuse. - No security concerns: no credentials, no injection surface, no auth bypass. USP/JNAP call paths are entirely unaffected by this PR.
- Symmetry across the two notifiers is maintained (both get the same fix, same pattern).
Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.
HankYuLinksys
left a comment
There was a problem hiding this comment.
Two follow-ups before merge. The root-cause analysis is correct and the fix is sound, but (1) the new code path has no test coverage, and (2) the two start paths now use inconsistent guard conditions. Details inline.
HankYuLinksys
left a comment
There was a problem hiding this comment.
Fix looks correct. Two follow-ups inline (both apply to the traffic notifier too).
| final domainReady = ref.read(dashboardDomainReadyProvider); | ||
| final isAuthenticated = ref.read(appConnectionStateProvider) == | ||
| AppConnectionState.authenticated; | ||
| if (domainReady is AsyncData && isAuthenticated) { |
There was a problem hiding this comment.
This new path is never hit by the tests — neither test overrides dashboardDomainReadyProvider, so it stays AsyncLoading at first read and this if never runs. Please add a test that resolves it before first read and asserts the timer started. (Same for the traffic notifier.)
| // already completed before this provider was first read, the listener above | ||
| // will never fire. Check current state and start timer if ready. | ||
| final domainReady = ref.read(dashboardDomainReadyProvider); | ||
| final isAuthenticated = ref.read(appConnectionStateProvider) == |
There was a problem hiding this comment.
Guard inconsistency: this path checks isAuthenticated, but the ref.listen(dashboardDomainReadyProvider) path above doesn't. Not a bug, but worth aligning. (Same for the traffic notifier.)
…y-resolved path - Extract _startTimerIfAuthenticated() helper to unify auth check between ref.listen and ref.read fallback paths (addresses Hank's review comment) - Add tests for when dashboardDomainReadyProvider already resolved before first provider read — covers the new microtask startup path - Add tests to verify timer does NOT start when domain ready but logged out Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
|
Both points from my earlier review are addressed: |
Resolved conflicts in polling providers by taking dev-2.5.0's _startTimerIfAuthenticated() fix for the already-resolved path. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
Summary
dashboardDomainReadyProviderhas already completedref.listen()only fires on state changes — if the provider was first read after domain ready resolved, the listener never firesbuild()to start timer if conditions are already metTest plan
🤖 Generated with Claude Code