feat(l10n): centralize error message localization for USP features - #953
Conversation
Error info (fault code + raw message) was lost when errors converged into ServiceError, on both conversion paths: - Path 1 (mapUspErrorToServiceError): empty-constructor subtypes like ResourceNotFoundError/UnauthorizedError dropped both code and message. - Path 2 (UspResultParser -> Usp*FailureError): kept only joined summary + failedPaths, discarding per-path errorCode/errorMessage. Changes: - ServiceError base class gains optional `code` (int?) and `detail` (String?) diagnostic fields; all subtypes accept them via super (except ServiceSideEffectError, a success-with-side-effect type). - Remove the per-subtype `message` field from InvalidInputError/NetworkError/ ConnectivityError/UnexpectedError/ServiceNotInitializedError and unify on the base `detail`; toString() now reads `detail`. InvalidInputError keeps `field`, UnexpectedError keeps `originalError`. - mapUspErrorToServiceError now passes code (faultCode/httpStatus) + detail into every mapped ServiceError. - UspCompleteFailureError/UspPartialFailureError store the full List<UspErrorDetail> failures (path + code + message); `failedPaths` becomes a derived getter for backward compatibility. - Mechanical caller updates across 14 services + tests: message: -> detail:, failedPaths: -> failures:, and .message reads -> .detail (login flow, diagnostics notifiers).
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? |
All USP requests (Get/Set/Add/Operate) across feature pages now flow through a unified error handling pipeline: - Path 1 (fetch): errors stored in `state.error`, displayed via `ServiceErrorView` - Path 2 (save): errors rethrown to View, displayed via snackbar with `localizeServiceError` Key changes: - Add `ServiceError.code` and `ServiceError.detail` for diagnostic context - Add `TimeoutError` subtype for timeout handling - Remove unused OTP/admin-password ServiceError subtypes - Add `localizeServiceError()` central mapper with exhaustive switch - Add `ServiceErrorView` shared widget replacing per-feature `_buildError()` - Add 12 ARB keys for error messages - Change all feature state models: `String? errorMessage` -> `ServiceError? error` - Update providers to pass through ServiceError objects (not stringify) - Update views to use shared components Coverage: all USP feature pages except firmware_update (deferred). Note: SSE subscription errors are out of scope for this change. Note: UnexpectedError surfaces raw `detail` string as final fallback.
fce4b2f to
281169e
Compare
The batch localizer (_localizeBatch) only recognized the five 7xxx codes exposed by UspErrorDetail's helpers, so write failures carrying bbfdm vendor codes (9001/9005/9007/9008) or the WASM transport code (9999) all fell through to the generic errorUnexpected message. The fetch path (_mapProtocolError) already mapped these, so the same firmware code localized differently depending on which path produced it. Extract _localizeFaultCode(code) and switch on the raw errorCode, mirroring _mapProtocolError's table: - 7004/7005/7006/9008 -> errorInvalidInput - 7026/7027/9005/9007 -> errorResourceNotFound - 9001 -> errorUnauthorized - 9999 -> errorNetwork (never reached the router) Most user-visible win: a 9999 (no connection to the router) now reads "Network error. Please check your connection." instead of the vague "Something went wrong." UspErrorDetail's helpers are left untouched (still used by the test console). Unknown vendor codes still fall back to the generic message and deliberately do not surface raw firmware text.
…ocalization # Conflicts: # lib/page/internet_settings/services/usp_internet_settings_service.dart
There was a problem hiding this comment.
🤖 Automated Review — Round 1 · 65887b9..a11a11f (full)
Verdict: 💬 Comment — 1 Critical found, not auto-approving. Solid refactor overall; one user-facing regression blocks a clean approve.
| Where | Issue (one-liner) | |
|---|---|---|
| 🔴 | auth_provider.dart:103 + usp_auth_coordinator.dart:165 |
Account-locked message is swallowed → users see a generic error when locked out |
service_error_localizations.dart:78 + usp_error.dart:231 |
[both reviewers] Fault-code table duplicated in two places; already drifting (9999) | |
service_error_localizations.dart:46 |
[both reviewers] Raw firmware/technical strings can leak to the UI via UnexpectedError.detail |
|
service_error.dart:1 |
ServiceError reverse-imports USP types, breaking the "source-agnostic" contract |
|
service_error_localizations.dart, service_error_view.dart |
Two new logic-bearing files have zero tests | |
| 💡 | speed_test_view.dart:49,398 |
Inconsistent adoption — still hardcodes error UI/titles instead of ServiceErrorView |
| 💡 | ~12 status models | Repeated error + clearError boilerplate (a base mixin could remove it) |
Items marked [both reviewers] were independently flagged by two agents → higher confidence.
🔴 Critical — account-locked translation chain severed
The coordinator now throws UnexpectedError(detail: 'Account locked'), and _mapToViewError dropped its old branch:
- if (error is AdminAccountLockedError) {
- return UnexpectedError(message: errorAdminAccountLocked, originalError: error);
- }So it falls into the generic branch → detail: errorUnexpected. At login_local_view.dart:142, errorCode becomes '_ErrorUnexpected' → errorCodeHelper maps to unknownHandle. The correct handler errorAdminAccountLocked => localLoginTooManyAttemptsTitle (error_code_helper.dart:45) is now unreachable: the coordinator passes the literal 'Account locked' ≠ the constant errorAdminAccountLocked, and _mapToViewError would overwrite it anyway. Net effect: a locked-out user sees "Something went wrong".
Fix: restore the account-locked branch in _mapToViewError, or have the coordinator throw with detail: errorAdminAccountLocked so the existing localLoginTooManyAttemptsTitle handler fires.
⚠️ Warning — details (4 items)
Duplicated fault-code table — _localizeFaultCode (service_error_localizations.dart:78-86) duplicates the switch in _mapProtocolError (usp_error.dart:231-243); fetch path uses one, batch-write the other, kept in sync only by a comment. Already drifted: 9999→errorNetwork exists only in the localizer. Fix: extract one classifyFaultCode(int) -> ErrorKind called by both; localization only maps ErrorKind → string. A contract test asserting the tables agree would lock it down.
Raw strings leak to UI — UnexpectedError(:final detail) => detail ?? l.errorUnexpected (:46). On the fetch path usp_error.dart:244 builds UnexpectedError(detail: e.message) where e.message is raw router/WASM text, so a failed fetch renders "Protocol error … (code: 7099)". Contradicts the file's own comment ("we deliberately do NOT surface the raw firmware errorMessage"). Also detail is now overloaded (technical text / l10n-key / human message). Fix: fall back to l.errorUnexpected; move auth to typed subtypes instead of stuffing identifiers into detail.
Contract layer coupling — service_error.dart:1-2 imports core/usp/models/... for UspErrorDetail, yet claims (L11-13) to be the source-agnostic contract. Fix: define a source-agnostic FailureDetail in core/errors, convert UspErrorDetail into it at the service layer.
Zero tests on new logic — service_error_localizations.dart (88 lines: batch first-pick, fault-code branches, sealed switch, fallback) and service_error_view.dart (error/null render) have no test references (confirmed via grep). This is exactly the logic that should guard the "two tables in sync" invariant. Fix: add type→key mapping tests + _localizeBatch / _localizeFaultCode branch tests.
✅ What looks good
- Typed-contract migration is clean and exhaustive; sealed switches cover all result categories; batch failures preserve
UspErrorDetail. - Three-layer dependency direction holds; no codegen-model leakage into Provider/View;
ServiceErrorViewusesui_kit_library. - Existing tests updated consistently (
errorMessage contains→isA<XxxError>();message:→detail:; mocks throw typed errors). - Null/empty + async-race paths checked: empty-failures guard, safe
fromMapdefaults, correctTimeoutException/ServiceErrorsplit in diagnostics.
Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.
…or removal
Removing the AdminAccountLockedError subtype severed the lockout-message
chain: the coordinator threw UnexpectedError(detail: 'Account locked') — a
free-form string that does not equal the errorAdminAccountLocked constant
('ErrorAdminAccountLocked') — and _mapToViewError no longer had an
account-locked branch, so it fell into the generic ServiceError arm and
overwrote detail with errorUnexpected. The login view then resolved
'_ErrorUnexpected' to unknownHandle, so a locked-out user saw "Something
went wrong" instead of the too-many-attempts / account-locked message. A
security-relevant lockout signal was swallowed.
Fix:
- Coordinator throws UnexpectedError(detail: errorAdminAccountLocked) — the
actual error-code identifier the view's errorCodeHelper recognizes.
- _mapToViewError passes such an UnexpectedError through unchanged instead
of overwriting it to errorUnexpected.
Tests:
- usp_auth_coordinator_test: tryUspLogin maps account-locked WASM error to
UnexpectedError(detail: errorAdminAccountLocked); plus invalid-credentials
and authenticated=false cases.
- auth_notifier_test: localLogin keeps errorAdminAccountLocked through
_mapToViewError (regression guard).
Both files shipped with zero test references. Add unit/widget coverage for the error-display logic, asserting the TYPE/CODE → l10n-key mapping (compared against loc(ctx).errorXxx, not hardcoded English) so the tests survive copy changes but break on mapping drift. service_error_localizations_test: - every sealed subtype → its l10n string (incl. infra types → errorUnexpected) - UnexpectedError surfaces detail when present, else fallback - non-ServiceError input → errorUnexpected - batch _localizeFaultCode per-code branches: 7004/7005/7006/9008 → invalidInput, 7026/7027/9005/9007 → resourceNotFound, 9001 → unauthorized, 9999 → network, unknown vendor code → unexpected (guards the no-raw-text-leak rule) - empty failures → unexpected; first-failure selection; partial-failure path service_error_view_test: - renders title + localized detail + retry when error is set - hides the detail line when error is null - invokes onRetry on tap
AustinChangLinksys
left a comment
There was a problem hiding this comment.
🤖 Automated Review — Round 2 · a11a11f..c40e92f (incremental)
Verdict: ✅ APPROVE — Core account-locked l10n fix is clean and well-tested; approving with warnings to clarify before merge (VLAN SET no-op risk + pppoeServiceName 9001 re-enable).
| Where | Issue (one-liner) | |
|---|---|---|
| ⚠ | lib/page/internet_settings/services/usp_internet_settings_service.dart:163 |
[both reviewers] Enabling VLAN when no existing instance is silently a no-op — SET-only path dropped the old Add fallback. |
| ⚠ | lib/page/internet_settings/services/usp_internet_settings_service.dart:302 |
[both reviewers] pppoeServiceName SET re-enabled (was disabled for bbfdm fault 9001); no test guards the new SET path and 9001 would surface as "unauthorized". |
| ⚠ | lib/page/internet_settings/services/usp_internet_settings_service.dart:21 |
Class doc still says "PPP/VLAN multi-instance lifecycle (Add/Delete)" — stale after VLAN moved to pure SET. |
| 💡 | lib/core/usp/providers/usp_auth_coordinator.dart:51 |
Account-locked regex `Account.*(locked |
| 💡 | lib/providers/auth/auth_provider.dart:332 |
An l10n-key string (errorAdminAccountLocked) is reused as a cross-layer control signal via ==; a dedicated sealed subtype (like InvalidCredentialsError) is the repo convention. |
| 💡 | lib/page/internet_settings/services/usp_internet_settings_service.dart:448 |
InternetSettingsFetchResult field doc still calls VLAN path "lifecycle management"; same drift as the class doc. |
Items marked [both reviewers] were independently flagged by two agents → higher confidence.
⚠ Warning details (open for evidence + fixes)
W1 — VLAN enable becomes a silent no-op when no instance exists usp_internet_settings_service.dart:149-151
The new code calls _saveVlanSettings only when vlanInstancePath != null. The removed _handleVlanLifecycle previously did VlanTermination.add in the "enable + no instance" case. The risk is corroborated by the PR's own tests: test/page/internet_settings/services/usp_internet_settings_service_test.dart:832 (skips VLAN SET when no vlanInstancePath provided) asserts no VLANTermination key is sent even with edited.vlanEnabled=true, and test/page/instant_setup/services/pnp_service_test.dart:715-716 notes codegen skips instances where all fields are zero/false/empty. So a device whose VLANTermination.1 is disabled (VLANID=0) yields vlanInstancePath==null → toggling VLAN on saves nothing and reports no error.
Fix: confirm firmware always pre-provisions the instance; otherwise keep the Add fallback or raise a visible error when vlanInstancePath==null && edited.vlanEnabled.
W2 — pppoeServiceName SET re-enabled without test + lands as wrong localized error usp_internet_settings_service.dart:302-303
The field was deliberately disabled with the comment "bbfdm rejects SET (fault 9001)"; this round uncomments pppoeServiceName: _diff(...). _diff only emits on change, so it fires only when the user edits Service Name — but if firmware still rejects it, 9001 recurs, and per _localizeFaultCode 9001 => errorUnauthorized, the user sees an "unauthorized" message unrelated to the real cause. The diff adds VLAN and account-locked tests but no assertion that a pppoeServiceName change is included in the PPP SET payload.
Fix: add a test covering the new SET path and cite in the PR description the evidence that fault 9001 is resolved firmware-side.
W3 — Stale class doc comment usp_internet_settings_service.dart:21
Class doc still reads "Handles PPP/VLAN multi-instance lifecycle (Add/Delete)…" after the Add/Delete lifecycle (_handleVlanLifecycle, _handleDeleteResult) was removed. Update to reflect SET-based enable/disable on an existing instance (same drift in the InternetSettingsFetchResult field doc at :448).
✅ What looks good
- Account-locked error-code chain is self-consistent and tested.
usp_auth_coordinator.dart:165now throwsUnexpectedError(detail: errorAdminAccountLocked)(import added at :100);auth_provider.dart:_mapToViewErrorpassthrough (diff :332-334) preserves it ahead of the genericerrorUnexpectedoverwrite, so the login view'serrorCodeHelperresolves the lockout message and no raw string leaks. Covered byusp_auth_coordinator_test.dartandauth_notifier_test.dart. - VLAN disable via SET (
Enable=false) instead of Delete is an intentional design change;_saveVlanSettingsonly emits changed params via_diff, no spurious SET or race. - Layering intact.
usp_auth_coordinator.dart:100imports a constants file (not a codegen model); codegen models (VlanTermination,UspResultParser) stay in the Service layer; the View (usp_ipv4_section.dart) touches no codegen. - UI uses ui_kit_library. Re-enabled
AppTextFormField/AppGap.md()come frompackage:ui_kit_library/ui_kit.dart(import at :10) — no hand-rolled widgets. - No dead/orphan imports.
parseDeleteResult/_handleDeleteResultremoved together with their callers;VlanTerminationandUspResultParserstill used elsewhere. - Localization is exhaustive — sealed switch is total, empty failures and unknown vendor codes fall back to
errorUnexpectedwithout leaking rawerrorMessage; edge cases (first-failure selection, empty list, unknown code) are tested. - Tooling/docs improvements:
.claude/skills/review-pr-readiness/SKILL.mdadds a mandatory "run affected tests" gate step;.claude/settings.jsondrops2>/dev/nullso format errors surface.
Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.
Apply dart format to the files the CI format check reported (whitespace / line-wrapping only, no logic changes; affected tests still pass). Verified the whole repo is now format-clean: dart format --set-exit-if-changed passes (1037 files, 0 changed).
After #951 moved VLAN tagging from Add/Delete to SET on an existing instance, the class doc and InternetSettingsFetchResult field doc still described the old "PPP/VLAN multi-instance lifecycle (Add/Delete)". Update them to reflect the current behavior: PPP instance lifecycle still uses Add; VLAN enable/disable is a SET on the existing instance. Comment-only; no logic change.
…hared-helper localization (#997) * docs(error-handling): add existing research and planning docs as baseline Snapshot the six error-handling & localization docs before reorganizing them into a best-practices guide. Committing first so subsequent deletions are diff-trackable. * docs(error-handling): strip general hardcoded-strings content Narrow the docs to error-handling scope only, ahead of writing the error-handling best-practices guide: - Delete 05 (844-entry general hardcoded-strings tracking table) — no error-handling content. - Slim 03 down to error-relevant parts only: i18n framework decision (no slang migration), ServiceError diagnostic-field groundwork, and the "Service-layer text is not localized" criterion. Removed the ~460-entry general string audit (sections 1/2/4/5/7 + appendix). - README: update 03/04 entries and status board (04 done via PR #953, drop the "non-error strings" follow-up line). - 04: drop the two bullets pointing at non-error hardcoded strings. * docs(error-handling): consolidate into reference + implementation guide Reorganize the error-handling docs into the two intended deliverables: - NEW error-handling-implementation-guide.md — the "how": per-layer (Service/Provider/View) patterns for implementing error handling in a USP feature, what to show vs. hide, localization, gotchas, and a pre-PR checklist. All examples verified against the current codebase (post PR #953), notably the two fetch-display patterns (ServiceErrorView for state.error pages, _buildError+localizeServiceError for AsyncValue pages) and the try/catch save path. - RENAME 01-usp-error-roundtrip-reference.md → usp-error-handling-reference.md (the "why": full round-trip background). Drop its section 4 (localization plan, now implemented) and refresh the progress notes. - DELETE 02/03/04 — their content is absorbed into the two docs above. - README: index just the two docs with a "how vs. why" reading guide. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> * docs(error-handling): reframe in reference voice, not progress-tracker These are reference docs, not a sprint board. Drop "done/todo/current work" status language in favor of describing the state of things: - Describe where implementations live (PR #953) rather than marking items "completed". - Correct the reference's section 3, which still described the pre-PR#953 state as the present (Provider stringifies '$e', View has no mapper). Reframe those as "pain points before PR #953" that motivated the refactor; point to the implementation guide for how the code reads now. - Update the flow diagram's Provider/View boxes to current behavior. - Phrase the GET 9999->9998 bug and the missing contract test as known issues / TODO, not checkboxes. - Drop volatile count snapshots ("36 guards") in favor of the rule. * fix(l10n): localize error in performUspMutation shared helper The shared dashboard-card mutation helper showed failures with a raw `'Error: $e'`, bypassing the central `localizeServiceError` mapper. This slipped past PR #953 because the raw string lived inside the helper, not at the call sites the audit grepped for. Route the caught error through `localizeServiceError` like the feature views do. This brings all 8 cards that use `performUspMutation` (internet_settings renew lease, local_network reservations, admin time, port_forwarding, wifi ×2, devices) into the localized error pipeline in one change. Verified: flutter analyze clean; internet_settings suite (127 tests) passes; no test asserted the old string. * docs(error-handling): cover performUspMutation + fix two inaccuracies - Add §3.3: dashboard cards trigger mutations via the shared performUspMutation helper, which now localizes failures internally — framed as a convenience entry point, NOT a third localization strategy. Note successMessage is shown as-is (caller must pass a loc()'d string). Add matching PR-checklist line. - Fix two claims found during codebase verification: - The _localizeFaultCode <-> _mapProtocolError sync reminder is one-directional in code; reworded accordingly. - The save-snackbar example showed only showFailedSnackBar; noted the ScaffoldMessenger+SnackBar variant some pages use — the API is secondary, the string must come from localizeServiceError. * docs(constitution): align Article XIII with post-PR#953 error handling Update the error-handling articles to match the current codebase and add the missing UI-layer principle. Keep it principle-level; details point to the implementation guide. - §13.2: ServiceError now carries diagnostic code/detail; drop the deleted OTP/admin subtype examples; note code/detail are diagnostic-only. - §13.4.2: performFetch stores the typed `error: e`, not `errorMessage: '$e'`. - §13.4.1: use an existing subtype (InvalidInputError) in the example. - §3.3.5: error classes extend the sealed ServiceError (no AuthError). - §13.1: UI layer localizes via the central localizeServiceError mapper. - Add §13.6 UI Layer Error Display — states the principle and links to doc/error-handling-localization/error-handling-implementation-guide.md. - Bump Last Amended to 2026-06-29 (version unchanged). * fix(l10n): localize success messages in card mutations The 7 hardcoded English successMessage strings passed to performUspMutation (and showRecoveryDialog) were the success-side counterpart to the error leak already fixed — all on dashboard cards / shell, missed by PR #953. - Reuse existing parametrized key for DHCP renew: leaseRenewed('DHCP'). - Add 6 new keys (reservationAdded, reservationDeleted, reconnectedToRouter, timeSettingsSaved, ruleAdded, channelUpdated), translated across all 26 locales. Translations follow each locale's existing style anchors (reservationReleased, wifiSettingsSaved, timezoneUpdated…) and noun usage (e.g. zh 信道 / zh_TW 通道 / ja チャネル for "channel"; per-locale "router"). - Update the 7 call sites to loc(context).xxx. Verified: 26/26 locales carry each key, all arb files valid JSON, flutter analyze clean, touched-feature tests pass (498). * docs(error-handling): translate the three docs to English The committed/shared docs must be English. Translate the README, the implementation guide, and the round-trip reference from Traditional Chinese to English in place. Pure translation — code blocks, identifiers, file paths, fault codes, ASCII diagrams, links, and section structure are preserved byte-for-byte; only prose was translated. Verified: 0 CJK characters remain, code fences balanced, sibling links resolve. --------- Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(error-handling): consolidate guides + align constitution + fix shared-helper localization (#997)
* docs(error-handling): add existing research and planning docs as baseline
Snapshot the six error-handling & localization docs before reorganizing them
into a best-practices guide. Committing first so subsequent deletions are
diff-trackable.
* docs(error-handling): strip general hardcoded-strings content
Narrow the docs to error-handling scope only, ahead of writing the
error-handling best-practices guide:
- Delete 05 (844-entry general hardcoded-strings tracking table) — no
error-handling content.
- Slim 03 down to error-relevant parts only: i18n framework decision
(no slang migration), ServiceError diagnostic-field groundwork, and
the "Service-layer text is not localized" criterion. Removed the
~460-entry general string audit (sections 1/2/4/5/7 + appendix).
- README: update 03/04 entries and status board (04 done via PR #953,
drop the "non-error strings" follow-up line).
- 04: drop the two bullets pointing at non-error hardcoded strings.
* docs(error-handling): consolidate into reference + implementation guide
Reorganize the error-handling docs into the two intended deliverables:
- NEW error-handling-implementation-guide.md — the "how": per-layer
(Service/Provider/View) patterns for implementing error handling in a
USP feature, what to show vs. hide, localization, gotchas, and a
pre-PR checklist. All examples verified against the current codebase
(post PR #953), notably the two fetch-display patterns (ServiceErrorView
for state.error pages, _buildError+localizeServiceError for AsyncValue
pages) and the try/catch save path.
- RENAME 01-usp-error-roundtrip-reference.md → usp-error-handling-reference.md
(the "why": full round-trip background). Drop its section 4 (localization
plan, now implemented) and refresh the progress notes.
- DELETE 02/03/04 — their content is absorbed into the two docs above.
- README: index just the two docs with a "how vs. why" reading guide.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(error-handling): reframe in reference voice, not progress-tracker
These are reference docs, not a sprint board. Drop "done/todo/current
work" status language in favor of describing the state of things:
- Describe where implementations live (PR #953) rather than marking
items "completed".
- Correct the reference's section 3, which still described the
pre-PR#953 state as the present (Provider stringifies '$e', View has
no mapper). Reframe those as "pain points before PR #953" that
motivated the refactor; point to the implementation guide for how the
code reads now.
- Update the flow diagram's Provider/View boxes to current behavior.
- Phrase the GET 9999->9998 bug and the missing contract test as known
issues / TODO, not checkboxes.
- Drop volatile count snapshots ("36 guards") in favor of the rule.
* fix(l10n): localize error in performUspMutation shared helper
The shared dashboard-card mutation helper showed failures with a raw
`'Error: $e'`, bypassing the central `localizeServiceError` mapper. This
slipped past PR #953 because the raw string lived inside the helper, not
at the call sites the audit grepped for.
Route the caught error through `localizeServiceError` like the feature
views do. This brings all 8 cards that use `performUspMutation`
(internet_settings renew lease, local_network reservations, admin time,
port_forwarding, wifi ×2, devices) into the localized error pipeline in
one change.
Verified: flutter analyze clean; internet_settings suite (127 tests)
passes; no test asserted the old string.
* docs(error-handling): cover performUspMutation + fix two inaccuracies
- Add §3.3: dashboard cards trigger mutations via the shared
performUspMutation helper, which now localizes failures internally —
framed as a convenience entry point, NOT a third localization strategy.
Note successMessage is shown as-is (caller must pass a loc()'d string).
Add matching PR-checklist line.
- Fix two claims found during codebase verification:
- The _localizeFaultCode <-> _mapProtocolError sync reminder is
one-directional in code; reworded accordingly.
- The save-snackbar example showed only showFailedSnackBar; noted the
ScaffoldMessenger+SnackBar variant some pages use — the API is
secondary, the string must come from localizeServiceError.
* docs(constitution): align Article XIII with post-PR#953 error handling
Update the error-handling articles to match the current codebase and add
the missing UI-layer principle. Keep it principle-level; details point to
the implementation guide.
- §13.2: ServiceError now carries diagnostic code/detail; drop the
deleted OTP/admin subtype examples; note code/detail are diagnostic-only.
- §13.4.2: performFetch stores the typed `error: e`, not `errorMessage: '$e'`.
- §13.4.1: use an existing subtype (InvalidInputError) in the example.
- §3.3.5: error classes extend the sealed ServiceError (no AuthError).
- §13.1: UI layer localizes via the central localizeServiceError mapper.
- Add §13.6 UI Layer Error Display — states the principle and links to
doc/error-handling-localization/error-handling-implementation-guide.md.
- Bump Last Amended to 2026-06-29 (version unchanged).
* fix(l10n): localize success messages in card mutations
The 7 hardcoded English successMessage strings passed to performUspMutation
(and showRecoveryDialog) were the success-side counterpart to the error
leak already fixed — all on dashboard cards / shell, missed by PR #953.
- Reuse existing parametrized key for DHCP renew: leaseRenewed('DHCP').
- Add 6 new keys (reservationAdded, reservationDeleted, reconnectedToRouter,
timeSettingsSaved, ruleAdded, channelUpdated), translated across all 26
locales. Translations follow each locale's existing style anchors
(reservationReleased, wifiSettingsSaved, timezoneUpdated…) and noun usage
(e.g. zh 信道 / zh_TW 通道 / ja チャネル for "channel"; per-locale "router").
- Update the 7 call sites to loc(context).xxx.
Verified: 26/26 locales carry each key, all arb files valid JSON,
flutter analyze clean, touched-feature tests pass (498).
* docs(error-handling): translate the three docs to English
The committed/shared docs must be English. Translate the README, the
implementation guide, and the round-trip reference from Traditional
Chinese to English in place.
Pure translation — code blocks, identifiers, file paths, fault codes,
ASCII diagrams, links, and section structure are preserved byte-for-byte;
only prose was translated. Verified: 0 CJK characters remain, code fences
balanced, sibling links resolve.
---------
Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* fix(dashboard): resolve card clipping and add mascot outside dismiss
- Remove ClipRect from dashboard cards to prevent shadow/border truncation
- Add dismissible barrier to all mascot interactive dialogs
- Bump ui_kit_library to v2.26.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(components): add optional secondary action to ServiceErrorView
Add `secondaryLabel` + `onSecondary` (both optional, null by default) so a
page that cannot load can offer an escape hatch (e.g. "Log out") below the
retry button. Behavior is unchanged when not provided. Covered by two new
widget tests.
* refactor(l10n): migrate AsyncValue error pages to shared ServiceErrorView
Replace the per-page private `_buildError` / inline error widgets on the
AsyncValue (AsyncNotifier) pages with the shared `ServiceErrorView`,
narrowing `Object error` via `error is ServiceError ? error : null`.
This unifies fetch-failure display across both page architectures
(state.error pages already used ServiceErrorView).
- system_log, instant_privacy, admin: straight swap (retry = ref.invalidate)
- dashboard: retry = notifier.refreshAll(); keeps its "Log out" escape hatch
via the new ServiceErrorView secondary action
- topology: previously an inline error with a hardcoded `unableToLoadTopology`
title and no localized detail — now gets the localized detail for free.
Removed the now-orphaned `unableToLoadTopology` key from all 26 locales.
Verified: flutter analyze clean on changed files; affected non-golden tests
pass; all 26 .arb files valid JSON.
* refactor(l10n): migrate diagnostics error pages to ServiceErrorView; localize apps error
Continue the AsyncValue error-display migration (rounds 2-3 of the 9 targets):
- speed_test_view, diagnostic_manual_tools_view, usp_speed_test_card:
replace private error widgets (which showed `error.toString()` or a
hardcoded key) with the shared `ServiceErrorView`, narrowing
`Object error` via `error is ServiceError ? error : null`. The diagnostics
providers throw ServiceError/TimeoutError, so failures now localize.
- Removed the now-orphaned keys: unableToLoadSpeedTest,
unableToLoadDiagnostics, errorLoadingSpeedTest (all 26 locales).
apps page is intentionally NOT migrated: it fetches lighttpd static JSON
(not USP/TR-181) and throws plain `Exception`, not `ServiceError`, so
`ServiceErrorView` would only show a generic title. It keeps its own error
widget but no longer surfaces the raw exception — shows the localized
`unableToLoadApps` instead. Also aligned `unableToLoadApps` to the plural
"Apps" brand word for locales that use that spelling (en/da/de/es_ar/nl/pt/it/zh_TW).
Verified: flutter analyze clean on changed files; diagnostics + apps tests
pass (234); all 26 .arb valid JSON; no dangling key references.
* docs(error-handling): unify AsyncValue pages on ServiceErrorView in guide + constitution
* fix(l10n): address PR review — error titles, compact card, missed call-sites
Round-1 review fixes for the ServiceErrorView migration:
- ServiceErrorView: add optional `title` (defaults to neutral errorUnexpected)
so non-settings pages no longer show "Failed to load settings"; add assert
for paired secondary action; unwrap onSecondary via local promotion. (C-1/W-1/W-3)
- Restore the 4 error-title ARB keys (topology/speedTest/diagnostics) to their
original positions across 26 locales; every ServiceErrorView caller now
passes a context-appropriate title. (C-1)
- speed_test_card: revert to a compact card-sized error widget instead of the
full-page ServiceErrorView (avoids DashboardCardTemplate overflow); localizes
via localizeServiceError; drop now-orphaned errorLoadingSpeedTest. (C-2)
- device_list: raw '$e' → ServiceErrorView (unableToGatherDeviceInfo). (C-3)
- health_status_view: hardcoded English → new localized unableToLoadHealthData
(26 locales), kept as the existing inline widget (not full-page). (C-4)
- l10n polish: fix unableToLoadApps capitalization to each locale's in-sentence
convention (de stays capitalized); align pt/ja/th/fr terminology.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* docs(error-handling): sync implementation guide with review fixes (title param, compact widgets)
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* feat(wifi): add channel dropdown to edit dialog (#1023) (#1027)
* feat(wifi): add channel dropdown to edit dialog (#1023)
Replace manual channel number entry (AppTextField) with a dropdown
(AppDropdown) in the Dashboard WiFi Status edit-channel dialog.
- Enrich WifiRadioUIModel with possibleChannels (populated at dashboard
fetch by UspWifiDataService, so the dialog renders synchronously with
no per-dialog fetch/loading/error state).
- Dialog offers Auto (recommended) + the band's possible channels; Auto
switch and dropdown stay consistent; DFS channels annotated.
- Add 4 ARB keys across all 26 locales.
- Unit tests for model, data-service enrichment, and dialog behaviour.
* fix(wifi): lock channel dropdown in Auto mode + always show current channel (#1023)
* fix(wifi): normalize band for DFS + filter invalid channels from PossibleChannels (#1023)
* refactor(wifi): drop redundant IgnorePointer, bump UI-kit v2.26.0->v2.26.1 (#1023)
UI-kit v2.26.1 gates the AppDropdown tap gesture when onChanged is null
(app_dropdown.dart:138,183), so the consumer-side IgnorePointer workaround
added for upstream privacyGUI-UI-kit#2 is now redundant. Remove it and rely
on onChanged==null to disable the control. Fix#1 interaction tests converted
from widget-tree (IgnorePointer.ignoring) to behavior (onChanged null/menu
does not open) assertions; all 16 tests pass.
* fix(dashboard): unify card rows + navigation fixes (#1017)
* fix(dashboard): unify card rows + navigation fixes (#1014, #1012, #1009, #1002)
- Add ToggleRow, NetworkRow, ProtocolBadge components to row_blocks.dart
- Refactor Port Forwarding card to use ToggleRow + ProtocolBadge
- Refactor DHCP Reservations card to use ToggleRow + DeviceRow
- Refactor WiFi Networks card to use NetworkRow component
- Add View Details navigation: Topology card → topology, Device Info → node detail
- Add Statistics page tab parameter for System Status/Traffic Analysis cards
- Remove "off" option from polling interval cards (#1012)
- Remove "admin" username from password card (#1009)
- Fix uptime not updating by including uptimeSeconds in SystemSnapshot
- Fix Network Diagnostics back button returning to dashboard (#1002)
- Move diagnostics route as child of menu route
- Use context.pop() instead of goNamed for proper back navigation
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: address PR review feedback
- Guard instancePath null in DHCP reservation toggle (W-1)
- Add canPop() guard before pop() in diagnostics view (W-3)
- Guard empty deviceId in device info card footer (W-4)
- Replace native widgets with UI Kit components in row_blocks (W-6)
- _GuestBadge → AppBadge
- _ShareButton → AppIconButton
- ProtocolBadge → AppBadge
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(dashboard): consistent device counts excluding mesh nodes (#1020, #1022, #1024)
Issues fixed:
- #1020: Dashboard "Devices" stat now uses clientDevices (excludes routers)
- #1022: Device Analytics excludes mesh nodes from counts and activity heatmap
- #1024: Network Topology signal indicator now uses 1:1 RSSI→LinkQuality mapping
consistent with UspSignalStrengthIndicator (getWifiSignalLevel SSoT)
Key changes:
- Use clientDevices instead of deviceModels across dashboard, mascot triggers,
PDF export, and feature dropdowns (port forwarding, DHCP, IPv6 port service)
- Add serial number scoping to analytics persistence to prevent data mixing
- Filter router MACs from persisted history on load (legacy data cleanup)
- Fix _rssiToLinkQuality() mapping: good→good, fair→fair, poor→unknown
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(topology): add defensive test for RSSI→LinkQuality SSoT consistency (#1024)
Ensures UspTopologyBuilder's LinkQuality mapping stays in sync with
getWifiSignalLevel() from wifi.dart. Tests all RSSI boundary values
to catch future mapping drift that caused #1024.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(dashboard): child node client signal + trend Y-axis (#1043, #1044)
#1044: Clients connected to child mesh nodes now show signal strength.
- Add `clientSignalMap` to MeshTopologyInfo (MAC → RSSI from DataElements)
- MeshTopologyBuilder extracts STA.SignalStrength (RCPI→RSSI conversion)
- Use as fallback in _toDeviceUIModel when WifiClients has no data
#1043: Trend chart Y-axis no longer shows duplicate numbers.
- Add explicit yAxis with calculated max and interval
- Ensures clean labels when device count is small (e.g., 0, 1, 2)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(analytics): address review feedback for device analytics (#1053)
- Guard _persistState() against race condition: don't persist before
_historyLoaded is set (avoids writing to legacy key when _serialNumber
is still null)
- Reset instance state (_historyLoaded, _serialNumber) in build() to
handle provider invalidation correctly
- Use isMeshNode getter instead of raw deviceRole string comparison in
_getRouterMacs() for SSoT consistency
- Use isClientDevice getter instead of raw deviceRole string comparison
in usp_pdf_service.dart for SSoT consistency
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: dart format usp_device_analytics_notifier.dart
* fix(analytics): set _historyLoaded=true on load failure to unblock persistence (#1053)
W-NEW-1: _loadPersistedHistory() only set _historyLoaded=true inside the try
block. Any exception (SharedPreferences cold-start race, corrupted JSON, etc.)
left it false, and the PR's new '_persistState() { if (!_historyLoaded) return; }'
guard then silently dropped every subsequent write for the provider's lifetime.
Set the flag in the catch block so a one-off load failure no longer permanently
gates persistence.
Refs #1053
* feat(auth): replace password storage with session token persistence
Security improvement: password is no longer stored locally. Instead,
session token is persisted in sessionStorage (cleared on browser close)
and used for session restoration via refreshToken(token?) API.
Key changes:
- Add UspTokenStorage with Web (sessionStorage) and stub implementations
- UspAuthCoordinator.restoreSession() uses token-only strategy
- Add reloginWithNewPassword() for admin password change flow
- Add isRecovering flag to suppress force logout during recovery
- Remove localPassword from AuthState
- Update WASM client to usp-client v0.12.0 with refreshToken(token?)
Test coverage: 47 tests for UspAuthCoordinator including:
- reloginWithNewPassword flow and error handling
- Null UspClient guards for all public methods
- Token persistence and restoration scenarios
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(auth): address review feedback for session token persistence
- dashboard_orchestrator: use isRecovering: true to prevent double
navigation (restoreSession's onForceLogout + NotAuthenticatedError)
- usp_token_storage_web: add logging for storage failures to aid
debugging in private browsing or quota-exceeded scenarios
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: format dashboard_orchestrator.dart
* fix(auth): handle relogin failure after password change (#1013)
Address Hank's review feedback:
- #2: Separate error handling for updatePassword and reloginWithNewPassword.
If password change succeeds but relogin fails, trigger logout instead of
reporting "update failed" — the password IS changed, user just needs to
re-enter it.
- #3: Add comment in auth_provider.init() explaining the init order
dependency with onForceLogout callback (currently safe but relies on
sseManagerProvider not being watched yet).
- #4: Add test for relogin failure scenario to verify logout is triggered.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: bump version to 2.6.0
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(wifi): unify guest WiFi detection on canonical alias rule
Guest WiFi detection was implemented three different ways across the app,
which disagreed with each other and caused the guest card to be
misclassified as main (wrong title, hidden password/security fields).
- Add shared helper `isGuestSsid` in _shared/utils/wifi_guest_detection.dart
as the single source of truth: an SSID is guest when its TR-181 Alias
ends with `-guest`.
- Converge all three call sites on the helper:
- usp_wifi_settings_service: replace inline alias check
- usp_wifi_data_service: drop per-radio instance-ordering + dead
_ssidInstanceIndex helper
- pnp_service: drop radio-occupancy heuristic
- wifi_network_card: allow guest networks to edit security mode too,
consistent with the Quick Setup card.
- Tests: give shared WiFi test data proper `-guest` aliases; add guest
detection assertions to data service and pnp service tests.
* fix(wifi): warn when no SSID matches the -guest alias rule
Address PR review: alias-based guest detection fails silently when
firmware omits or doesn't follow the `-guest` alias convention (all
SSIDs fall back to main). Emit a warning log — including the observed
aliases — when multiple SSIDs exist but none match, so the failure is
diagnosable on-device. Detection strategy is unchanged.
* fix(wifi): write both SSID.Enable and AccessPoint.Enable for network toggle (#971, #972)
Problem:
- WiFi Settings page only wrote SSID.Enable when toggling networks
- Dashboard WiFi Status card wrote Radio.Enable (different layer)
- This caused inconsistency between Dashboard and Settings (#971)
- SSID.Enable alone did not stop AP broadcasting (#972)
Solution:
- Write both SSID.Enable and AccessPoint.Enable together for all
network enable/disable operations
- Remove radio-level toggle from Dashboard WiFi Status card (now
read-only status display)
- Dashboard WiFi Networks card uses toggleSsidsByName (per-SSID-name)
- WiFi Settings uses saveQuickSetup/saveAdvanced (per-network)
- PnP Guest WiFi uses same dual-layer write pattern
Changes:
- codegen: Add writable flag to AccessPoint.Enable in YAML
- WifiAccessPointUIModel: Add ssidInstancePath, accessPointInstancePath
- usp_wifi_settings_service: All save/toggle methods write both layers
- usp_wifi_status_card: Remove AP row toggle, keep as status display
- pnp_service: Add _throwIfNotSuccess checks, write AP.Enable for guest
- Remove dead code: toggleNetwork (replaced by toggleSsidsByName)
Tested: Firmware correctly sets Status=Down/Disabled when both layers
are written, and WiFi scanner confirms SSID disappears.
* fix(wifi): ensure L1 cache invalidation on partial failure
- Move wifiDataProvider read inside withLock() in toggleSsidsByName to
avoid TOCTOU race with concurrent mutations
- Wrap ref.refresh/invalidate in finally blocks to ensure L1 cache is
always refreshed even when mutations partially fail, keeping UI in
sync with firmware state
- Apply same fix to updateRadioChannel for consistency
* fix(web): move validation to unfocus to prevent TextField focus loss (#1059) (#1099)
On Flutter Web, calling setState during onChanged causes widget tree
changes that break TextField's TextInputConnection. This manifests as:
- Auto-unfocus when validation error appears/disappears
- Cannot delete all text (one character remains)
Fix: trigger validation on unfocus instead of onChange for affected pages:
- Instant Privacy (Add MAC dialog)
- DHCP Reservation (Edit dialog)
- Local Network (all IPv4 fields)
- DMZ (Destination IP)
For AppIpv4TextField, use onFocusChanged callback which only fires
(null, false) when focus leaves the entire field, not between segments.
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(dhcp): reject duplicate MAC/IP in reservation dialog (#1070) (#1078)
* fix(dhcp): reject duplicate MAC/IP in reservation dialog (#1070)
The DHCP reservation Add/Edit dialog validated only MAC format, IPv4
format, and the reserved-IP rule; it never compared the entered MAC/IP
against existing reservations, so a duplicate was accepted and sent as a
USP ADD request (backend accepts it).
Add duplicate detection to the dialog's _validate(): the caller now
passes the current reservation list via existingReservations, and the
entered MAC (case-insensitive) / IP is rejected if it collides with any
other reservation. When editing, the reservation being edited is
excluded so it can keep its own address. Wire both detail-card callers
(add + edit) to supply the list, and add duplicateMacAddress /
duplicateIpAddress l10n keys.
* fix(dhcp): exclude self by stable instancePath in reservation duplicate check
The DHCP reservation edit dialog excluded the edited reservation from the
duplicate MAC/IP check via Equatable value-equality (r != widget.reservation).
Because DhcpReservationUIModel.props includes the non-key 'enable' field, an
SSE-driven re-fetch that toggles the edited entry's enable flag while the
dialog is open makes value-equality fail to match self, so the user's own
unchanged MAC/IP is falsely flagged as a duplicate and Save is disabled.
Exclude self by stable instancePath identity instead; fall back to
identical() for not-yet-saved local reservations (null instancePath).
Adds a regression test reproducing the SSE enable-drift scenario (fails on
the old value-equality filter, passes with the identity-based fix).
* fix(dhcp): validate reservations added from Dashboard card (#1067) (#1077)
* fix(dhcp): validate reservations added from Dashboard card (#1067)
The Dashboard DHCP card invoked the unvalidated DhcpReservationDialog,
which only silently no-op'd on empty MAC/IP and applied no format
validation. Malformed/empty reservations were therefore accepted and
persisted via AddInstance.
Point the Dashboard card at the existing DhcpReservationEditDialog
(already used by the DHCP detail page), which enforces MAC-address and
IPv4 format rules plus reserved-IP checks and disables the Add button
until the form is valid. Remove the now-orphaned DhcpReservationDialog.
Refs #1067
* chore: trigger CI (empty commit)
Re-trigger pull_request CI for #1077 (base changed to dev-2.6.0 did not fire sync).
No file changes.
* refactor(dhcp): centralize reservation device-option data source (#1109)
* fix(hooks): use absolute paths in PreToolUse hooks
The PreToolUse Bash hooks used a relative path (.claude/hooks/pr_gate.py)
and relied on the shell's working directory. When the working directory
changed (e.g. to a subdirectory), the hook could no longer locate the
script and blocked all Bash commands.
Use $CLAUDE_PROJECT_DIR to resolve the pr_gate.py path, and cd into the
project root in the dart-format hook so staged file paths resolve
correctly regardless of the current working directory.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(dhcp): move device option data source into reservations notifier
Extract the client-device data lookup out of the reservation cards and
into UspDhcpReservationsNotifier.deviceOptions(), which returns pure data
(ReservationDeviceOption records) instead of UI Kit types. The cards now
map that data to AppAutoCompleteOption at the call site, keeping the
cross-provider read in the notifier and the UI projection in the view.
- Wire device autocomplete options into the local-network add dialog.
- Add duplicateMacAddress / duplicateIpAddress strings across all 26
locales.
- Add unit tests for deviceOptions() (mapping, name fallback, mesh-node
exclusion, empty case).
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* feat(internet): support PPTP, L2TP, and Bridge WAN connection types (#1094)
* chore: bump version to 2.5.0 and add test report generation
- Update version from 2.4.0 to 2.5.0 in pubspec.yaml
- Add --report flag to run_tests.sh for markdown test report output
- Add standalone tools/test_report.sh for generating categorized reports
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test(firmware-update): add golden tests for all firmware update states
Add comprehensive golden test coverage for the firmware update page,
covering all 16 visual states across phone and desktop viewports.
Firmware Update View states (12):
- idle_no_file, idle_file_selected
- picking, validating, uploading
- triggering, installing, rebooting, verifying
- done, failed, banks_empty
Recovery Dialog states (4):
- waiting_initial, waiting_unreachable
- wifi_warning, serial_mismatch
Files added:
- mock_firmware_update.dart: Provider overrides for golden tests
- firmware_update_test_data.dart: Test fixtures and state builders
- firmware_update_view_test.dart: Main view golden tests
- firmware_recovery_dialog_test.dart: Recovery dialog golden tests
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(skill): add golden test coverage check to review-pr-readiness
Add Phase 4.5 to review-pr-readiness skill for checking golden test
coverage when View files are changed. The new checks include:
- 4.5.1: Golden test file existence for changed views
- 4.5.2: Golden test freshness when views are modified
- 4.5.3: Deep analysis of view states vs golden test coverage
- 4.5.4: Mock and fixture file existence
- 4.5.5: Optional golden test execution verification
This ensures PR reviewers are prompted to add/update golden tests
when visual changes are made, maintaining screenshot test coverage.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(pnp): unify troubleshoot flow UI patterns and behavior (#907)
Review pass over the PnP no-internet troubleshoot flow (no-internet hub,
unplug-modem, modem-lights-off, waiting-modem, isp selection, pppoe,
static IP). Establishes consistent layout patterns and fixes save-flow
behavior gaps so the troubleshoot pages feel like the rest of the app.
Layout — onboarding/wizard pages
- Wrap content in `withSliver` + `Center` + `ConstrainedBox(maxWidth: 480)`
+ `EdgeInsets.all(xl)` so cards stop stretching across desktop viewports.
- Replace sticky `UiKitBottomBarConfig` with inline `AppButton.primary`
at the bottom of the column — onboarding flows are linear, the next
action belongs in content flow, not a sticky save bar.
- Standardize illustration width (160px) and crossAxisAlignment.
Layout — full-screen status overlays (saving / countdown / checking)
- Switch from `withSliver` to plain `UiKitPageView` with
`useMainPadding: false`, then wrap content in `Center`. `withSliver`
collapses children to intrinsic height (so MainAxisAlignment.center has
no room) and `useMainPadding: true` applies grid pageMargin (which
pushes overlays off-center on wide screens).
Save flow
- Extract `PnpIspSavingProgress` shared widget so DHCP, PPPoE, and Static
IP all show the same three-step progress UI.
- Add proper localization keys for the save-step labels (previously
borrowed unrelated strings like "Save" and the ISP-type page title).
- Surface save failures via SnackBar — `errorMessage` was being written
to state but no view consumed it.
- Disable the form Save button until required fields are filled.
- Show button loading state on the no-internet "Try again" button.
Save responsibility boundary
- Convert `PnpIspSettingsView` to `ConsumerStatefulWidget`. DHCP save is
driven by a local `_dhcpSaving` flag and a one-shot post-await phase
read; the page no longer keeps a `ref.listen` on the global PnP phase.
This prevents the parent index page from reacting to save outcomes
triggered by its child form pages (PPPoE / Static IP), which would
otherwise cause double-fired SnackBars and unnecessary rebuilds.
* feat(firmware-update): add OTA firmware update support (#917)
* feat(firmware-update): add OTA firmware update support
Add cloud API integration to check for available firmware updates and
trigger OTA download directly to router. OTA and local manual upload
share the same flow from FirmwareImage.Download() onwards (flash →
reboot → verify).
- Add FirmwareOtaCheckService for cloud API integration
- Add FirmwareOtaInfo model for API response parsing
- Add checkingOta phase and OTA state fields
- Add OTA check UI card with "Check for Updates" button
- Add triggerOtaDownload() for remote firmware URL
* test(firmware-update): add tests for OTA update functionality
- Add FirmwareOtaInfo model tests (JSON parsing, toQueryParams)
- Add FirmwareOtaCheckService tests (HTTP calls, error handling)
- Add triggerOtaDownload service tests
- Add checkForOtaUpdate and triggerOtaInstall notifier tests
* refactor(firmware-update): move OTA param building to notifier layer
Address code review feedback:
- Fix: Remove PII (MAC/IP) from log by only logging URI path
- Fix: Move OTA check param building logic from View to Notifier
- buildOtaCheckParams(), _formatMacAddress(), _parseHardwareVersion()
- View now only calls notifier methods, no business logic
* fix(firmware-update):
1. Make releaseDate nullable instead of using DateTime.now() fallback
- Prevents non-deterministic behavior in Equatable comparison
- null semantics correctly represent "not provided"
2. Add clearOtaInfo flag to FirmwareUpdateState.copyWith()
- Allows resetting otaInfo to null when needed
- Pattern: copyWith(clearOtaInfo: true)
* chore: upgrade Flutter 3.38.5 → 3.44.0 and dependencies (#914)
* chore: upgrade Flutter 3.38.5 → 3.44.0
- Update .fvmrc to pin Flutter 3.44.0 (Dart 3.12.0)
- Update vendored CanvasKit for offline web deployment
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore(deps): remove unused packages and upgrade for SPM support
Remove unused packages:
- connectivity_plus (not used in codebase)
- network_info_plus (not used in codebase)
- flutter_local_notifications (not used in codebase)
Upgrade packages for Swift Package Manager support:
- flutter_secure_storage: 9.2.2 → 10.3.1
- device_info_plus: 9.1.2 → 11.1.0
- package_info_plus: 4.1.0 → 8.1.0
- share_plus: 7.1.0 → 10.1.0
- printing: 5.13.1 → 5.14.3
SPM warnings reduced from 11 to 4.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore(deps): upgrade go_router 14.2.8 → 17.0.0
No breaking changes affecting current codebase.
All 2630 tests pass.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore(deps): remove unused permission_handler
- Remove lib/util/permission.dart (Permissions mixin never used)
- Remove permission_handler dependency from pubspec.yaml
SPM warnings reduced from 4 to 3.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore(deps): upgrade ui_kit_library v2.20.0 → v2.21.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(mascot): integrate mascot overlay with dashboard (#922)
* feat(mascot): integrate mascot overlay with random speech
- Add mascot integration to USP dashboard shell
- Implement DashboardDialogProvider with FAQ, diagnostics, print report
- Add random speech timer (10-30s interval, auto-hide after 5s)
- Add mascot toggle in GeneralSettingsWidget
- Redesign GeneralSettingsWidget layout (unified row height, AppSwitch)
- Fix ThemeModeTile to use dialog selection pattern
- Fix Theme Studio persistence with keepAlive
- Fix popup dismiss behavior with TapRegion groupId
- Show mascot only after dashboard data is ready
- Upgrade ui_kit_library v2.21.1 → v2.23.1
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(mascot): simplify coordinator and add unit tests
- Rename mascotRandomSpeechProvider → mascotCoordinatorProvider
- Move startup logic from shell to MascotCoordinatorNotifier.build()
- Remove complex ref.listen/Future.microtask from shell
- Delete unused network_health_score.dart
- Add dashboard_dialog_provider_test.dart (13 tests)
- Add mascot_coordinator_notifier_test.dart (4 tests)
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>
* feat: mesh topology enhancement with layout system refactor (#915)
* feat(codegen): update usp-codegen with resolveBy and regenerate .g.dart files
- Update tools/usp-codegen binary to v0.15.2
- Add resolveBy feature for dynamic WAN/LAN interface resolution via Alias
- Fix absolute path handling bug (DHCP paths no longer incorrectly prefixed)
- Add new MeshNode backhaul fields: BackhaulDeviceID, BackhaulMACAddress,
LinkType, MACAddress, LastDataDownlinkRate
- Regenerate all .g.dart files with new codegen
- Update mesh_topology_builder_test.dart for new MeshNode fields
Closes #909
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(topology): mesh topology enhancement phase 2 with diagnostics integration
- Consolidate RSSI thresholds to single source (wifi.dart)
- Merge wifi_performance_helpers.dart into wifi.dart/wifi_ui.dart
- Add DataElements enrichment fields to NodeUIModel for backhaul diagnostics
- Add mesh backhaul check to unified diagnostics service
- Fix signal strength display with text labels and proper units
- Change diagnostic results from GridView to Wrap for flexible height
- Use dialog instead of bottom sheet for diagnostic details (desktop UX)
- Update stale threshold from 5 to 10 minutes
- Update codegen to v0.15.3 with resolveBy fix for updateOrdered
- Fix test mocks for _resolveInstance get calls
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ui): unify speed formatting and enhance backhaul card design
- Consolidate speed formatting to NetworkUtils.formatSpeed/formatSpeedWithUnit
- Single source of truth for kbps → human-readable conversion
- Gbps: 2 decimal places, Mbps: 0 decimals, kbps: no decimals
- Update DetailSpeedCard to use speedKbps parameter (TR-181 standard unit)
- Enhance BackhaulSignalIndicator with visual bar design matching Device Detail
- Fix PHY Rate display to use unified formatting (Mbps → Gbps when applicable)
- Remove duplicate formatSpeed from wifi.dart and node_detail_popup.dart
- Fix rate field comments (was incorrectly documented as bps, actually kbps)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ui): unify upload speed card color to tertiary
Device Detail was using secondary for upload while Node Detail used
tertiary. Unified to tertiary (upload) and primary (download) across
both views for visual consistency.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ui): introduce layout blocks system and unify dashboard cards
- Create reusable layout blocks library (lib/page/_shared/components/layout_blocks/)
- Block: universal base wrapper with consistent background styling
- CardHeader: fixed 36px height for consistent title alignment
- StatusBlock, AlertBanner: status indicators
- InfoGrid, InfoList, ListPreview: data display blocks
- HighlightValue, DualMetric, StatTile: metric blocks
- NetworkRow, DeviceRow, DataRow, StatusRow: row blocks
- ProgressBlock, QuotaBlock, RangeBlock: data visualization blocks
- Redesign all dashboard cards using Block-based layout patterns:
- Hero block + metric tiles + InfoGrid design pattern
- Consistent visual styling across all cards
- All progress bars now use UI Kit AppLoader (linear variant)
- Fix multi-interface device detection in Ethernet ports card
- Now correctly shows wired connections from devices with both WiFi and Ethernet
- Update dashboard presets and widget specs for proper card sizing
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ui): apply Block layout pattern across all feature pages
Consistently apply Block component pattern throughout the application
for unified visual hierarchy and semantic grouping within AppCard
containers. Also update tests to match model changes from prior sessions.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* chore: remove design showcase page and routes
The Block layout pattern has been applied across all feature pages,
so the showcase page is no longer needed for development reference.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(layout-blocks): consolidate design system and remove unused components
- Add BlockConstants for unified alpha, padding, borderRadius values
- Extract SwitchBlock, SettingBlock, NavLinkBlock, FormFieldBlock
- Remove unused: StatusBlock, AlertBanner, IpAddressBlock, ProgressBlock,
QuotaBlock, RangeBlock, HighlightValue, DualMetric, VersionBlock,
ComparisonBlock, ListPreview, NetworkRow, DataRow, StatusRow,
ToggleListItem, SplitRow, SectionDivider, CountBadge
- Apply SwitchBlock to Firewall view (removes _switchRow helper)
- Apply SettingBlock to WiFi Network Card (removes _SettingBlock)
Reduces layout_blocks from 8 files (~850 lines) to 6 files (~400 lines)
while adding reusable patterns for common UI elements.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* docs(constitution): add Article XIV Layout Composition Patterns
Define project-level layout conventions that complement UI Kit:
- Block pattern: visual grouping container (surfaceContainerHighest @ 50%)
- Three usage patterns: Card+Block, Block alone, Card alone
- Shared components: SwitchBlock, SettingBlock, NavLinkBlock, DeviceRow
- Implementation rules and file organization
Renumber UI Kit Library Principle to Article XV.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(dashboard): adjust traffic monitor timer to Off/10s/30s/60s
Change the Traffic Monitor refresh interval options from Off/2s/5s/10s
to Off/10s/30s/60s, with 10s as the new default.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
* refactor(DeviceRow): use AppListTile from UI Kit
Replace custom Row/Container layout with AppListTile to comply with
UI Kit First principle. Preserves icon container styling via leading
parameter.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(architecture): fix constitution compliance and clean up core layer
Article XIII compliance:
- Add error mapping to PnpService (6 methods)
- Create DiagnosticsScopeService to wrap NetworkDiagnosticsExecutor
- Remove usp_error.dart imports from provider layer (speed_test_notifier,
manual_tools_notifier)
SSoT fixes:
- Consolidate MeshBackhaulSeverityBucket into MeshBackhaulSeverity enum
- Remove switch conversion in unified_diagnostics_notifier
Core layer cleanup (remove Flutter Material imports):
- Move device_classifier.dart to lib/page/_shared/utils/
- Move recovery_dialog_helper.dart to lib/page/_shared/helpers/
- Extract DeviceConnectionTypeExt to lib/page/_shared/extensions/
Test updates:
- Move device_classifier_test.dart to test/page/_shared/utils/
- Add DiagnosticsScopeService unit tests (20 test cases)
- Update test imports and enum references
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(review): address PR #915 review comments
- Fix speed row UX: show only available directions instead of '--'
- Extract shared MetricTile to layout_blocks (remove duplication)
- Refactor NetworkBadgeWidget to use AppBadge from UI Kit
(icon support pending #916)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(layout-blocks): rename Block to LayoutBlock
Avoid name collision with go_router.Block
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* feat(usp): update WASM client with UspClientBuilder support
Add builder pattern for creating UspClient with custom configuration:
- authToken(): set Bearer token (skip login flow)
- endpoint(): set custom USP endpoint path
- extraHeader(): add custom HTTP headers
- build(): create UspClient instance
Required for Remote Assistance integration via Guardian API.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(usp): add Remote Assistance POC via Guardian proxy
Add support for Remote Assistance mode that allows remote control of
router via Guardian API using temporary access token.
Changes:
- Add UspClientBuilderJS WASM binding for builder pattern
- Add UspClientWeb.fromJsClient() and UspClient.fromBuilder() factories
- Add RemoteAssistanceProvider for RA state management
- Add RemoteAssistanceConfirmView for token input UI
- Add /remoteAssistance route with ?sessionId query param
- Add URL detection: /?ra_session=xxx redirects to RA confirm page
Usage:
1. Navigate to http://localhost:5000/?ra_session=test-session-123
2. Enter temporary access token on confirm page
3. Click Connect to initialize Guardian-proxied USP client
4. Dashboard loads with USP operations routed through Guardian
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(build): add force=remote build mode for Remote Assistance
- Add ForceCommand.remote enum value
- Add BuildConfig.isRemote() helper
- Redirect to /remoteAssistance when force=remote is set
Usage: flutter run --dart-define force=remote
Or use VSCode "linksys - Web (Remote)" launch config.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(config): add GlobalConfig with ThemeConfig integration
- Rename feature_flags.json to app_config.json with new structure
- Add ThemeConfig to GlobalConfig for CI/CD theme configuration
- Extract ThemeSource enum to separate file to avoid circular import
- ThemeConfigLoader now reads from GlobalConfig.theme if configured
- Add app_config.json.template with full schema documentation
The theme section in app_config.json is optional - when absent,
ThemeConfigLoader falls back to dart-define environment variables.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(usp): add conditional export for UspClientBuilderJS
- Create usp_client_builder.dart as platform-agnostic entry point
- Add usp_client_builder_stub.dart for VM/tests
- Add usp_client_builder_web.dart to re-export from WASM
- Fix test failures caused by unconditional WASM import
The previous direct export of UspClientBuilderJS from usp_client_wasm.dart
caused dart:js_interop to be imported on non-web platforms, breaking tests.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* test: update tests for auth check architecture change
- Remove isAuthenticated getter tests from service tests
- Update notifier tests to use appConnectionStateProvider for auth check
- Remove obsolete unauthenticated service test from internet settings
Auth checks moved from Service layer (isAuthenticated getter) to
Provider layer (appConnectionStateProvider). Services are now stateless
and trust the upper layer handles auth before navigation.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(remote): complete Remote Assistance mode implementation
Remote Assistance mode allows support agents to view router status
through Guardian proxy without direct network access.
Changes:
- Add RemoteAccessProvider with sessionStorage persistence for refresh
- Add GlobalConfig.remote for centralized UI/feature restrictions
- Skip SSE in Remote mode (Guardian proxy limitation)
- Fix router redirect loop after Connect
- Use fixed remote preset layout for Dashboard
- Add topology card to remote preset
- Remove isAuthenticated checks from Services (moved to Provider layer)
- Simplify General Settings in Remote mode (hide Legal, Logout)
- Add RemoteSessionChip for session info display
Known limitations:
- Operate-based diagnostics (Ping/Traceroute) don't work in Remote mode
due to SSE dependency for OperationComplete events
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(remote): improve Remote Assistance UX
- Change query param key from 'sessionId' to 'session'
- Show expiry time instead of countdown in popup (fixed value)
- Add session polling every 30s to sync remaining time with server
- Fix End Session redirect to show session ended view
- Use go() instead of goNamed() to clear navigation history
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(remote): fix End Session navigation race condition
- Capture GoRouter before async gap to avoid context unmount issue
- Delay logout() until after navigation completes via postFrameCallback
- Clean up unused cloud_const.dart constants (30+ unused entries removed)
- Add guardianDomain constant for future domain migration
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(dashboard): unify card templates and enhance AI assistant (#931)
* refactor(dashboard): unify card layout with DashboardCardTemplate
Extract common card structure (header, scrollable content, footer) into
a reusable template supporting three modes:
- Single content: standard cards
- Multi-section: composite cards (DHCP, Port Forwarding)
- Tabbed: cards with tab navigation (System Status, Analytics)
Migrated 18 dashboard cards to use the template, reducing ~225 lines
of duplicated layout code while ensuring consistent visual appearance.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(ai): add modular Section architecture and new AI commands
Router AI Assistant enhancements:
## Modular Section Architecture
- Add 9 domain sections: WanSection, LanSection, WifiSection, DevicesSection,
SystemSection, FirewallSection, EthernetSection, DhcpSection, PortForwardingSection
- Add 2 advanced sections: TopologySection (with tap-to-popup), DiagnosticsSection
- Add 3 chart sections: LineChartSection, BarChartSection, PieChartSection
- Add utilities: SectionHeader, AiInfoRow, AppDivider
- Total: 37 components (16 data sections, 9 legacy cards, 12 basic)
## New AI Commands (15 total)
- getSystemInfo, getConnectedDevices, getWifiSettings, getWanStatus
- getNetworkOverview, getLanInfo, getDhcpInfo, getEthernetPorts
- getFirewallStatus, getPortForwarding, getTimeSettings
- getTrafficStats (with history for charts)
- getSystemMonitor (CPU/Memory history)
- getDeviceAnalytics (device distribution stats)
- getWifiStatus (Tx power, bit rate, channel, bandwidth per radio)
## TopologySection Features
- Tap-to-show-details popup with MAC, IP, signal, speed
- Animation enabled via theme override
- Supports extenders and clients with metadata
## Infrastructure
- RouterChatController with A2UI v0.9 protocol support
- UspCommandProvider reads from L1 dashboard providers
- ComponentCatalog with sync tests for registry/prompt alignment
- System prompt caching support (~80-90% token savings)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix: update ComponentBuilder import for ui_kit 2.25.0 compatibility
Add generative_ui import for ComponentBuilder type which is now
exported from gen_ui_contracts instead of ui_kit_library directly.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(mascot): hide dismiss button for random idle messages
Random speech bubbles now use showDismissButton: false so users
can only dismiss them by tapping the bubble or waiting for autoHide.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(mascot): add dynamic message provider with L2 architecture
Introduce MascotMessageProvider for generating context-aware random messages:
## Message Categories
- **Guidance** (20%): Feature discovery tips (WiFi settings, diagnostics, etc.)
- **Status** (50%): Dynamic system state (CPU, memory, devices, mesh, WAN)
- **Tips** (30%): Network security and knowledge sharing
## Architecture
- `mascot_message_templates.dart`: Template definitions with conditions
- `mascot_message_provider.dart`: L2 Provider reading from L1 data providers
- Templates use `MascotMessageContext` for dynamic text generation
- Conditional templates only show when their condition is met
## Data Sources (L1 Providers)
- systemInfoDataProvider: CPU%, Memory%, uptime
- devicesDataProvider: online/total count, mesh nodes
- wifiDataProvider: radio enabled count
- wanDataProvider: connection status
## Extensibility
- Add new templates: just add to the corresponding List
- Add new category: add enum, create List, update weights
- Add new context field: extend MascotMessageContext, read in _buildContext()
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(cards): integrate layout_blocks primitives with DashboardCardTemplate
Merge both design systems:
- Keep DashboardCardTemplate as outer wrapper (header/footer/scroll)
- Use layout_blocks primitives inside content (LayoutBlock, MetricTile, InfoGrid, etc.)
Files updated: 12 dashboard cards across admin, dashboard, devices, firewall,
internet_settings, local_network, port_forwarding, topology modules.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(devices-card): remove nested Expanded in scrollable content
DashboardCardTemplate already wraps content in Expanded + ScrollView,
so the device list shouldn't add another Expanded + SingleChildScrollView.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* style: format usp_ethernet_ports_card.dart
* fix: address code review findings
Critical fixes:
- Fix type safety: buildRouterContext now accepts WidgetRef instead of dynamic
- Fix password exposure: use full masking ('********') instead of partial
Major fixes:
- Remove debug prints from TopologySection.build()
- Add Semantics wrapper to DashboardCardTemplate footer link for accessibility
- Add documentation for intentional Navigator.push usage in mascot animation
Tech debt:
- Remove unused onViewAll parameter from UspConnectedDevicesCard
- Remove unused onViewAll parameter from UspWifiNetworksCard
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ai): use ProviderReader type for buildRouterContext
Changed buildRouterContext to accept a ProviderReader function type
instead of WidgetRef, allowing both WidgetRef.read and ProviderContainer.read
to be passed. This fixes test compatibility while maintaining type safety.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(ai): update buildRouterContext call to use ref.read
Pass ref.read function to match ProviderReader type signature.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ai): use UI Kit components in router_assistant_view dialogs
- Replace Text with AppText in confirmation and config dialogs
- Replace TextButton/FilledButton with AppButton.text/AppButton.primary
- Add unit tests for routerCommandProviderProvider
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ai): replace debugPrint with logger.d for release exclusion
Use project logger instead of debugPrint to ensure AI debug logs
are excluded from release builds and properly masked for sensitive data.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* test(remote): add unit tests for Remote Assistance feature
- Add RemoteAssistanceService tests (19 tests)
- Add RemoteAccessNotifier tests (20 tests)
- Add RemoteAssistanceNotifier tests (18 tests)
- Add RemoteClientNotifier tests (22 tests)
Also:
- Add poll failure tracking with hasPollError state
- Add 15s timeout to session validation API call
- Extract magic numbers to named constants
- Add lint ignore reason comment
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* fix(cloud): remove /cloud prefix from Guardian RA endpoints
Guardian API endpoints don't use the /cloud prefix.
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* refactor(ui): use AppSurface and AppText in RemoteSessionChip
- Replace Container with AppSurface for theme-aware styling
- Replace raw Text with AppText.labelMedium/labelSmall
- Use semantic colors (urgency indicated by text/icon color, not background)
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat: golden test framework consolidation and HTML report tooling (#925)
* docs: add golden test verification report design spec
Design spec for automated HTML report generation after golden test
verification runs. Covers report structure, failure image comparison,
coverage scanning, and self-contained HTML output.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* docs: add golden test verification report implementation plan
Six-task plan covering: test result parser enhancement, coverage
scanning, HTML template rewrite, verify script creation, snapshot
script simplification, and end-to-end smoke testing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: extract failure image paths in test result parser
Add extractFailureImages() to parse golden test failure messages and
extract expected/actual/diff image paths into a failureImages field.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add coverage scanning and --embed flag to combine_results
Scans lib/page/*/views/usp_*_view.dart against test/usp_test/page/*/
to calculate golden test coverage. The --embed flag converts failure
images to base64 data URIs for self-contained CI artifacts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: rewrite HTML report template with modern UI
Self-contained HTML with embedded CSS/JS. Includes donut chart,
coverage panel, filter bar, collapsible feature groups, and
three-way image comparison for failures. Supports dark mode.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add run_golden_verify.sh for verification-mode testing
Runs golden tests without --update-goldens, parses results, and
generates an HTML report with pass/fail stats, failure image
comparison, and coverage analysis. Supports --embed for CI artifacts.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor: remove report generation from snapshot update script
Report generation is now handled exclusively by run_golden_verify.sh.
The update script focuses solely on regenerating golden baseline files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: add FVM detection, remove --tags=loc, handle empty test results
- Add FVM detection to run_golden_verify.sh (consistent with run_tests.sh)
- Remove --tags=loc since golden tests don't use tag annotations
- Use test/usp_test/ directory path for full-mode test targeting
- Fix test_result_parser.dart crash on empty results (use fold instead
of reduce, handle null suites/result gracefully)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: update extractInfo regex for new golden test name format
The new golden framework produces test names like:
"viewName - state - device - locale (variant: macOS)"
instead of the legacy format. Add new regex pattern matching first,
fall back to legacy format for backwards compatibility.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: resolve smoke test issues with failure image extraction
- Add || true to test_result_parser calls (parser exits 1 on failures,
which would halt the script under set -e)
- Move extractFailureImages to onDone phase (test metadata like tsName
isn't populated during message events)
- Add Strategy 2: infer failure image paths from test metadata when
Alchemist doesn't include paths in error messages
- Fix testCaseFilePath leading slash for relative path resolution
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: prepend ../ to failure image paths for correct report resolution
Report lives in snapshots/ subdirectory, so failure image paths
(relative to project root) need ../ prefix to resolve correctly
when viewing the HTML report in a browser.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: support --dart-define locale/screen override in golden runner
Allow command-line control of which locales and screen sizes to run
via --dart-define=locales and --dart-define=screens, without requiring
changes to individual test files.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: remove unused verify_golden_coverage.sh
Coverage scanning is already handled by scanCoverage() in
combine_results.dart. This script was never referenced by any
CI workflow.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: consolidate golden tests to test/golden_test/ with gallery report
- Move golden tests from test/usp_test/ to test/golden_test/
- Add generate_gallery_report.dart for visual golden gallery
- Simplify run_generate_loc_snapshots.sh (fvm detection, remove snapshots/ copy)
- Update run_golden_verify.sh to output report in test/golden_test/
- Fix combine_results.dart json filter and relative path logic
- Fix test_result_parser.dart to write output alongside input
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add coverage ignore list for views without golden tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add lightbox, comparison view, and thumbnail sizing to gallery report
- Lightbox with keyboard navigation (←/→/Esc) and section position indicator
- Compare mode: same state side-by-side across locales for quick l10n review
- Thumbnail size toggle (S/M/L) for adjustable grid density
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: improve golden framework stability and report UX
- Replace naive 5×pump loop with pumpAndSettle + timeout fallback
- Add precacheImages config for views with async asset images
- Add search, lightbox, overlay slider to verify report
- Add search box and Components device grouping to gallery report
- Add golden test report usage guide
- Update spec to document new settle/precache mechanisms
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace hardcoded find.text with locale-independent finders
Interaction steps using find.text('English string') fail in non-English
locales. Replace with find.byType(Tab).at(index), find.byType(AppButton),
find.byIcon, etc. Also fix _resolveDevices to not override custom device
configs, and add finder rules to spec.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: add overflow error detection and reporting to golden tests
Collect RenderFlex overflow warnings during golden test execution and
write them to goldens/overflow_warnings.json. Both gallery and verify
reports now display overflow badges and support overflow-only filtering.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: enhance report UI with filters, zoom, back-to-top, and fix page heights
- Add select all/none toggle for feature, locale, and device filters
- Use CSS grid layout for filter groups to prevent overlap
- Add fixed back-to-top button (visible after 400px scroll)
- Add lightbox zoom with scroll-wheel zoom and drag-to-pan
- Increase golden test heights for admin, device_list, dhcp, dashboard,
unified_diagnostics, menu, and statistics pages
- Update clear_goldens.sh to only delete PNGs and clear overflow artifacts
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore: consolidate golden tests under test/golden_test/ and fix doc naming
- Move firmware_update tests from test/usp_test/ to test/golden_test/page/
- Update all path references in golden_test_specification.md
- Rename golden-test-report-guide.md to golden_test_report_guide.md
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat: replace checkbox filters with chip-style toggles in report UI
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* style: format golden_runner.dart
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: propagate test failure exit code in run_golden_verify.sh
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: remove set -e to ensure verify report is always generated
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix: replace hardcoded find.text with locale-independent finders in wifi_settings
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* Fix format
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(cloud): replace LinksysCloudRepository with GuardianApiClient
- Create GuardianApiClient as single point for Guardian API calls
- Remove LinksysCloudRepository (only Remote Assistance was using it)
- Delete 7 unused service files (asset, auth, device, event, ping, smart_device, user)
- Delete 10 unused model files (cloud_account, cloud_phone, etc.)
- Clean up ~30 unused constants from cloud_const.dart
- Update RemoteAssistanceService to use GuardianApiClient
- Update tests to mock GuardianApiClient
Files removed: 18
Lines removed: ~2500
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
* feat(connection): auto-detect unexpected disconnection and enter wait-for-recovery (#932)
* feat(connection): auto-detect unexpected disconnection and enter wait-for-recovery
When SSE reconnection fails 2 consecutive times (indicating the device
has likely moved out of router range), automatically enter the
wait-for-recovery flow instead of waiting for all 5 retries to exhaust.
Recovery probe takes over with lightweight health checks every 10s,
and shows a modal dialog informing the user of the disconnection.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(connection): skip redundant enterWaiting when shell shows natural recovery dialog
The auto-detect path already transitions to waitingForRecovery via
_onSseReconnectFailed; the shell listener only needs to display the
dialog without re-entering the state.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* chore(skill): use fvm dart format in review-pr-readiness skill
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(connection): address code review findings from PR #932
- Add reentrancy guard in _scheduleReconnect after onReconnectFailed
callback to prevent timer/state mutation after intentional disconnect
- Add state check + try/finally in _showNaturalRecoveryDialog to prevent
stuck dialog on race condition or exception
- Clear _recoveryContext on recovered, serialMismatch, and exitToLogout
so the public getter accurately reflects "null when not in recovery"
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(connection): update recovery_dialog_helper import path after merge
The file was moved from lib/core/connection/helpers/ to
lib/page/_shared/helpers/ in dev-2.5.0; update the import in
usp_dashboard_shell.dart accordingly.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(connection): address PR review suggestions
- Move state check before setting _recoveryDialogShowing flag for
more intuitive flow in…
Summary
All USP requests (Get/Set/Add/Operate) across feature pages — except firmware_update (deferred) and SSE subscriptions (separate path) — now flow through a unified error handling pipeline.
When an error occurs, all error sources are aggregated and converted to the sealed
ServiceErrorclass at the Service layer. TheseServiceErrorobjects are then passed to the View layer for display. Most importantly, allServiceErrorsubtypes now have corresponding localizations (exceptUnexpectedError, which displays the rawdetailstring as a final fallback — this may be English firmware technical messages).Error display paths
state.errorServiceErrorView(empty state widget)ref.listen+localizeServiceError→ snackbarKey changes
ServiceError.codeandServiceError.detailfor diagnostic contextTimeoutErrorsubtype for timeout handlinglocalizeServiceError()central mapper with exhaustive switch on sealed classServiceErrorViewshared widget replacing 18 per-feature_buildError()methodsString? errorMessage→ServiceError? errorServiceErrorobjects (not stringify with'$e')Coverage
StorageError/SerialNumberMismatchErrorNotes
UnexpectedError.detailis the final fallback — it surfaces raw technical strings when no typed error matchesUspPartialFailureError/UspCompleteFailureError) show the first entry's concrete error message, not "N items failed"Test plan
flutter analyzepasses./run_tests.shpasses (2854 tests)ServiceErrorViewPart of #919
🤖 Generated with Claude Code