feat(pnp): add "Log into router" bypass on no-internet page (#1104) - #1165
Conversation
The USP no-internet page had no exit: a router that is online but has no WAN (e.g. a node paired behind an upstream router, or a factory-default box with no cable) trapped the user with only Restart modem / Enter ISP / Try again. This ports the dev-1.3.0 "Log into router" escape hatch. Two deliberate decisions differ from 1.3.0: - Bypass acknowledges PnP completion (Tr181 SetUserAcknowledgedAutoConfig) before navigating, so a later `/` redirect (full-page reload) does not bounce the user back into PnP. Without acknowledging, the redirect's needsPnp check would re-trap them. acknowledge is best-effort (fire-and- forget, no WAN needed) — a rare silent failure only re-offers PnP once. - Bypass goes straight to the dashboard rather than into a trimmed-down wizard. The USP wizard is phase-driven and has no forceLogin branch, and the dashboard does not depend on internet being up. WiFi stays at factory defaults; the user can personalize it later from settings. bypassToDashboard() never throws — an escape hatch must always let the user through, so a failed acknowledge/save is logged and swallowed. Adds the logIntoRouter l10n key (26 locales, values from dev-1.3.0) and notifier tests covering the SN-present, SN-missing, and acknowledge-failure paths.
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 · d796b51..a4a5e32 (full)
Verdict: ✅ APPROVE — 0 Critical; 5 Warnings flagged (4 worth addressing before merge). Logic is sound, auth guard is correct, notifier-layer test coverage is solid.
| Conf. | Where | Issue (one-liner) | |
|---|---|---|---|
| High | pnp_no_internet_view.dart:102 |
[both reviewers] Cross-button guard missing — _retrying and _bypassing are independent; both buttons can fire concurrently causing a double-navigation race |
|
| High | pnp_notifier.dart:425-441 |
Duplicate acknowledge+save sequence from saveChanges() (line 266-274) with inconsistent error-handling semantics — extract to _acknowledgeAndSaveSession() |
|
| Med | pnp_notifier.dart:433 |
acknowledge() is a USP Operate mutation but is not wrapped in uspMutationLockProvider.withLock() — violates Provider mutation rule |
|
| Med | pnp_notifier.dart:427-430 |
SN-null early-return is silent to caller — navigation still proceeds without persisting pCurrentSN or acknowledging; PnP re-offered on next / reload (by design per doc, but no view-layer test covers this path) |
|
| Med | pnp_notifier.dart:425-441 |
No terminal phase transition after bypass — state stays NoInternet; _onLogIntoRouter calls context.go() directly, bypassing the view's ref.listen routing pattern |
|
| 💡 | High | pnp_no_internet_view.dart:103 |
Trailing ) indented 2 extra spaces vs. sibling AppButton.text block — likely triggers Dart formatter warning |
| 💡 | High | test/page/instant_setup/ |
No View-layer widget tests for _bypassing loading state, button disable (onTap: null), or navigation to uspDashboard |
| 💡 | Low | pnp_notifier_test.dart:27 |
SpySessionNotifier diamond-inherits Notifier<SessionState> + SessionNotifier — safe if always used via overrideWith(() => SpySessionNotifier()), risky if directly instantiated (please double-check) |
| 💡 | Low | pnp_notifier.dart:425 |
Success path in bypassToDashboard() has no info-level log; other notifier methods log on success for diagnostics |
Confidence: High = code-verified · Med = located + reasoned, not fully confirmed · Low = speculative, please double-check.
Items marked [both reviewers] were independently flagged by two agents → higher confidence.
⚠️ Warning Details
W-1 [both reviewers] — Cross-button mutual exclusion missing
pnp_no_internet_view.dart:102 (new onTap) and existing onTap: _retrying ? null : _onRetry
// Log into router button (new, ~line 102):
onTap: _bypassing ? null : _onLogIntoRouter,
// Try again button (existing):
onTap: _retrying ? null : _onRetry,Each button only guards itself. When _retrying = true, the "Log into router" button is still tappable and vice versa. Scenario: user taps "Try again" -> retryInternetCheck() starts -> user taps "Log into router" -> bypassToDashboard() starts concurrently -> context.go(uspDashboard) fires -> retryInternetCheck() completes, internet may recover -> ref.listen triggers context.go(pnp) -> two context.go calls race, final destination is undefined.
Fix: Use a combined _busy = _retrying || _bypassing guard on both onTap fields, or add cross-checks in each handler (if (_retrying) return; / if (_bypassing) return;).
W-2 [single] — SN-null early-return: navigation proceeds unguarded
pnp_notifier.dart:427-430
if (sn == null || sn.isEmpty) {
logger.w('[PnP] bypassToDashboard: no serial number, skipping acknowledge');
return; // method completes normally — no throw
}Caller _onLogIntoRouter cannot distinguish this case from a successful bypass and proceeds to context.go(RoutePath.uspDashboard). The user enters the dashboard without pCurrentSN persisted and without UserAcknowledgedAutoConfig set. On a subsequent full-page reload routed through /, router_provider._prepare re-runs the PnP check and bounces the user back. The doc comment acknowledges the acknowledge-fail case but not the SN-null navigation case.
Fix (low-friction): Add a widget test that exercises the SN-null path from the view layer and asserts that context.go is still called (confirming the design is intentional). Medium friction: consider returning a bool/enum from bypassToDashboard() so the view can decide whether to navigate.
W-3 [single] — Duplicate acknowledge+save logic with divergent error semantics
pnp_notifier.dart:425-441 (new) vs. pnp_notifier.dart:266-274 (existing saveChanges)
// saveChanges() — no local catch, errors propagate upward:
await ref.read(pnpStatusServiceProvider).acknowledge(state.serialNumber!);
await ref.read(sessionProvider.notifier).saveSelectedNetwork(state.serialNumber!, '');
// bypassToDashboard() — swallows all errors silently:
try {
await ref.read(pnpStatusServiceProvider).acknowledge(sn);
await ref.read(sessionProvider.notifier).saveSelectedNetwork(sn, '');
} catch (e) { logger.w(...); }The two copies have different error-propagation semantics; future changes to one will likely miss the other.
Fix: Extract _acknowledgeAndSaveSession(String sn, {bool swallowErrors = false}) and have both callers use it. Explicitly document the fire-and-forget vs. propagating distinction.
W-4 [single] — withLock() missing on USP Operate
pnp_notifier.dart:433
await ref.read(pnpStatusServiceProvider).acknowledge(sn);
// Tr181PnpStatusService.acknowledge -> SetupOperations.setUserAcknowledgedAutoConfig(_usp)
// This is a USP Operate (write) — architecture rule: all Provider mutations must
// wrap uspMutationLockProvider.withLock()If another Provider is mid-mutation when bypass is triggered, concurrent USP writes could occur. Note: saveChanges() also places acknowledge() outside its withLock block (line 267) — this may be an accepted existing pattern. Please confirm whether Tr181 Operate calls are considered lock-exempt.
W-5 [single] — No terminal phase transition after bypass
pnp_notifier.dart:425-441; pnp_no_internet_view.dart:36-37
// bypassToDashboard() does not emit any new phase — state.phase remains NoInternet.
// _onLogIntoRouter navigates directly (imperative), not via ref.listen:
await ref.read(pnpProvider.notifier).bypassToDashboard();
if (mounted) context.go(RoutePath.uspDashboard);All other navigation from this view flows through ref.listen(pnpProvider, ...). The bypass is the sole exception. If pnpProvider is read elsewhere after bypass, it still reports NoInternet phase, which could confuse downstream logic. Emitting a PnpBypassed (or similar) phase would make the state machine self-documenting and consistent.
✅ What looks good
- Auth guard intact:
RoutePath.uspDashboardstarts with/usp;router_provider.dartchecksisLoggedInbefore allowing any/usp*route. The bypass cannot be used by an unauthenticated user. - No hardcoded secrets in any changed file.
- Escape-hatch error contract is correct: double try/catch (inner in
acknowledge()service + outer inbypassToDashboard()) ensures navigation is never blocked by a USP write failure. Design intent is clearly documented. - Notifier-layer test coverage solid: three
bypassToDashboardtest cases (happy path, no-SN, acknowledge-throws) withSpySessionNotifiercorrectly mounted viaoverrideWith— this is the right pattern. mountedguards present in all async widget callbacks — no setState-after-dispose risk in the happy path.- L10n complete: all 26 locale files updated with the
logIntoRouterkey.
Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.
- Guard both no-internet buttons on a combined `_busy` flag so a retry and a bypass can't run concurrently and race their navigation. - Document the intentional divergence in error handling between bypassToDashboard() (swallows) and saveChanges() (propagates), cross-linking both sites so future edits keep them in sync. - Add a success-path info log to bypassToDashboard(). - Add view-layer widget tests for PnpNoInternetView: both buttons render, tapping "Log into router" bypasses and navigates to the dashboard, and the bypass still navigates when the notifier skips acknowledge (SN-null path).
|
Thanks for the review. Point-by-point response, using your W-/S- labels (pushed in d86f3b5): W1 — Cross-button guard missing ( W2 — SN-null early-return is silent; no test covers navigation on that path — ✅ Test added. The SN-null branch is by design (navigation should proceed regardless). The notifier-level "skips acknowledge when serial number is missing" test already covers the no-throw behavior; I added a view-level test asserting the view still navigates to the dashboard when the notifier does nothing on bypass. W3 — Duplicate acknowledge+save with inconsistent error semantics ( W4 — W5 — No terminal phase transition after bypass — ⏭️ No change. S1 — Trailing S2 — No View-layer widget tests — ✅ Added. New S3 — S4 — No success-level log in |
Summary
Decision 1 — Bypass acknowledges PnP completion
Tapping "Log into router" calls
pnpStatusService.acknowledge()(TR-181SetUserAcknowledgedAutoConfig) before navigating.Why: the USP router redirect only re-checks
needsPnpwhen it passes through/or/localLoginPassword(router_provider._prepare). Without acknowledging, a full-page reload back to/would bounce the user right back into PnP. Acknowledging mirrors 1.3.0, where a configured router's save already sendsSetUserAcknowledgedAutoConfig.Trade-off: the user bypasses without configuring anything, yet we mark the router "PnP done" — slightly different from 1.3.0 (which acknowledges only after a WiFi step's save). Accepted because the USP flow has no unconfigured/configured split (
checkFactoryDefaultalways returns false), and keeping the user stably in the dashboard matters more.Decision 2 — Bypass goes straight to the dashboard (no mini-wizard)
Unlike 1.3.0 (which routed into a trimmed-down
forceLoginwizard), the USP bypass navigates directly to the dashboard.Why: the USP wizard is phase-driven and has no
forceLoginbranch; grafting one on would fight the architecture and carry a large surface (new wizard path, save, post-WiFi-change reconnect). The core complaint in #1104 is "router is online but I can't reach the UI" — going straight to the dashboard addresses that directly.Trade-off: a first-time-setup user with no WAN lands on the dashboard without being guided to set WiFi (stays at factory defaults; changeable later in settings). A future "show the wizard only for a true factory default" needs
checkFactoryDefaultfixed first (tied to #1104 Problem B/C).Known limitation
acknowledgeis fire-and-forget (best-effort): a silent TR-181 write failure would leave the router un-acknowledged, so a later reload could re-offer PnP once. This is benign, needs no WAN (normally succeeds while offline), and is the same acknowledge the normal wizard path uses.bypassToDashboard()deliberately swallows any error and still navigates — an escape hatch must never trap the user.Testing
logIntoRouterl10n key (26 locales, values from dev-1.3.0).