Skip to content

feat(l10n): centralize error message localization for USP features - #953

Merged
HankYuLinksys merged 8 commits into
dev-2.5.0from
hank/usp-error-localization
Jun 18, 2026
Merged

feat(l10n): centralize error message localization for USP features#953
HankYuLinksys merged 8 commits into
dev-2.5.0from
hank/usp-error-localization

Conversation

@HankYuLinksys

@HankYuLinksys HankYuLinksys commented Jun 17, 2026

Copy link
Copy Markdown
Collaborator

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 ServiceError class at the Service layer. These ServiceError objects are then passed to the View layer for display. Most importantly, all ServiceError subtypes now have corresponding localizations (except UnexpectedError, which displays the raw detail string as a final fallback — this may be English firmware technical messages).

Error display paths

Path Trigger Storage Display
Path 1 (fetch) Initial data load fails state.error ServiceErrorView (empty state widget)
Path 2 (save) Mutation fails Provider rethrows ref.listen + localizeServiceError → snackbar

Key changes

  • Add ServiceError.code and ServiceError.detail for diagnostic context
  • Add TimeoutError subtype for timeout handling
  • Remove unused OTP/admin-password ServiceError subtypes (6 types deleted)
  • Add localizeServiceError() central mapper with exhaustive switch on sealed class
  • Add ServiceErrorView shared widget replacing 18 per-feature _buildError() methods
  • Add 12 ARB keys for error messages
  • Change all feature state models: String? errorMessageServiceError? error
  • Update providers to pass through ServiceError objects (not stringify with '$e')
  • Update views to use shared localization components

Coverage

Scope Status
USP feature pages ✅ Covered
firmware_update ⏸️ Deferred (complex flow)
SSE subscription errors ⏸️ Out of scope (separate path)
StorageError / SerialNumberMismatchError ✅ Mapped but won't reach UI (caught at session/auth layer)

Notes

  • UnexpectedError.detail is the final fallback — it surfaces raw technical strings when no typed error matches
  • Batch failures (UspPartialFailureError / UspCompleteFailureError) show the first entry's concrete error message, not "N items failed"

Test plan

  • flutter analyze passes
  • ./run_tests.sh passes (2854 tests)
  • Manual test: trigger fetch error → see localized message in ServiceErrorView
  • Manual test: trigger save error → see localized snackbar message

Part of #919

🤖 Generated with Claude Code

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-code-review

Copy link
Copy Markdown

Qodo reviews are paused for this user.

Troubleshooting steps vary by plan Learn more →

On a Teams plan?
Reviews resume once this user has a paid seat and their Git account is linked in Qodo.
Link Git account →

Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center?
These require an Enterprise plan - Contact us
Contact us →

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.
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

@AustinChangLinksys AustinChangLinksys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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 UIUnexpectedError(: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 couplingservice_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 logicservice_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; ServiceErrorView uses ui_kit_library.
  • Existing tests updated consistently (errorMessage containsisA<XxxError>(); message:detail:; mocks throw typed errors).
  • Null/empty + async-race paths checked: empty-failures guard, safe fromMap defaults, correct TimeoutException/ServiceError split 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 AustinChangLinksys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤖 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:165 now throws UnexpectedError(detail: errorAdminAccountLocked) (import added at :100); auth_provider.dart:_mapToViewError passthrough (diff :332-334) preserves it ahead of the generic errorUnexpected overwrite, so the login view's errorCodeHelper resolves the lockout message and no raw string leaks. Covered by usp_auth_coordinator_test.dart and auth_notifier_test.dart.
  • VLAN disable via SET (Enable=false) instead of Delete is an intentional design change; _saveVlanSettings only emits changed params via _diff, no spurious SET or race.
  • Layering intact. usp_auth_coordinator.dart:100 imports 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 from package:ui_kit_library/ui_kit.dart (import at :10) — no hand-rolled widgets.
  • No dead/orphan imports. parseDeleteResult / _handleDeleteResult removed together with their callers; VlanTermination and UspResultParser still used elsewhere.
  • Localization is exhaustive — sealed switch is total, empty failures and unknown vendor codes fall back to errorUnexpected without leaking raw errorMessage; edge cases (first-failure selection, empty list, unknown code) are tested.
  • Tooling/docs improvements: .claude/skills/review-pr-readiness/SKILL.md adds a mandatory "run affected tests" gate step; .claude/settings.json drops 2>/dev/null so 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.
@HankYuLinksys
HankYuLinksys merged commit 34d7e43 into dev-2.5.0 Jun 18, 2026
2 checks passed
@HankYuLinksys
HankYuLinksys deleted the hank/usp-error-localization branch June 18, 2026 17:13
@HankYuLinksys HankYuLinksys linked an issue Jun 18, 2026 that may be closed by this pull request
4 tasks
AustinChangLinksys pushed a commit that referenced this pull request Jun 30, 2026
…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>
AustinChangLinksys added a commit that referenced this pull request Jul 16, 2026
* 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…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[loc] Localize error messages across all USP features (cross-cutting)

2 participants