feat: remote assistance for local login users#956
Conversation
…checkDeviceInfo timeout - Uncomment and enable support_agent icon in TopBar for local login - Add retry (1) and increased timeout (10s) for checkDeviceInfo in remote mode - Add explicit text color for session expire counter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
- Add checkActiveSession in polling for local login users - Add session status listener in DashboardShell with loginType guard - Support passive mode in remote assistance dialog - Fix countdown timer using .abs() instead of * -1 - Prevent dialog from showing in remote login mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
# Conflicts: # test/mocks/pnp_notifier_mocks.dart
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? |
|
Reviewed the Remote Assistance changes on this branch. The functional scope is small and focused: enabling the support-agent icon in the TopBar for local-login users, detecting an active remote session via the existing polling loop and auto-showing the dialog in passive mode, and a few countdown/timeout fixes. (Most of the 233-file diff is 🟠 No tests for the new RA detection logic 🟢 Magic number 🟢 |
🤖 Automated Review — Round 1This PR's diff (3,517 lines across 233 files) exceeds the automated review limits (max 4,000 lines / max 60 files). Auto AI review is skipped for this round. Recommendation: Please request a human code review. First 15 changed files (click to expand)
Automated — this comment was posted by the PrivacyGUI PR review bot. |
…checkActiveSession - Extract 2700 to kPendingSessionDurationSec constant (45-min PENDING window) - Add unit tests for checkActiveSession (empty/active/error cases) - Add unit tests for countdown .abs() behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
|
Thanks for the review @HankYuLinksys! Addressed both actionable points in c5ab78a: 🟠 Tests for RA detection logic
🟢 Magic number 2700 On the last point — agreed the sign-convention reliance on |
AustinChangLinksys
left a comment
There was a problem hiding this comment.
🤖 Automated Review — Round 2 · 9925300..c5ab78a (incremental)
Verdict: 💬 Comment — Contains Critical findings; final decision deferred to Austin.
| Where | Issue (one-liner) | |
|---|---|---|
| 🔴 | remote_client_provider_test.dart (countdown tests) |
[both reviewers] Tests hardcode 2700 instead of referencing kPendingSessionDurationSec — defeats the entire purpose of the constant extraction |
| 🔴 | remote_client_provider_test.dart (checkActiveSession tests) |
[both reviewers] Tests reference checkActiveSession method which does not exist in production provider; tests will not compile/pass against current codebase |
remote_assistance_dialog.dart:94 |
.abs() vs * -1 formula change: numerically equivalent for expected inputs but diverges if expiredIn > 0 (server clock skew / sign-convention bug); semantics undocumented |
|
remote_client_provider_test.dart (countdown test) |
Test only exercises expiredIn.abs() in isolation — never validates the full composed formula (kPendingSessionDurationSec + expiredIn).abs() |
|
remote_client_provider.dart:43 |
initiateRemoteAssistance() has no try/catch; test covers exception→null on checkActiveSession but production path on network error leaves dialog in permanent loading state |
|
remote_assistance_dialog.dart:11 |
kPendingSessionDurationSec is a domain/business-rule constant declared in the View layer — belongs in Service/core layer per 3-layer arch rule |
|
remote_client_provider_test.dart |
Bespoke _FakeDeviceManagerNotifier duplicates existing MockDeviceManagerNotifier in test/mocks — will silently diverge as DeviceManagerNotifier evolves |
|
remote_assistance_dialog.dart:94,121 |
_buildPendingWidget now uses .abs(); sibling _buildCountingWidget still uses * -1 — inconsistent idiom for identical operation |
|
| 💡 | remote_assistance_dialog.dart:11 |
Doc comment on constant omits sign-convention context for expiredIn arithmetic (why the sum then .abs() pattern is needed) |
Items marked [both reviewers] were independently flagged by two agents → higher confidence.
🔴 Critical — Details
C-1 · Test hardcodes 2700 instead of kPendingSessionDurationSec [both reviewers]
File: test/core/cloud/providers/remote_assistance/remote_client_provider_test.dart (countdown timer test, ~line 141)
The new test file re-introduces the same magic number 2700 that this PR was specifically changing to a named constant. If kPendingSessionDurationSec changes (e.g., business rule changes to 3600 s), the constant is updated, the production code is correct, but the test still asserts against 2700 and passes — giving false confidence that the value is tested.
Fix: Import kPendingSessionDurationSec from the dialog file (and from the Service layer once C-1 of architecture review is addressed) and use it in the test assertion.
C-2 · checkActiveSession method does not exist in production [both reviewers]
File: test/core/cloud/providers/remote_assistance/remote_client_provider_test.dart
Three tests call notifier.checkActiveSession() but the production RemoteClientNotifier has no such method. The closest methods are fetchSessions() (line 35) and initiateRemoteAssistance() (line 43). These tests will produce a compile error. Either the method was planned but not added, or the test is testing a future interface.
Fix: Either add checkActiveSession() to the provider with appropriate implementation, or rewrite the tests to cover the actual existing methods.
⚠️ Warning — Details
W-1 · .abs() vs * -1 formula semantics
File: lib/page/components/styled/remote_assistance/remote_assistance_dialog.dart:94
Original: (2700 + (expiredIn ?? 0)) * -1
New: (kPendingSessionDurationSec + (expiredIn ?? 0)).abs()
For the expected domain (expiredIn ≤ 0 for pending sessions), both yield the same positive result. But .abs() and * -1 diverge when expiredIn > 0: * -1 yields a negative value (widget shows expired/zero); .abs() yields a large positive value (widget counts from a huge number). Since TimerCountdownWidget uses .take(initialSeconds + 1), a large spurious positive would cause a very long countdown. No code comment explains why .abs() is the correct choice here.
Fix: Add a comment: // expiredIn is ≤ 0 for pending sessions; adding to window then abs() yields remaining seconds. Consider max(0, kPendingSessionDurationSec + (expiredIn ?? 0)) as a safer guard.
W-2 · Test covers only sub-expression, not full formula
File: test/core/cloud/providers/remote_assistance/remote_client_provider_test.dart (~line 113)
The test at lines 113–123 exercises sessionInfo.expiredIn.abs() in isolation, not (kPendingSessionDurationSec + sessionInfo.expiredIn).abs(). With testSessionInfo.expiredIn = -748, the expected composed result is (2700 + (-748)).abs() = 1952. No test asserts this.
Fix: Add a test case: expect((kPendingSessionDurationSec + (-748)).abs(), 1952) or exercise _buildPendingWidget end-to-end.
W-3 · initiateRemoteAssistance has no error handling
File: lib/core/cloud/providers/remote_assistance/remote_client_provider.dart:43
The production initiateRemoteAssistance() method is a bare await chain with no try/catch. The test correctly verifies that exceptions return null from checkActiveSession, but if initiateRemoteAssistance() throws (network error, 401), the exception bubbles to the dialog's .then() handler at remote_assistance_dialog.dart:28, which has no .catchError(). The dialog will remain in a loading state indefinitely.
Fix: Wrap initiateRemoteAssistance() body in try/catch and emit an error state (or at minimum verify the dialog's error handling covers this path).
W-4 · Domain constant in View layer
File: lib/page/components/styled/remote_assistance/remote_assistance_dialog.dart:11
kPendingSessionDurationSec represents a cloud/business rule (45-minute PENDING window). By PrivacyGUI's 3-layer rule, View-layer files must not define domain constants that other layers need. If a Provider or Service class ever needs this timeout value, it cannot import from the View layer. Fix: Move to lib/core/cloud/constants.dart or alongside GRASessionInfo in guardians_remote_assistance.dart.
W-5 · _FakeDeviceManagerNotifier duplicates existing mock
File: test/core/cloud/providers/remote_assistance/remote_client_provider_test.dart (bottom)
The project has MockDeviceManagerNotifier in test/mocks/device_manager_notifier_mocks.dart. Using a bespoke _FakeDeviceManagerNotifier creates a second implementation that will diverge silently when DeviceManagerNotifier adds new abstract methods. Fix: Use overrideWith(() => MockDeviceManagerNotifier()) consistent with existing test patterns.
W-6 · Inconsistent sign-flip idiom in sibling functions
File: lib/page/components/styled/remote_assistance/remote_assistance_dialog.dart:94,121
_buildPendingWidget now uses .abs() while adjacent _buildCountingWidget still uses * -1 for the same sign-flip operation. This creates a fragile inconsistency for anyone reasoning about the sign contract of expiredIn. Fix: Standardise on .abs() by updating _buildCountingWidget in the same PR.
✅ What looks good
- Extracting
2700intokPendingSessionDurationSecis the right intention — magic number was a real readability issue. The constant name is clear and includes the unit. - The doc comment
/// Total duration (in seconds) of the PENDING session window (45 minutes).is useful. - New test file has good structural coverage intent: empty sessions list, active session found, and exception path — exactly the three meaningful
checkActiveSessionbranches. - Test uses
@GenerateNiceMocks+ Mockito consistently with the rest of the test suite. setUp/tearDownwithProviderContainer.dispose()is properly lifecycle-managed.
Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.
Keep polling session info while the active-mode remote assistance dialog is open, auto-request a PIN once a session exists, and avoid opening a second dialog when the session becomes active. - Add nextPollInterval helper (5s before active, 60s once active). - Add pollSessionOnce: clears session info and pin when no session exists, requests a PIN when the session is INITIATE or PENDING and no PIN exists. - Rewrite initiateRemoteAssistance to poll once immediately then keep polling via a cancelable timer until the dialog closes; no longer returns early on empty sessions. Guard against re-entry and keep the loop alive across transient poll errors. - Add an isDialogShown flag to RemoteClientState so the dashboard does not auto-open a passive dialog while a dialog is already shown. - Document the hasInitialized guard and the dashboard's local re-entry guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Apply `dart format` formatting to instant setup (PnP) files and the generated pnp mocks. No logic changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
- Move kPendingSessionDurationSec from the view layer to the core model (guardians_remote_assistance.dart) per the 3-layer architecture rule. - Reference kPendingSessionDurationSec in the countdown tests instead of hardcoding 2700, and assert the full pending-window formula. - Replace the bespoke _FakeDeviceManagerNotifier with the existing generated MockDeviceManagerNotifier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
AustinChangLinksys
left a comment
There was a problem hiding this comment.
🤖 Automated Review — Round 3 · c5ab78a..7b8229d (incremental)
Verdict: 💬 Comment — Critical architecture violations and async correctness issues require resolution before merge.
| Where | Issue (one-liner) | |
|---|---|---|
| 🔴 | remote_client_state.dart |
[both reviewers] isDialogShown is View-layer UI state stored in service-layer state object, pollutes serialization |
| 🔴 | remote_client_provider.dart |
Active poll timer (_activePollTimer) never cancelled in dispose() — resource leak / runaway API traffic |
remote_client_provider.dart · pollSessionOnce |
[both reviewers] fetchSessionInfo returning null silently preserves stale sessionInfo/pin in state |
|
remote_assistance_dialog.dart |
[both reviewers] setDialogShown(false) stuck if showDialog throws — cleanup only in .then(), no .catchError |
|
remote_client_provider.dart · _scheduleNextPoll |
In-flight pollSessionOnce (incl. createPin) continues executing after endRemoteAssistance stops polling |
|
remote_client_provider.dart |
Two parallel session-tracking mechanisms (timer vs. stream) with divergent null-handling semantics | |
remote_client_provider.dart · initiateRemoteAssistance |
Polling starts unconditionally even when initial poll fails — no back-off, no retry limit | |
dashboard_shell.dart |
Dual-boolean guard (_isRemoteSessionDialogShown + isDialogAlreadyShown) is redundant and can diverge |
|
| 💡 | remote_client_provider.dart · pollSessionOnce |
sessions.first without sort/filter guarantee — may latch onto stale session |
| 💡 | remote_client_state.dart |
isDialogShown (ephemeral UI flag) included in toMap/fromJson — can restore as true after hot-restart |
Items marked [both reviewers] were independently flagged by two agents → higher confidence.
🔴 Critical Details
remote_client_state.dart — isDialogShown is View-layer state in service-layer object
RemoteClientState is a service-layer object tracking remote-session lifecycle data. Whether a dialog is currently visible on screen is a View-layer ephemeral UI concern. Storing it in RemoteClientState crosses the three-layer boundary upward, violates single-responsibility, and creates a serialization artifact: isDialogShown is added to fromMap/toMap/props, so if the app is force-quit while a dialog is open, it will deserialize as true and permanently block re-display.
Fix: Move isDialogShown to a local bool field in _DashboardShellState, or use a minimal StateProvider<bool> in the View layer.
remote_client_provider.dart — Active poll timer not cancelled in dispose()
RemoteClientNotifier extends Notifier with no dispose() override anywhere in the file. The _activePollTimer self-reschedules indefinitely via _scheduleNextPoll(). If the container is torn down (hot-restart, router replacement, test teardown) before endRemoteAssistance() is called, the timer orphans and keeps firing cloud API calls every 5 seconds.
Fix:
@override
void dispose() {
_stopActivePolling();
_expiredCountdownTimer?.cancel();
super.dispose();
}⚠️ Warning Details
Stale sessionInfo when fetchSessionInfo returns null [remote_client_provider.dart · pollSessionOnce]
final sessionInfo = await fetchSessionInfo(sessions.first.id);
if (sessionInfo == null) {
return null; // ← state.sessionInfo NOT cleared
}The previous initiateRemoteAssistance did state = RemoteClientState() on null return — a full reset. Now state is left stale, and _scheduleNextPoll() uses the old state.sessionInfo?.status for its interval, potentially keeping 60-second cadence after the session is gone.
Fix: Add state = state.copyWith(sessionInfo: () => null, pin: () => null); before the early return.
setDialogShown(false) can get permanently stuck [remote_assistance_dialog.dart]
setDialogShown(true) is called before showDialog; the reset is only in .then(). If showDialog throws synchronously (stale context, disposed provider), .then() never fires and isDialogShown stays true permanently, blocking all future dialog presentations.
Fix: Chain .catchError((_) => notifier.setDialogShown(false)) alongside .then().
In-flight pollSessionOnce after endRemoteAssistance [remote_client_provider.dart · _scheduleNextPoll]
pollSessionOnce performs 3 sequential awaits. During that window, endRemoteAssistance can fire. _stopActivePolling() sets _activePolling = false, but the in-flight coroutine resumes and state mutations (including createPin) can still fire for a session being cleaned up.
Two parallel session-tracking mechanisms [remote_client_provider.dart]
Post-merge the notifier has two entirely separate session-tracking mechanisms: initiateRemoteAssistance (timer-based, local-login) and initiateRemoteAssistanceCA (stream-based, cloud-auth), with different null-handling and idempotency contracts. Consider extracting a unified _RemoteSessionPoller collaborator.
No back-off when initial poll fails [remote_client_provider.dart · initiateRemoteAssistance]
_startActivePolling() is called unconditionally even when initial pollSessionOnce throws. If the device is offline, the loop hammers the cloud API every 5 seconds indefinitely with no back-off.
Dual-boolean guard in dashboard_shell.dart
Two independent booleans (_isRemoteSessionDialogShown + isDialogAlreadyShown) guard the same condition. If they diverge (stuck-flag scenario from above), the composite guard either allows a duplicate dialog or permanently prevents it. Consolidate to one authoritative source.
✅ What looks good
- 200+ lines of well-structured unit tests covering
pollSessionOnce,nextPollInterval, polling lifecycle, and dialog flag — excellent regression coverage. - The
hasInitializedguard inshowRemoteAssistanceDialogcorrectly prevents re-running init on everyConsumerrebuild. setDialogShown(true)called beforeshowDialog(not inside the builder) avoids a window where the dialog is open but the flag is unset._stopActivePolling()called first in_startActivePolling()is a clean idempotent restart pattern.- The 5s/60s adaptive interval is a sensible performance/responsiveness tradeoff.
- Formatting-only changes in
pnp_*.dartfiles are appropriate housekeeping.
Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.
|
Thanks for the automated review. Note this round was based on Already resolved on the branch (predating this review's base):
Addressed in this round:
Deferred (out of this PR's scope):
|
|
Thanks for the thorough breakdown @PeterJhongLinksys — the item-by-item status is clear, and I agree with deferring W-1. One thing worth flagging from that last point: if |
HankYuLinksys
left a comment
There was a problem hiding this comment.
Approving — review feedback addressed and verified on the branch. The deferred expiredIn contract question is fine to follow up separately. Thanks for the turnaround.
AustinChangLinksys
left a comment
There was a problem hiding this comment.
🤖 Automated Review — Round 4 · 7b8229d..7345544 (incremental)
Verdict: 💬 Comment — Critical: constant moved but main usage site still hardcodes 2700
| Where | Issue (one-liner) | |
|---|---|---|
| 🔴 | lib/page/components/styled/remote_assistance/remote_assistance_dialog.dart:94 |
_buildPendingWidget still uses magic 2700 — constant move is incomplete |
test/core/cloud/providers/remote_assistance/remote_client_provider_test.dart |
New test uses active-session fixture for pending-formula — test intent is unclear | |
lib/page/components/styled/remote_assistance/remote_assistance_dialog.dart:94 |
* -1 (production) vs .abs() (tests) semantic divergence not fixed in this PR |
|
lib/core/cloud/model/guardians_remote_assistance.dart:8 |
kPendingSessionDurationSec only consumed by UI layer — mild SRP concern for model file |
|
| 💡 | lib/core/cloud/model/guardians_remote_assistance.dart:44 |
Docstring example contains real-looking serial number — recommend placeholder |
| 💡 | Test suite | Other per-file private fake classes remain; consider migrating to shared mocks |
No items were flagged by both reviewers simultaneously.
🔴 Critical Details
remote_assistance_dialog.dart:94 still hardcodes 2700
This PR moves kPendingSessionDurationSec = 2700 from remote_assistance_dialog.dart to guardians_remote_assistance.dart to create a single source of truth. However, the actual usage site in _buildPendingWidget was not updated:
// lib/page/components/styled/remote_assistance/remote_assistance_dialog.dart:94 (current)
final initialSeconds = (2700 + (state.sessionInfo?.expiredIn ?? 0)) * -1;
// ^^^^ should be kPendingSessionDurationSecThe import guardians_remote_assistance.dart (line 10) is already present, so the constant is accessible. This means the refactoring is only half-complete — if kPendingSessionDurationSec is ever changed, line 94 silently diverges.
Fix: Replace 2700 with kPendingSessionDurationSec at line 94. One-character change.
⚠️ Warning Details
W-1: New test uses active-session fixture for pending-formula — remote_client_provider_test.dart (new test)
testSessionInfo has status: GRASessionStatus.active and expiredIn: -748. The pending countdown formula (kPendingSessionDurationSec + expiredIn).abs() is semantically for PENDING sessions. Using an active-status fixture makes the test intent ambiguous for future maintainers.
Assertion: expect(initialSeconds, kPendingSessionDurationSec - 748) = 1952 ✓ (math is correct; testSessionInfo.expiredIn = -748 matches the model docstring example).
Fix: Add a comment: // Tests arithmetic correctness of the abs() formula; session status is irrelevant here — or use a pending-specific fixture.
W-2: * -1 vs .abs() semantic mismatch
Production code (remote_assistance_dialog.dart:94): (2700 + expiredIn) * -1 → for expiredIn = -748, result is -1952.
All tests: (kPendingSessionDurationSec + expiredIn).abs() → result is 1952.
Same magnitude, opposite signs. TimerCountdownWidget presumably expects a positive count-down value, which the * -1 produces when expiredIn is negative. The tests use .abs() which is algebraically equivalent for negative expiredIn, but the semantic intent differs. This pre-existing mismatch is not fixed in this PR. Recommend a follow-up issue.
W-3: kPendingSessionDurationSec SRP concern — guardians_remote_assistance.dart:8
This constant represents a UI countdown window, not a domain data attribute. It is consumed only by remote_assistance_dialog.dart (UI layer). All 8 other importers of guardians_remote_assistance.dart gain implicit access to a UI timing constant. Not a blocker, but consider a remote_assistance_config.dart for future constants.
✅ What looks good
- Constant extraction from dialog to model layer is directionally correct — tests now correctly reference
kPendingSessionDurationSec(7 test references updated) _FakeDeviceManagerNotifier→MockDeviceManagerNotifier+ Mockitowhen().thenReturn()is the right shared-mock pattern- New test case for full formula with
testSessionInfoincreases coverage of the pending-calculation logic - Test fixture data (
AA:BB:CC:DD:EE:FF, serialTEST123) are clearly placeholder values — no security concern mockDeviceManagerNotifier.build()+ProviderContainer.overrideWith()correctly initializes Riverpod state for tests
Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.
* chore: bump version to 1.3.0 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: remote assistance for local login users (#956) * feat: enable remote assistance support agent icon and improve remote checkDeviceInfo timeout - Uncomment and enable support_agent icon in TopBar for local login - Add retry (1) and increased timeout (10s) for checkDeviceInfo in remote mode - Add explicit text color for session expire counter Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: format codebase Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: detect active remote session for local login and show alert dialog - Add checkActiveSession in polling for local login users - Add session status listener in DashboardShell with loginType guard - Support passive mode in remote assistance dialog - Fix countdown timer using .abs() instead of * -1 - Prevent dialog from showing in remote login mode Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * docs: add comment explaining async stream state update timing Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * style: format remote assistance code Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: extract magic number 2700 to named const and add unit tests for checkActiveSession - Extract 2700 to kPendingSessionDurationSec constant (45-min PENDING window) - Add unit tests for checkActiveSession (empty/active/error cases) - Add unit tests for countdown .abs() behavior Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * feat: continuously poll session info in active remote assistance dialog Keep polling session info while the active-mode remote assistance dialog is open, auto-request a PIN once a session exists, and avoid opening a second dialog when the session becomes active. - Add nextPollInterval helper (5s before active, 60s once active). - Add pollSessionOnce: clears session info and pin when no session exists, requests a PIN when the session is INITIATE or PENDING and no PIN exists. - Rewrite initiateRemoteAssistance to poll once immediately then keep polling via a cancelable timer until the dialog closes; no longer returns early on empty sessions. Guard against re-entry and keep the loop alive across transient poll errors. - Add an isDialogShown flag to RemoteClientState so the dashboard does not auto-open a passive dialog while a dialog is already shown. - Document the hasInitialized guard and the dashboard's local re-entry guard. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * style: apply dart format to pnp files Apply `dart format` formatting to instant setup (PnP) files and the generated pnp mocks. No logic changes. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * refactor: address PR review for remote assistance - Move kPendingSessionDurationSec from the view layer to the core model (guardians_remote_assistance.dart) per the 3-layer architecture rule. - Reference kPendingSessionDurationSec in the countdown tests instead of hardcoding 2700, and assert the full pending-window formula. - Replace the bespoke _FakeDeviceManagerNotifier with the existing generated MockDeviceManagerNotifier. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> * feat: add Web Interface Guide link to FAQ page Add a prominent card at the top of the FAQ list linking to the generic Pinnacle router support article (KB #9921). The card includes info and external link icons in primary color. Added translations for all 26 supported languages. Closes #985 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add identifier to AppCard for UI automation and a11y Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: screenshot testing fixes and documentation (#990) * fix: add simpleModeWifi to wifi test data and fix semanticLabel spacing - Add simpleModeWifi field to all wifi list test state constants - Set isSimpleMode to false for advanced mode tests - Fix semanticLabel spacing issue in WifiNameField and WifiPasswordField when semanticLabel is empty - Add IPTV enabled test state and screenshot test for Advanced settings Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: update test data and mocks for screenshot tests - Add macAddress field to all node detail test states - Update MockServiceHelper with new isSupportHealthCheckManager2 method Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * docs: add screenshot testing guide and update mock file - Add English screenshot testing guide covering commands, mock workflow, common errors, and test data locations - Fix minor formatting in jnap_service_supported_mocks.dart Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * feat: add Auto Master status check in PnP flow (#994) * feat: add Auto Master status check in PnP flow to prevent race condition - Add GetAutoMasterStatus JNAP action to detect if Auto Master is running - Implement two defense lines: 1. PnP Admin: check after examineAdminPassword, wait if running 2. PnP Setup: check before save, handle edge case (Idle→Complete) - Add connection error UI with retry when polling fails 3 consecutive times - Add screenshot tests for Auto Master waiting and connection error states Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add auth flag and timeout handling for Auto Master status check - Add auth: true to getAutoMasterStatus API calls in pnp_provider - Add _checkAutoMasterStatus step in admin view login flow - Add timeout fallback logic when Auto Master polling exceeds max retry - Handle router reconnection check after polling timeout Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: handle unauthorized error in Auto Master status check Add ExceptionAutoMasterUnauthorized to handle 401 errors during Auto Master status check, redirecting users to login page when session expires. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address code review issues in Auto Master status check - Add missing ExceptionAutoMasterUnauthorized handler in _retryAutoMasterCheck - Add retry limit to prevent infinite recursion in _saveChanges - Use ValueGetter pattern for autoMasterStatusOnEntry in copyWith - Remove unused ExceptionAutoMasterRunning class Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: throw exception on Auto Master polling failure to stop chain Critical fix: When Auto Master polling fails (consecutive failures or timeout with reconnect failure), the previous `return` statement would let the Promise chain continue, causing uncontrolled navigation to pnpConfig and bypassing the Auto Master protection. Changes: - Add ExceptionAutoMasterPollingFailed exception - Replace `return` with `throw ExceptionAutoMasterPollingFailed()` on failure paths - Add catchError handlers in initState chain, Continue button chain, and retry handler The UI correctly stays on the error view with retry button when polling fails. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * fix: only show DFS confirmation when DFS is being enabled The DFS confirmation dialog was showing whenever any WiFi advanced setting was saved, as long as DFS was already enabled. This was confusing when users only changed IPTV or other settings. Now the dialog only appears when DFS is being turned ON (changing from disabled to enabled), not when it's already enabled. Fixes #1004 Related: linksys/LinksysWRT#399 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: correct PnP saveChanges timing for AutoParent scenario The AutoParent scenario (false, false, true) had incorrect saveChanges logic - WiFi steps used `? null : null` which always returned null, causing saving to happen at YourNetwork instead of before it. Changes: - AutoParent: save before YourNetwork (like Unconfigured) to ensure smart mode = master before adding nodes - PrePaired fallback: restore original design with no saveChanges, letting onLastStep handle saving - Fix Auto Master tests: use immediate Stream.value to avoid pending timer issues, rename unauthorized test to connection error test Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: handle AutoParent flow in _saveChanges and reconnect The initial fix only updated buildSteps but missed the flow control logic that determines what happens after saving completes. Changes: - Update _saveChanges whenComplete to use showYourNetwork instead of isUnconfigured, so AutoParent continues to YourNetwork step - Update reconnect flow to use showYourNetwork for consistent behavior - Update onLastStep to use showYourNetwork to avoid duplicate saving - Improve Auto Master connection error UI with AppCard and maxWidth Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: restore onLastStep for forceLogin + !isPrePaired scenario The previous change to onLastStep condition broke the forceLogin scenario where isUnconfigured=false and isPrePaired=false. In this case: - buildSteps returns only PersonalWiFiStep() without saveChanges - onLastStep was set to null due to !isPrePaired condition - Result: WiFi changes were silently dropped Fix: Exclude forceLogin from the !isPrePaired condition so that forceLogin scenarios still use onLastStep for saving. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: merge duplicate case branches and add canGoNext guard to GuestWiFiStep Address code review feedback: - Merge (false, true, _) and (false, false, true) into (false, _, true) since they had identical bodies - prevents future divergence - Add canGoNext(saveChanges == null) to GuestWiFiStep.onInit to prevent potential double-advance when GuestWiFi is the save-carrying step Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * feat: add Failed status support for Auto Master - Add 'failed' status to AutoMasterStatus enum for cases where Auto Master exits without configuring the device (e.g., found another Master on network) - When Auto Master status is 'Failed', continue normal PnP flow instead of redirecting to login page, since password is still 'admin' Ref: LinksysWRT#383 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: handle Failed status in pnp_provider and pnp_setup_view - Add failed to pollAutoMasterStatus() condition to stop polling - Add failed status handling in pnp_setup_view's polling loop - Use flag to skip timeout handling when Auto Master failed This fixes the issue where users would be stuck on the waiting screen for ~5 minutes when Auto Master reports Failed status. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add loading indicator and retry for SmartConnect not ready in Add Nodes - Replace AppFilledButton with AppFilledButtonWithLoading for the Next button - Add retry logic (3s interval, 20 retries max = 60s) for ErrorSmartConnectNotReady - Show error snackbar on timeout, allow manual retry - Create AddNodesException sealed class for exception handling - Hide topbar only when entering from PnP flow This fix provides visual feedback while waiting for SmartConnect to be ready. It does not address the root cause (SmartConnect startup time after PnP). Closes #1018 Related: linksys/LinksysWRT#396 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address code review issues for Add Nodes loading retry - Remove unused ExceptionSmartConnectNotReady class - Use try-finally to ensure polling resumes on error/timeout - Add async/await and try-catch to _resultView tryAgain button - Add general catch block to prevent spinner freeze on non-timeout errors - Replace showSimpleSnackBar with showFailedSnackBar for error states Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: reset isLoading in finally block to prevent frozen spinner Ensures isLoading is reset to false even if an exception occurs during the auto-onboarding process, preventing the UI from being stuck on the loading spinner. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: show loading spinner during SmartConnect retry and add mounted check - Move isLoading: true before retry loop so full-screen spinner shows during the entire retry period (~57s), not just after retry succeeds - Update plugins/widgets submodule with mounted check fix - Remove unused import Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * Disable remote assistance for local login (#1102) * Disable remote assistance for local login Comment out the remote assistance entry points for local login sessions: - Skip the active remote session check during polling in polling_provider, which prevents the passive dialog from being triggered in dashboard shell. - Hide the support agent button that launches the remote assistance dialog from the top bar. * Apply dart format to touched files Reformat files with dart format (line wrapping and indentation only, no logic changes) across pnp setup/admin views, add nodes provider and view, wifi name field, top bar, and the related localization tests and pnp notifier mocks. * fix: display CPU utilization as integer without decimal places (#1096) Remove unnecessary two decimal places from CPU utilization display. Values now show as "0%" instead of "0.00%" and "18%" instead of "18.00%". Fixes #1095 Related: LinksysWRT#411 Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> * fix: Auto Master race condition in ISP Settings flow (#1092) * fix: add Auto Master protection to ISP Settings flow and improve error handling - Add Auto Master status check in pnp_isp_save_settings_view before saving ISP settings (Static IP / PPPoE). Shows waiting UI if Auto Master is running, redirects to PnP entry if completed. Handles polling failures and connection errors gracefully. - Handle _ErrorUnauthorized in pnp_setup_view's _saveChanges catchError. When Auto Master completes during save, the 401 error now redirects to login page instead of showing generic error. - Add WiFi settings verification after reconnect in _showNeedReconnect. Checks if SSID was applied correctly after save timeout, supports split mode. Re-saves once if mismatch detected. Fixes QA test failures: Case 3, 4, 6, 7, 9, 10 from issue #1006. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: address code review issues for Auto Master race condition - Change unauthorized error redirect from localLoginPassword to pnp - Reset _waitingForAutoMaster in onRetry to prevent stuck UI state - Fix testConnection to properly await async success callback Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> * fix: add mounted checks in whenComplete and WiFi verification - Add mounted check at start of whenComplete to prevent setState on disposed widget - Add mounted check before _saveChanges() in WiFi verification re-save path Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com> Co-authored-by: Peter Jhong <52424995+PeterJhongLinksys@users.noreply.github.com>
Summary
Test plan
🤖 Generated with Claude Code