Skip to content

chore: merge dev-2.6.0 to usp - #1147

Merged
AustinChangLinksys merged 56 commits into
uspfrom
merge/dev-2.6.0-to-usp
Jul 16, 2026
Merged

chore: merge dev-2.6.0 to usp#1147
AustinChangLinksys merged 56 commits into
uspfrom
merge/dev-2.6.0-to-usp

Conversation

@AustinChangLinksys

Copy link
Copy Markdown
Collaborator

Merge dev-2.6.0 to usp

Promotes the completed 2.6.0 work from dev-2.6.0 into the usp (2.x main) branch for release.

  • 57 commits / 38 merged PRs since dev-2.5.0
  • Full changelog, bug-fix table, and firmware-support list: 📦 Release 2.6.0 #1125

Highlights

Verification

  • dart format clean (303 files)
  • flutter analyze: 0 error / 0 warning (455 pre-existing info-level lints)
  • ./run_tests.sh: 3,327 / 3,327 passed (2026-07-16)
  • No merge conflicts — usp HEAD is the merge-base of this diff

Diff base: dev-2.5.0…dev-2.6.0.

Closes #1125

HankYuLinksys and others added 30 commits June 30, 2026 10:51
…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>
- 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>
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.
…View

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.
…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.
…l-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>
…tle param, compact widgets)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
* 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 (#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>
…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>
…ncy (#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>
#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>
- 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>
…rsistence (#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
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>
- 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>
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>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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.
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.
…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.
- 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
…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)

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).
…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.
* 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>
…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 _showNaturalRecoveryDialog
- Add comment explaining why threshold is 2 (avoids ~6 min wait)
- Add tests verifying recoveryContext is cleared after recovered and
  serialMismatch probe results

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>

* fix(remote): address code review findings

- H2: Add HTTP status validation in GuardianApiClient._request()
- M2: Capture all refs before async gap in RemoteSessionChip._disconnect()
- M4: Switch domainBase from linksysDomain to guardianDomain

M3 analyzed: Duplicate timers are intentional - confirm_view timer is for
pre-connection validation, remote_access_provider timer is for post-connection
session tracking. Different lifecycle stages require separate management.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(remote): add client-side Remote Assistance with session recovery

- Add RemoteAssistanceBanner for PENDING state after page refresh
- Add RemoteAssistanceActiveDialog for ACTIVE state recovery
- Add deviceCredentialsProvider to unify credential access
- Integrate session recovery in DashboardShell via orchestrator
- Add checkAndRestoreSession() to RemoteClientNotifier
- Fix ServiceError handling per constitution Article XIII
- Use showAppDialog instead of showDialog per UI Kit guidelines
- Consolidate test data to test/mocks/test_data/
- Add golden tests for banner (pending, pending_urgent states)
- Add golden tests for dialog (initiate, pending, active, invalid states)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(mascot): health dashboard with problem-first display (#939)

* feat(mascot): add health dashboard with problem-first display and DRY refactor

- Replace word cloud with HealthStatusView showing problem-first display
- Add DimensionDetailView for expanded dimension actions
- Add healthEvaluationContextProvider to eliminate 4x code duplication
- Migrate all Text widgets to AppText per constitution Article XV
- Add brand color comments for mascot hardcoded colors
- Delete unused health_word_cloud.dart
- Fix demo mode to directly enter Dashboard for UI verification

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(ui): enable icon support in NetworkBadgeWidget

Pass NetworkBadge.icon to AppBadge now that ui_kit_library v2.25.0
supports the icon parameter.

Closes #916

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(mascot): address code review feedback for health dashboard

- #2: Migrate Text to AppText in health_dialog_provider
- #3: Remove BuildContext from provider, use onNavigate callback (IoC)
- #1: Replace _Epoch hack with nullable DateTime? lastEvaluated
- #4: Add previousDisabledRadios state tracking to WiFi trigger
- #6: Simplify Registry to static HealthDimensions class
- #8: Update documentation (Word Cloud → Health Status View)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>

* feat(internet-settings): add MTU validation and MAC Clone editing (#757, #843)

- Update codegen for FW 1.2.1 alias changes (cpe-wan/cpe-lan → wan/lan)
- Add MTU range validation (576-1500, PPPoE: 576-1492) with error messages
- Add MAC Clone input field with Clone button to select connected devices
- Integrate WanMacClone.update() for writing Ethernet.Link.MACAddress
- Invalidate wanDataProvider after save to sync Dashboard
- Update WiFi guest detection to use alias suffix instead of index
- Fix test mocks for new alias values and Ethernet.Link data

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): display meaningful error message on login failure (#940)

Previously, USP login failures showed "Unknown error: _ErrorUnexpected"
because error details were lost in the coordinator-to-provider chain.

Changes:
- UspAuthCoordinator.tryUspLogin() now throws typed ServiceError instead
  of returning boolean, preserving error context
- AuthNotifier maps ServiceError to UnexpectedError with proper error
  codes for View layer consumption
- login_local_view handles cases where no delay/attempts data is present
- Added errorInvalidAdminPassword to error_code_helper for proper i18n

Note: Account locked vs invalid password distinction requires WASM client
fix (linksys/usp_framework#34) - currently both show as incorrect password.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(internet-settings): enable PPPoE Service Name field

Re-enable pppoeServiceName editing that was previously disabled due to
bbfdm fault 9001. The firmware issue has been resolved.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* Revert "feat(internet-settings): enable PPPoE Service Name field"

This reverts commit ef0650d749aceb13f21dc085ccedef9c0aa6f8fe.

* revert: disable PPPoE Service Name and MAC Clone features

- Revert PPPoE Service Name field (FW still rejects SET with fault 9001)
- Remove MAC Clone UI and service integration (WanMacClone.g.dart)
- Keep MTU validation (576-1500/1492) and Dashboard sync fix

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: update validator tests for MTU range enforcement

- Add valid mtu value (1500) to all test forms
- Remove mtu=0 (auto) test since auto mode is hidden
- Add PPPoE-specific MTU max (1492) tests

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(wasm): update usp_client to return full error messages

WASM client now correctly returns complete error messages from
usp-auth-cgi, enabling meaningful login error display (e.g.,
"Too many failed attempts" for account locked).

Ref: #940

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(dashboard): disable Speed Test card

Speed Test feature blocked by FW support (#857). Comment out:
- Widget spec definition
- Factory registration
- Import

Route preserved for future re-enablement.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: update dashboard tests for Speed Test removal

Update widget count expectations from 19 to 18 after disabling
Speed Test card (#857).

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(internet-settings): add 6rd tunnel field validation with error messages

- Add IPv6 CIDR format validation for 6rd prefix using IPv6WithReservedRule
- Add IPv4 validation for border relay using IpAddressRule
- Add inline error messages via externalErrorText
- Rejects reserved addresses: loopback, multicast, unspecified

Closes #852

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(internet-settings): skip MTU validation for bridge mode

Bridge mode uses auto MTU (mtu=0 sent to firmware). The validator was
blocking save because 0 < 576. Now bridge mode:
- Skips MTU validation (allows mtu=0)
- Shows "Auto" in UI instead of editable field

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(errors): preserve diagnostic code/detail through ServiceError

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

* feat(l10n): centralize error message localization for USP features

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.

* fix(remote): add session recovery guard to UspDashboardShell

- Create RemoteAssistanceSessionGuard widget for client-side session recovery
- Integrate guard into UspDashboardShell (the actual shell used in routing)
- Add RemoteAssistanceBanner to UspDashboardShell for PENDING state
- Shows blocking dialog when ACTIVE session exists after page refresh

The previous implementation incorrectly added recovery logic to DashboardShell,
but the app actually uses UspDashboardShell for routing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(remote): force end CA session on 401 unauthorized

When the CA's session polling receives a 401, automatically:
- Mark session as invalid (triggers UI state change)
- Stop polling/countdown timers
- Navigate to confirmation page with expired=true param
- Logout the CA user

This ensures the CA UI doesn't hang when the session token expires.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(remote): handle INVALID status in client RA dialogs

When the session becomes INVALID (CA ended it or session expired):
- _RemoteAssistanceDialog: Shows snackbar notification
- RemoteAssistanceActiveDialog: Shows snackbar and auto-closes

Also handles edge case where dialog opens with already-invalid status
by showing appropriate UI with close button.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore(usp): update usp_client WASM to 0.11.1

Update vendored WASM client artifacts from usp_framework:
- usp_client.js: 0.9.0 -> 0.11.1
- usp_client_bg.wasm: 0.9.0 -> 0.11.1

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: remove unused DashboardShell

DashboardShell is not referenced by any code — only UspDashboardShell
is used in routing. Remove to avoid confusion.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(internet-settings): use SET instead of ADD/DELETE for VLAN tagging and enable PPPoE ServiceName (#951)

- Replace VLAN lifecycle (ADD/DELETE) with SET Enable on the existing
  VLANTermination.1 instance, fixing the bug where disabling VLAN still
  showed as enabled due to a system-default instance that cannot be deleted.
- Uncomment PPPoE ServiceName field in UI and service layer now that
  bbfdm supports SET on PPPoE.ServiceName.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(internet-settings): clean up stale comments after VLAN lifecycle removal

- Fix step numbering gap (Step 4 → Step 6 becomes Step 4 → Step 5)
- Remove orphaned VLAN Lifecycle section header
- Remove leftover DELETE doc comment on _handleOperateResult

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* chore(.claude): fix dart format hook by removing unmatched if glob

The `if: "Bash(git commit *)"` glob cannot match heredoc-style
commit commands. Remove the condition since the hook command itself
already guards with `if [ -n "$staged" ]`.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* test(internet-settings): align VLAN tests with SET-based approach

Replace ADD/DELETE VLAN lifecycle tests with SET Enable tests:
- internet_settings_service: test enable/disable via SET, skip when no instance
- pnp_service: test enable/disable VLAN via SET on existing instance
- Remove obsolete DELETE failure test

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(skill): add mandatory test execution to review-pr-readiness

Static checks (format, analyze, file existence) cannot catch logic
regressions. Add Step 4.3 that collects and runs all affected tests
before issuing the gate stamp — test failures now block PR creation.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* fix(remote): use abs() for expiredIn forward compatibility

Cloud API will change expiredIn from negative to positive format.
Using abs() ensures both conventions work during transition:
- Current: negative value = remaining seconds
- Future: positive value = remaining seconds

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(l10n): align batch fault-code localization with _mapProtocolError

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.

* fix(auth): restore account-locked message after AdminAccountLockedError 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).

* fix(remote): address PR #955 review issues

Critical fixes:
- Move RemoteAssistanceService to lib/core/cloud/services/ (fix core→page dependency)
- Fix error-mapping: _validateResponse now throws ErrorResponse for proper 401 handling
- Remove PIN plaintext logging (security)
- PENDING close now revokes server session (PIN exists server-side)
- Add mutation lock to UspClient swap in activate()/deactivate()

Warning fixes:
- Strip token from URL after reading (prevent history/Referer leakage)
- deactivate() now disposes and unregisters UspClient
- Add null/empty validation for device token and createPin responses

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test(l10n): cover service_error_localizations and ServiceErrorView

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

* style: dart format the three files flagged by CI

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

* docs: fix stale lifecycle comments in usp_internet_settings_service

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.

* fix(polling): start timer when dashboardDomainReadyProvider already resolved

ref.listen() only fires on state changes — if dashboardDomainReadyProvider
completed before the polling provider was first read, the listener never
fires and the timer never starts. This caused Dashboard traffic cards and
Statistics page to show "Waiting for data..." indefinitely.

Add immediate state check in build() to start timer if conditions are
already met.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(polling): unify auth guard logic and add test coverage for already-resolved path

- Extract _startTimerIfAuthenticated() helper to unify auth check between
  ref.listen and ref.read fallback paths (addresses Hank's review comment)
- Add tests for when dashboardDomainReadyProvider already resolved before
  first provider read — covers the new microtask startup path
- Add tests to verify timer does NOT start when domain ready but logged out

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(remote): address Hank's review on remote_client_provider

1. Add ref.onDispose() to clean up timers/subscriptions on rebuild
2. Replace _creds getter with _credsOrNull — graceful early return
   instead of throwing StateError in Timer callbacks
3. Fix poll loop condition: check status != INVALID and expiredIn <= 0
   instead of expiredIn.abs() > 0 (which never terminated)
4. Only start countdown timer if not already running (avoid rebuild on
   every poll tick)
5. Fix ServiceError API: use 'detail' instead of 'message' (dev-2.5.0)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(remote): use ServiceError.detail instead of .message in view layer

After merging with dev-2.5.0, the ServiceError API changed from message
to detail. This caused compilation errors in pattern matching.

Also fix test setup for "not authenticated" tests to use LoggedOutNotifier
instead of AlwaysAuthenticatedNotifier, avoiding unintended USP calls
from auto-timer triggered by dashboardDomainReadyProvider.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(wan): implement PPTP/L2TP connection type support (#839)

- Add GRE Tunnel and L2TP Tunnel codegen definitions (fetch/update)
- Extend UspWanConnectionType with pptp/l2tp variants and detection logic
- Add PPP LowerLayers field to codegen for tunnel type selection
- Implement save orchestration: PPP lifecycle → LowerLayers → tunnel
  RemoteEndpoints → WAN mode switch → PPP credentials
- Add server address field and PPTP/L2TP-specific UI in IPv4 section
- Add form validation for server address when PPTP/L2TP selected
- Update connection status banner to handle PPP-based tunnel types
- Cover new logic with unit tests (model, validator, service)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* feat(l10n): localize hardcoded strings across USP pages (#960)

* feat(l10n): localize hardcoded strings across USP pages

Replace hardcoded English strings with loc(context).xxx calls:
- Add 717 new ARB keys (1018 → 1735 total)
- Add 1041 new loc() calls across 82 view files
- No keys deleted or modified (only additions)
- All changes in View layer only (no provider/service changes)

Covers: statistics, diagnostics, dashboard, wifi_settings, port_forwarding,
devices, dhcp, firewall, dmz, static_routing, ipv6_port_service,
instant_privacy, instant_safety, ai_assistant, firmware_update, admin, menu

* refactor(l10n): deduplicate and consolidate ARB keys

- Remove 12 duplicate keys (same key appearing twice in app_en.arb)
- Consolidate 60 pairs of different keys with identical/similar values
- Rename keys across all 26 locale files to preserve translations
- Delete unused dead-code keys (vpn*, modal*, pnp* prefixes)
- Fix avgValue key that was accidentally deleted

* fix(auth): reduce duplicate login requests on app startup

- Add Completer deduplication to restoreSession() and authProvider.init()
  to coalesce concurrent calls
- Add cooldown mechanism after failed login to prevent account lockout
- Implement token-first strategy: try refreshToken() before password login
- Remove redundant restoreSession() calls from wifi_settings_provider
  and internet_settings_notifier (auth already handled by orchestrator)
- Remove unnecessary autoConfigurationLogic() call on /localLoginPassword
  route (user is already on login page)

This reduces login requests from 5 to 1 on app startup with stored
credentials, preventing potential account lockout from rapid retries.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(pnp): prevent guest WiFi page from appearing twice (#963)

Root cause: PnpEntryView's ref.listen triggered navigation to /pnp/config
when save failed and state reverted to WizardConfiguring, causing the view
to recreate and reset _currentStep to 0.

Changes:
- Add guard in pnp_entry_view.dart to only navigate on initial transition
- Update login_local_view.dart to route via '/' for proper PnP check
- Refactor PnP UI with AppCard + LayoutBlock for consistent styling
- Add split SSID mode support (per-band WiFi config) for #935 alignment
  - New PnpWifiBand model for per-band configuration
  - PnpWifiConfig extended with mainBands/guestBands lists
  - Service layer handles both unified and split mode save
  - View renders per-band forms when split mode detected
  - WiFi Ready page shows all band credentials in split mode

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): address review findings for login deduplication PR

- Restore autoConfigurationLogic + redirectLogic on /localLoginPassword
  route to prevent logged-in users from staying on login page
- Fix dead code in AuthNotifier.init(): AsyncValue.guard never throws,
  so catch block was unreachable; now properly checks AsyncError
- Add test coverage for restoreSession coalescing and cooldown
- Add test coverage for AuthNotifier.init() coalescing

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(pnp): address review findings for guest WiFi duplicate PR

- C1: Add missing meshNodes to updateGuestSsid (prevents mesh regression)
- C2: Revert login navigation to goNamed(uspDashboard) to avoid rebuild
  loops (context.go('/') requires #976 coalescing which isn't merged)
- W1: Add empty guard in _buildCompleteSplitMode to prevent crash
- W5: Add mainSsids empty check in fetchWifiConfig to prevent crash

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(pnp): restore context.go('/') for proper PnP check + address Round 2 review

Round 2 fixes:
- C1: Restore context.go('/') to enable PnP check via router redirect
  (requires #976 auth coalescing to prevent rebuild loops)
- C2: Use mapUspErrorToServiceError for mainSsids empty check
- W2: Add logger.w for unexpected empty mainBands fallback

Depends on: #976 (auth coalescing) — merge #976 first to prevent
rebuild loops from repeated init() calls.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(auth): use Dart 3 pattern matching for AsyncError check

Replace redundant `as AsyncError` cast with pattern matching:
`if (state case AsyncError(:final error, :final stackTrace))`

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): propagate error to first caller and add completeError test (W3)

- First caller now also throws when init() fails (instead of returning null)
- Add test for concurrent init waiters receiving error via completeError
- Fix dart format issue from previous commit

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(auth): restore "init never throws" contract per review feedback

Remove Error.throwWithStackTrace — callers (app.dart, router_provider.dart)
use bare .then() without .catchError, so throwing would leave the splash
screen stuck or break the redirect. Primary caller now receives null on
error; concurrent waiters still receive error via completeError.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* l10n: complete missing translations, migrate hardcoded strings, fix existing bugs (#979)

* chore(l10n): clean up unused/orphan keys and add keys for hardcoded strings

Clean up the ARB files and prepare l10n keys before the code-side
hardcoded-string migration:

- Remove 512 unused keys (verified no Dart references via flutter analyze;
  kept avgValue/copyRight which are accessed via cross-line loc() calls)
- Remove 901 orphan entries (60 keys present in locale files but absent
  from app_en.arb)
- Add 111 new keys to app_en.arb fo…
HankYuLinksys and others added 24 commits July 10, 2026 14:22
…lling

- Add privateMacWarningTitle / privateMacWarningDesc for all 25
  non-English locales
- Apply native-review fixes: zh_TW use 私密 (Apple's term), it use
  compreso, vi align setting name to OS term, nl fix spacing
- Standardize "WiFi" to "Wi-Fi" across the warning strings, incl. en
…f dialog

- Move warning description to page banner (below feature desc)
- Add red "Private" badge on device list items with private MAC
- Simplify enable dialog to show title-only warning
- Set isPrivateMac flag on allowed devices (ON list) too
- Add privateMacLabel translations for all 26 locales
Port #926 fix to dev-2.6.0 branch.
Move inline <script> blocks from index.html to early-bootstrap.js,
enabling removal of 'unsafe-inline' from script-src CSP directive.

Closes #1084
Ref: linksys/LinksysWRT#352

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…1079)

DHCP Renew/Renew6 operations return void - no need to parse and validate
the result. The previous strict parsing caused false-positive errors on
some firmware versions that returned non-standard response formats.

Closes #967

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…1082) (#1086)

* fix(static-routing): block interface change without gateway update (#1082)

When editing a static route, changing the Interface (LAN<->Internet)
without also updating the Gateway left the old gateway (belonging to the
previously selected interface's subnet) in place, and the form still
passed validation and submitted the SET. The backend accepts it, so the
route saved successfully instead of being blocked client-side.

validateRoute() now takes optional interfaceName / originalInterfaceName
/ originalGateway; in edit mode, if the interface changed but the gateway
is unchanged it returns a gateway error. Add mode (no original values)
skips the check. Invalid-gateway-format still takes precedence.

Refs #1082

* fix(static-routing): validate gateway subnet based on interface selection (#1082)

Replace the "interface changed but gateway unchanged" check with proper
subnet validation using existing HostValidForGivenRouterIPAddressAndSubnetMaskRule:
- LAN interface: gateway must be within LAN subnet
- Internet interface: gateway must be outside LAN subnet

Also triggers validation on interface toggle so error message displays immediately.

Refs #1082

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(static-routing): optimize validation and add l10n support (#1082)

Address PR review feedback:
- W-2: Change _isFormValid to use cached _errors instead of recomputing
- S-2: Add l10n keys for gateway subnet validation errors (26 locales)

Also initialize _errors in initState to ensure correct initial form state.

Refs #1082

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(static-routing): guard empty-string LAN data in gateway subnet validation

W-NEW-1: validateRoute() only null-checked lanIp/lanSubnetMask, but
LanData.empty() returns ipAddress:'' / subnetMask:'' (empty strings, not
null). ipToNum('') == 0, so HostValidForGivenRouterIPAddressAndSubnetMaskRule
computed hostSubnet == routerSubnet == 0 and reported isInLan == true for ANY
gateway, firing a permanent false-positive gatewayMustBeOutsideLanSubnet error
on Internet-interface routes while LAN data was still loading (device boot /
network reset). Guard now also requires isNotEmpty + valid IP/mask before
running the subnet rule. Added a regression test covering the empty-string
LanData.empty() fallback.

Refs #1086

* fix(static-routing): suppress validation errors on add-dialog open (#1082)

W-NEW-A: initState called _computeErrors() unconditionally, so opening the
add-route dialog showed nameRequired/destIpRequired/subnetMaskRequired errors
before the user typed anything. Compute errors only in edit mode; keep the
empty-map default on add.

* fix(static-routing): enforce form validity and await LAN data (#1082 review-fix)

- _isFormValid now recomputes from live field values instead of the cached
  _errors map, so a blank add-mode form no longer enables Save (N-NEW-1).
- _showAddDialog/_showEditDialog await lanDataProvider.future instead of
  reading valueOrNull, so subnet validation is not silently bypassed on a
  fast tap while LAN data is still loading (W-NEW-2).

* fix(static-routing): catch lanDataProvider errors in add/edit dialog openers (#1082 review-fix)

W-R3-1: _showAddDialog/_showEditDialog awaited lanDataProvider.future with no
try/catch. When the provider is in AsyncError (router offline, USP timeout,
auth failure) the throw propagated through the onTap lambda and Flutter
swallowed it silently — tapping Add/Edit did nothing with no error shown.
Now both openers surface the error via showFailedSnackBar + localizeServiceError
and return early, matching the established _onSave pattern.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(dhcp): use Hosts.Active for online status indicator (#1034)

DHCP client indicator was showing green for offline devices because it
used DHCPv4.Client.Active (lease validity) instead of Hosts.Host.Active
(actual device connectivity).

Changes:
- Add isOnline field to DhcpClientUIModel from Hosts.Active
- Rename active to leaseActive for semantic clarity
- Dashboard DHCP card now filters to online clients only
- DHCP Detail page shows all clients with filter chips (All/Online)
- Normalize MAC to uppercase in model constructors
- Listen to devicesDataProvider for real-time online status updates

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(dhcp): address PR review feedback

- W-1: Fix DhcpData.props to use full list comparison instead of lengths
- W-3: Replace Material FilterChip with UI Kit AppChipGroup
- W-4: Move filter state to StateProvider for persistence across navigation
- W-5: Make isOnline null handling explicit in PDF export
- S-1: Use null coalescing for isOnline in AI command provider

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(dhcp): narrow devicesDataProvider listener to online-status changes

W-6: the devicesDataProvider listener re-fetched DHCP on ANY DevicesData
change (RSSI/band/SSID), and read a possibly-stale snapshot mid-rebuild.
Now compare the mac->isActive online-status map (via MapEquality, since
Dart Map == is identity-based) and only debounce-invalidate when it
actually changed.

Refs #1034

* fix(dhcp): use clientDevices (exclude mesh nodes) for online-status diff

Mesh nodes (master/slave) never hold DHCP leases, so their MACs cannot
appear in DHCP client models. Diffing over deviceModels caused a satellite
node going on/offline to falsely fire _debouncedInvalidate() and trigger a
redundant DHCP re-fetch. Switch both the devicesDataProvider listener diff
and the _fetch() isOnlineByMac map to clientDevices, which excludes mesh
nodes. Addresses PR #1087 review finding W-NEW-4.

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
#1093)

* fix(nav): use pushNamed for sub-page navigation to preserve back stack (#1029)

Replace goNamed with pushNamed when navigating from list views to detail
pages. goNamed rebuilds the route stack declaratively, losing the push
history from Dashboard cards. pushNamed preserves the navigation stack
so back button correctly returns to the previous page.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(nav): revert firewall and local_network changes per code review

Revert to goNamed for:
- Firewall → IPv6PortService: sibling routes under AdvancedSettings,
  pushNamed + destination's goNamed back causes stack corruption
- LocalNetwork → DhcpDetail: pushNamed bypasses onExit dirty-check guard

Keep pushNamed for:
- DeviceList → DeviceDetail: parent-child route, preserves back stack
- NodeDetail → DeviceDetail: cross-section navigation, works correctly

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…#1061) (#1101)

Editing the "Forwarded Ports" value of an existing port-triggering rule
silently reverted. Two independent defects:

1. UI (usp_port_triggering_tab.dart _showEditDialog): the copyWith() that
   built the edited rule omitted `forwardRules`, so copyWith retained the
   old forward rules and the dialog's forwarded-port edit was discarded
   before it ever reached the notifier.

2. Service (usp_port_forwarding_service.dart saveTriggeringBatch): the
   update path only patched parent-level trigger fields (Enable/Description/
   Port/PortEndRange/Protocol) and never reconciled the nested forwarded-port
   sub-table (Device.NAT.PortTrigger.{i}.Rule.{j}). Even a correct model
   change would not have been written.

Fix:
- Dialog now rebuilds the first forward rule from the dialog result,
  preserving its instancePath for in-place update, and carries any extra
  forward rules (beyond what the single-mapping dialog shows) untouched.
- saveTriggeringBatch now reconciles forward rules for existing parents via
  a new _reconcileForwardRules helper: in-place Set for changed rules,
  Add for new (null-instancePath) rules, Delete for removed rules.

Tests: 4 new regression tests in usp_port_forwarding_service_test.dart
covering edit/add/delete of forward rules on an existing trigger and the
no-op case. 50/50 port_forwarding tests pass; dart format + analyze clean.

Refs #1061
…e and private MAC filters (#1107) (#1110)

- Replace single-select dropdowns with multi-select chips (OR within dimension, AND across)
- Add Device Type filter with category icons (phone, tablet, computer, etc.)
- Add Private/Public MAC filter using OUI lookup
- Fix signal filter bug: exclude WiFi with known RSSI when only unknown selected
- Add private MAC badge overlay to device list tile icons
- Localize 'Private'/'Public' labels across all 26 language files
- Bump ui_kit_library to v2.27.0

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…efix sync (#1039) (#1117)

- Fix firmware error 7005 on save: LAN settings span multiple USP Services
  (Device.IP, Device.DHCPv4, Device.DeviceInfo), which the firmware cannot
  apply in one atomic SET. Pass allowPartial: true so each Service applies
  independently.

- Add post-save redirect flow for IP changes: changing the router IP makes the
  old address unreachable and the SET response never returns, so treat an
  IP-changing SET as terminal (timeout / transport error = success; a real
  firmware fault still surfaces), redirect the browser to
  https://<hostName>.local, and intentionally drop SSE to suppress the recovery
  dialog. Triggered only on an IP address change — a mask-only or DHCP-only
  change is awaited normally with a success message.

- Fix address pool prefix sync following IP or subnet mask changes: the pool
  prefix only re-synced when the IP changed, not the mask. Tightening the mask
  (e.g. /16 to /24) widened the locked prefix without updating the pool,
  leaving pool octets both out-of-subnet (validation error) and locked
  read-only — a dead end. Sync now runs on either change.

- Add lanIpRedirect strings across all 26 locales.

- Add tests: terminal-SET classification (timeout/transport = success, fault
  rethrows), redirect + SSE-disconnect branch, and pool prefix auto-sync.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
* fix(dashboard): exit edit mode when navigating away (#1037)

Extract edit mode state from view-local to a centralized provider so it
can be accessed by route guards. When user navigates away from dashboard
(e.g., tab switch) during edit mode, onExit cancels edit mode and
reverts any unsaved layout changes.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(dashboard): preserve applied layout changes on edit-mode exit (#1089)

Address PR #1089 review. Root cause: exitEditMode({bool save}) was an
inverted-boolean trap — the !save branch persisted the pre-edit snapshot,
so the settings-panel reset/preset path (which had already applied its
change) called save:false and silently reverted the user's change.

- Replace exitEditMode({bool save}) with explicit commitEditMode()
  (keep changes) and cancelEditMode() (revert to snapshot), sharing a
  private _exitEditMode({required bool revert}) that always resets state
  in a finally block (no stranded isEditing=true on save/restore failure)
- Route _openLayoutSettings reset/preset path to commitEditMode() so the
  just-applied change is preserved; drop dead 'toggle_off' branch
- enterEditMode: add re-entrant guard and claim isEditing before the
  await so an onExit firing in the async gap can never strand edit mode
- route_usp_dashboard onExit: wrap cancelEditMode in try-catch and
  document the intentional silent-discard policy
- Tighten DashboardEditState.layoutSnapshot to List<Map<String, dynamic>>?
- Delete unused lib/route/linksys_route.dart (dead code; active
  LinksysRoute lives in route_model.dart)
- Add regression tests: commit-preserves-changes, re-entrant guard (W-1),
  cancel-during-async-gap (W-2)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…1068)

* feat(devices): introduce MeshNetwork architecture with SSoT pattern (#1043, #1044)

Phase 1-3 of DeviceUIModel/NodeUIModel refactoring:

New models (lib/page/_shared/models/):
- NetworkEntity: abstract base class for network entities
- ClientDevice: client device model with WifiConnectionInfo
- NodeEntity: sealed class (MasterNode/SlaveNode) with BackhaulInfo
- MeshNetwork: top-level SSoT container with lookup helpers
- WifiConnectionInfo/BackhaulInfo: value objects for connection details

New builder (lib/page/_shared/utils/):
- MeshNetworkBuilder: constructs MeshNetwork from Hosts + DataElements

Integration:
- DevicesData: added meshNetwork field alongside legacy deviceModels/nodeModels
- UspDevicesDataService: builds MeshNetwork in fetch() and rebuild methods
- Compatibility layer maintains existing API for gradual migration

Also fixes Device Analytics card (#1043, #1044):
- Simplified to 4 tabs: Overview, Signal, Trend, Activity
- Fixed Y-axis duplicate labels in trend chart
- Use node hostname instead of model name for child node clients
- Exclude mesh nodes from client distribution counts

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(topology): migrate to MeshNetwork architecture (Phase 4)

- UspTopologyBuilder: add buildFromMeshNetwork() method, deprecate old build()
- UspNetworkTopologyCard: use meshNetwork parameter instead of device/node lists
- UspTopologyView: use buildFromMeshNetwork()
- uspNodeDetailProvider: support NodeEntity with legacy model conversion

The provider now uses MeshNetwork.findNode() for direct lookup and
pre-organized connectedClients, while maintaining backward compatibility
by converting to NodeUIModel/DeviceUIModel for existing views.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(devices): complete Phase 5 - remove DeviceUIModel/NodeUIModel (#1043, #1044)

Complete migration to MeshNetwork architecture as Single Source of Truth:

- Remove DeviceUIModel class and related extensions
- Remove NodeUIModel class and legacy conversion helpers
- Remove obsolete test files for deleted models
- Migrate all 26+ consumers to use ClientDevice/NodeEntity directly
- Update MeshNetworkBuilder to patch parentNodeName on all clients
- Improve Device Analytics card UI with LayoutBlock grid layout
- Show connected node name for ALL devices (including master node clients)

Architecture benefits:
- SSoT: MeshNetwork contains all network entities in one place
- Sealed classes: NodeEntity (MasterNode/SlaveNode) enable pattern matching
- Direct ownership: Nodes own their connectedClients list
- Cleaner lookups: meshNetwork.findNode()/findClient() replace manual loops

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* style: dart format usp_network_topology_card.dart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(devices): correct Equatable props and add model tests (#1068)

Fix DevicesData.props to use full objects instead of .length for proper
Riverpod state comparison. Add comprehensive unit tests for MeshNetwork,
ClientDevice, NodeEntity models and UspTopologyBuilder.

- Fix props bug: meshTopology/hostNameByMac now compared as objects
- Add DevicesTestData builder (484 lines) for centralized test factories
- Add MeshNetwork tests (45 tests): accessors, lookups, Equatable
- Add ClientDevice tests (44 tests): displayName, WiFi, multi-interface
- Add NodeEntity tests (28 tests): Master/Slave, backhaul, extensions
- Add UspTopologyBuilder tests (28 tests): nodes, links, edge cases

Total: 145 new tests, all 3047 tests passing.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat(wifi): show slave node clients in WiFi Performance card

- Add clientBandSsidMap to MeshTopologyInfo for slave client band/SSID
- Add buildBssidToBandMap() to resolve BSSID → band via SSID LowerLayers
- Use meshNetwork.allClients as data source instead of wifiClientMap
- Add band/SSID fallback to clientBandSsidMap for slave node clients
- Truncate long client names in Speed tab chart to prevent overlap
- Add comprehensive tests for MeshNetworkBuilder, MeshTopologyBuilder,
  and UspWifiDataService

Closes #1043, #1044

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(devices): address PR #1068 review — RA UUID, wired categorization, fallback

Resolve the two blocking review issues plus a rule violation and a
band-fallback logic bug found in MeshNetworkBuilder:

- RA deviceUUID regression: add hostsDeviceId (Hosts UUID) to MasterNode,
  plumb it through MeshNetworkBuilder, and use it in deviceCredentialsProvider
  instead of the MAC. Returns null when no UUID is available (matches the
  legacy behavior; avoids sending a wrong deviceUUID to Guardian RA).
- Wired-client miscategorization: _getDeviceCategory now keys off parentNodeId
  (null only for master clients) instead of the parentNodeName that the builder
  patches onto ALL clients — master wired clients bucket under "Wired" again.
- Remove dead test/test_helpers/mesh_network_test_helper.dart (unused;
  duplicates DevicesTestData; violated constitution §1.6.2 location rule).
- Band/SSID fallback: treat empty-string band/ssid from connectionDetailMap as
  absent so the DataElements value is used (empty string no longer masks it).

Tests: add device_credentials_provider_test; update notifier test to model the
real patched shape (master clients carry parentNodeName) as a regression guard;
add empty-string fallback case and MasterNode.hostsDeviceId equality test.

Note: slave-node client band display (#1044 follow-up) needs codegen YAML
changes and is tracked separately — see doc/usp/dataelements-sta-band-enhancement.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(devices): categorize by band on mesh + guard SNR average (PR #1068 review)

Address the two review findings from @PeterJhongLinksys and @HankYuLinksys.

- _getDeviceCategory: categorize WiFi clients by band whenever a band is
  present, regardless of parentNodeId. On a real mesh, MeshTopologyBuilder maps
  every node's STAs — including the master's own clients — into clientToNodeMap,
  so master WiFi clients get a non-null parentNodeId and the old
  `parentNodeId != null` check collapsed their band under the gateway name.
  Master WiFi band comes from the local WiFi.AccessPoint chain (FW-provided), so
  band-first is correct; band-less WiFi (slave clients, pending #1118) falls back
  to the node name; wired → "Wired".
- usp_wifi_performance_card: only clients with real noise data (noise != 0)
  contribute to the per-radio average SNR. Slave clients have noise 0, so
  including them would deflate the average once #1118 gives them a band; they
  are still counted in clientsPerRadio.
- Update analytics regression test to the real mesh shape (master WiFi client
  with non-null parentNodeId + band → categorized as band, not gateway name).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…el logs (#1075)

* feat(diagnostics): add DiagnosticLoggable mixin for structured state logging

Add a centralized state logging system that captures provider states
for diagnostic reports without polluting the main log stream.

Changes:
- Add DiagnosticLoggable mixin with namedProps for JSON serialization
- Add StateLogObserver to capture provider states to cache
- Add loggable flag (default true) for opt-out control
- Support both async and sync providers
- Add trace level filtering for dev-only logs in production
- Migrate all Data providers and UIModels to use DiagnosticLoggable

State log is captured silently and included only when user downloads
the diagnostic report via outputFullWebLog().

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(diagnostics): address code review findings

- Add explicit enum handling in _toJsonSafe (outputs "name" not "EnumType.name")
- Add Duration handling (outputs milliseconds)
- Simplify Map handling (single branch handles all Map types)
- Add @VisibleForTesting getters for state log cache verification
- Add tests for enum, Duration, non-string map keys
- Fix observer tests to verify actual cache contents
- Document that didUpdateProvider only fires on state changes (not initial build)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(test): update DhcpData test for DiagnosticLoggable props change

The test was asserting `props == [2, 1]` (lengths), but after migrating
to DiagnosticLoggable, props now contains the full namedProps values.
Updated the test to verify equality and list lengths separately.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(ui): unify TopBar to UspTopBar

- Delete legacy `top_bar.dart` — it was a JNAP-era component
- Update `ui_kit_page_view.dart` to use UspTopBar
- Enhance UspTopBar:
  - Add optional `controllerProvider` param (defaults to uspMenuController)
  - Add DebugObserver mixin for rapid-tap log download
  - Fix Apps button visibility based on login state

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor(logging): use trace level for dev-only logs

- Change verbose development logs from `logger.d()` to `logger.t()`
  - App build lifecycle (`[App]: build`)
  - Throttler dispatch details
  - WiFi/Topology internal diagnostics
  - Devices/mesh internal matching logs
- Simplify SSE log tags (`[USP][SSE][Bootstrap]` → `[SSE]`)
- Remove redundant debug logs in devices service (mesh node matching)
- Production builds filter out trace level, reducing log noise

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address code review warnings and suggestions for PR #1075

Warnings fixed:
- W-1: Mask sensitive data (MAC/serial/password) in state log cache
- W-2: Change SSE pretty-print log from debug to trace level
- W-3/W-4: Add explicit props override to WifiData for reliable equality
- W-5: Replace IconButton with AppIconButton + Tooltip

Suggestions fixed:
- S-1: Log warning on WiFi fallback fetch failure instead of silent catch
- S-2: Remove dead code (tag == 'State' branch in logger)
- S-3: Add Map equality tests for DiagnosticLoggable
- S-4: Add diagnosticName getter to avoid dart2js minification issues
- S-5: Add .select() to theme config providers in UspTopBar

Additional improvements:
- USP request ID format changed from incremental integer to
  LNU{HEX-TIMESTAMP}{3-CHAR-RANDOM} (e.g., LNU6A4B4BD8F90)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: address round 2 review feedback

- W-1: Remove no-op identity selector on demoThemeConfigProvider
- W-2: Use millisecond timestamp + 16-bit random for request ID
  (reduces collision probability from >50% at 91 req/s to negligible)
- Update stale doc comment in logger.dart

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(dhcp): repair reservation dialog test broken by dev-2.6.0 merge

The DhcpReservationEditDialog test (added in #1078) used
`const DhcpReservationUIModel(...)`, but #1087 later made that model's
constructor non-const (MAC uppercase normalization). The two landed on
dev-2.6.0 without a full re-check, so the test file had 9 compile errors
that only surfaced after merging dev-2.6.0 into this branch.

- const -> final for DhcpReservationUIModel literals; nullable `existing`
  param with in-body fallback (const default no longer valid).
- _enterMac/_enterIp now blur the field after typing. The dialog validates
  on FocusNode blur, not per keystroke, so single-field entry tests
  (duplicate IP, edit-to-existing MAC) never ran _validate() and saw null
  errorText. Blur mirrors a real user tabbing away.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(firewall): add explicit FirewallData props override for reliable equality

Addresses PR #1075 review: FirewallData derived props from namedProps, which
narrowed equality to ruleSummaries.length and dropped ruleContext and
dmzSummaries. A rule whose content changed without changing the count (or a
DMZ-only change) would compare equal, so the provider skipped notifying
listeners and the UI went stale.

Mirror the WifiData fix: keep namedProps lean for diagnostic JSON output, but
override props with the full field list [firewallModel, ruleContext,
ruleSummaries, dmzModel, dmzSummaries]. Add regression tests asserting
content-only rule changes and DMZ-only changes break equality, and that
namedProps stays lean.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* feat(diagnostics): extend state logging to MeshNetwork entities

The #1068 mesh refactor introduced ClientDevice, NodeEntity (MasterNode/
SlaveNode), MeshNetwork, BackhaulInfo, ClientInterfaceInfo and
WifiConnectionInfo, all built `with EquatableMixin`. They nest inside the
DiagnosticLoggable states DevicesData and MeshTopologyInfo, but since they were
neither Equatable nor DiagnosticLoggable, _toJsonSafe fell through to
`toString()` and serialized them as opaque "Instance of 'ClientDevice'" in
diagnostic reports.

DiagnosticLoggable is constrained `on Equatable`, so these EquatableMixin
models cannot mix it in. Extract the JSON contract into a new DiagnosticNamed
mixin (diagnosticName + namedProps + JSON toString) with no Equatable
constraint, and have the six new models mix it in alongside EquatableMixin —
keeping their own `props` for equality untouched. _toJsonSafe now recognizes
DiagnosticNamed, so nested entities render as keyed JSON, consistent with the
existing UIModel logging.

DiagnosticLoggable keeps its `on Equatable` constraint (implements
DiagnosticNamed for the nested-serialization type check) so the 30+ existing
`extends Equatable with DiagnosticLoggable` classes need no change.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
* fix(l10n): align port forwarding naming with 1.x (#1081)

- Rename "Port Triggering" to "Port Range Triggering" across all locales
- Update tab labels to full names without count (moved count to section titles)
- Update related strings: add/edit dialogs, empty state messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix(l10n): reuse existing section keys for port forwarding tab labels (#1097 review)

Drop the three new *Tab keys (singlePortForwardingTab, portRangeForwardingTab,
portRangeTriggeringTab) and reuse the existing section keys (singlePortForwarding,
portRangeForwarding, portTriggering) for the tab labels.

The new keys duplicated existing keys with identical English values but were
translated independently, causing the same concept to render two different ways
on one screen (e.g. zh_TW tab "轉發" vs. section title "轉寄"). Reusing the
section keys removes the duplication and the translation divergence in one step.

English UI is unchanged (identical values); other locales now use one consistent
translation per concept.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
…Internet Settings (#1119) (#1122)

* fix(auth): unify USP login check so Remote Assistance can open Wi-Fi/Internet Settings (#1119)

In Remote Assistance mode the WASM USP client is pre-authorized via
authToken, so `usp.isAuthenticated` stays false by design. The Wi-Fi and
Internet Settings providers gated their fetch on that raw transport flag,
short-circuiting with "You are not signed in" even though the backend
serves data fine. The router already guards all /usp routes on loginType
(RA-aware) and 9 sibling feature providers have no such gate, making these
two gates both redundant and wrong.

- Add `AuthState.isRemoteAssistance` as the canonical login-intent check,
  replacing scattered `loginType == LoginType.remote` comparisons.
- Add `uspAuthReadyProvider` as the single source of truth for
  "USP layer is authorized to serve data" (RA || usp.isAuthenticated),
  documenting that usp.isAuthenticated is non-reactive.
- Remove the raw isAuthenticated fetch gate in the Wi-Fi and Internet
  Settings providers (keep the usp == null guard). This fixes #1119.
- Converge dashboard_orchestrator, router_provider and the RA session
  guard onto the new getter; the orchestrator keeps direct
  usp.isAuthenticated reads because it observes restoreSession flips.
- Tests: repurpose the internet "unauthenticated" test into an RA-bypass
  proof, add wifi RA-bypass + usp==null tests, and add unit tests for the
  new getter and provider.

Follow-up (out of scope): session_service.dart and sse_bootstrap still
read the raw flag but are off the RA render path.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(auth): drop unused uspAuthReadyProvider — auth stays a router concern (PR #1122 review W-1)

The provider was introduced as a "single source of truth" for USP auth
readiness but ended up with zero production consumers: wifi/internet fixed
#1119 by removing their fetch gate (relying on the router guard) rather than
routing through the provider, so it was dead code.

Keeping it would also push the auth concern back into page providers (each
page would ref.read it), which is the coupling we want to avoid. Auth is a
navigation-layer concern: the router already gates every /usp route on
loginType (RA-aware), so pages render data without an auth dependency — matching
the 9 sibling USP settings providers that never had a gate.

- Delete usp_auth_ready_provider.dart and its test.
- Drop the stale provider reference from the orchestrator comment.
- Keep AuthState.isRemoteAssistance: it is consumed by the router (the auth
  boundary) and by RA-strategy decisions in the orchestrator/session guard,
  not as a per-page login gate.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(auth): add AuthState.isLoggedIn and converge login checks (PR #1122 review W-4)

The app could identify the login *kind* (isRemoteAssistance) but had no
canonical "is the user logged in" check — that was scattered as inline
`loginType == LoginType.none` comparisons across 8 sites. Adding isLoggedIn
completes the pair: isLoggedIn answers "logged in?", isRemoteAssistance
answers "which kind?".

- Add AuthState.isLoggedIn => loginType != LoginType.none, named to avoid
  confusion with the transport-layer usp.isAuthenticated (WASM flag).
- Migrate the none-checks in app, router redirect, connection state, top bar,
  login view, root container and general settings widget to isLoggedIn.
- One router site (redirectLogic) keeps the loginType local because it reuses
  the enum value later, not just the logged-in boolean.
- Add isLoggedIn truth-table test.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(auth): clarify loginType is the source of truth, getters are shortcuts

Document that AuthState.loginType is the single three-state source of truth and
that isLoggedIn / isRemoteAssistance are derived named shortcuts, not a separate
mechanism — so readers know when to use the enum vs the booleans.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(auth): use const literal in fromJson test to clear analyzer info

Fixes the pre-existing prefer_const_literals_to_create_immutables hint on the
empty-map fromJson case, now that this file is touched by the auth getter tests.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…aseline (#1113) (#1123)

Bump ui_kit_library and generative_ui from v2.27.0 to v2.28.0. v2.28.0
adds AppLineChart.preventCurveOverShooting (default true) which constrains
cubic-bezier smoothing so the curved, filled line cannot dip below the
lowest data point. This fixes the Traffic Monitor card whose upload/download
curve could overshoot below the y=0 baseline in near-zero troughs.

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…#1025, #1038) (#1124)

* fix(wifi): hide DFS channels when DFS disabled; dedup channel parser (#1025, #1038)

when DFS (IEEE 802.11h) was disabled. Firmware leaves them in PossibleChannels
regardless of DFS state and TR-181 exposes no DFS-vs-non-DFS field, so the
client now classifies and filters DFS channels itself:
- WiFi Settings filters in the service (buildWifiNetworks), before computing
  per-bandwidth lists, so the dropdown and "N channels available" counts agree.
- Dashboard threads a per-radio isDfsEnabled flag onto WifiRadioUIModel and the
  channel dialog filters at display time.

single hardened parsePossibleChannels in lib/core/utils/wifi_channel.dart, and
converge both services onto it. The shared copy carries the hardened logic
(malformed-range guard + "0" sentinel filtering) as the baseline.

No-op guard: when the radio's current channel is a DFS channel that the
firmware left set while DFS is off, it is filtered out of the option list and
the editor opens on Auto. Confirming without moving the selection now compares
against the initial dropdown selection (not the radio's stored channel), so it
is correctly treated as a no-op and issues no mutation.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* fix(wifi): move radios off DFS channel when DFS is disabled (#1025)

When DFS (IEEE 802.11h) is disabled from the Advanced tab, a radio manually
parked on a DFS channel (e.g. 5 GHz ch 100) stays there — SSH-verified that
the firmware does not vacate the channel on its own, leaving the radio on an
illegal channel with no radar detection.

On save, when DFS is being turned off, any radio currently on a manual DFS
channel now also gets AutoChannelEnable=true in the same set() call, so the
firmware reselects a legal non-DFS channel. Radios already on auto-channel, on
non-DFS channels, or on non-5 GHz bands are untouched. Reuses isDfsChannel()
from wifi_channel.dart for the classification.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* refactor(wifi): address PR #1124 review — dedup path helper, parse set result, doc DFS contracts

Follow-up to the automated review on PR #1124.

- W-6: extract the 4 identical `_ensureTrailingDot`/`_withTrailingDot` copies
  (ethernet + 2 wifi services + advanced provider) into a single shared
  `ensureTrailingDot` in lib/core/utils/tr181_path.dart.
- W-3: parse the USP `set()` result in UspWifiAdvancedService.setIeee80211hEnabled
  via UspResultParser, throwing UspPartialFailureError / UspCompleteFailureError
  on partial/complete firmware rejection (mirrors the settings-service pattern).
  A partial rejection of the forced AutoChannelEnable write is no longer silently
  swallowed. Adds partial/complete-failure tests and updates the existing set()
  stubs to a success-shaped map.
- W-2: document that WifiRadioUIModel.possibleChannels is raw (DFS filtering
  happens at display time in WifiChannelDialog on the dashboard path).
- W-5: note that dfsChannels5GHz is the US/FCC (UNII-2A/2C) set — extend per
  regulatory domain if multi-market support is required.

W-1 (no-op guard not auto-correcting a pre-existing DFS-parked radio) is
intentionally not changed — it would contradict the deliberate "view-only Apply
must not trigger a write" design and reintroduce a spurious radio restart.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
…1126)

* feat(dashboard): show spinner on card toggle during mutation (#1055)

- Add isLoading param to ToggleRow and NetworkRow components
- Display CircularProgressIndicator in place of AppSwitch when loading
- Pass isLoading to WiFi Networks, DHCP Reservations, Port Forwarding cards
- Add missing wifi_networks card to Professional preset (17→18 cards)
- Update test expectation for Professional preset card count

* fix(dashboard): address PR review feedback for toggle spinner

- Replace CircularProgressIndicator with AppLoader from ui_kit_library
- Add instancePath null guard to DHCP delete button
- Add instancePath null guard to port forwarding/triggering toggles
- Disable share button during loading state
- Hoist ref.watch to build() in WiFi networks card
…1132)

* fix(pnp): distinguish router read failure from no-internet (#1098)

The PnP internet check collapsed two distinct outcomes into NoInternet:
a genuine "no internet" (WAN read succeeds, Status != 'Up') and a read
failure (USP GET returns empty / missing fields → WanStatus.fetch throws).
The two are already separated by control flow — return false vs throw — but
the catch block discarded that distinction, so "Try again" could never
recover while GETs stayed empty, and the no-internet troubleshooter options
(restart modem / enter ISP settings) were shown for a read failure where
they are meaningless.

Split the two paths: return false → NoInternet (unchanged); throw → new
AdminReadFailure phase, carrying the ServiceError code/detail for diagnostics
and rendered as a plain error card + retry on the entry view (no redirect to
the no-internet hub). Both startPostLoginFlow and _checkInternet now branch
on ServiceError, so a read failure at either the SystemInfo or WanStatus step
lands in the same phase. AdminReadFailure fully replaces the former AdminError.

* fix(pnp): route read failures back to entry from all check callers (#1098)

The classification fix landed AdminReadFailure at the notifier, but
_checkInternet() has multiple callers and only the entry view rendered the
new phase. The no-internet retry and the ISP-save (PPPoE / Static IP / DHCP)
flows had no branch for it, so a read failure there would stall silently.

Route every read-failure path back to the entry view (RoutePath.pnp). Because
/pnp and the no-internet subtree are independent top-level route trees, go()
mounts a fresh PnpEntryView whose initState re-runs startPostLoginFlow — an
implicit retry that settles on the read-failure card only if it still fails.

The ISP-save WRITE failure path is deliberately left on NoInternet + snackbar:
_checkInternet does not rethrow, so a trailing check read-failure is handled
inside it (AdminReadFailure), and only a genuine save-write failure reaches the
saveIspWithProgress catch — the user should stay on the form to fix it.

Also delete saveIspSettingsAndCheck (dead code, zero callers; superseded by
saveIspWithProgress) and add a regression test for the save-succeeds-but-check-
read-fails path.

* style(pnp): apply dart format to pnp_notifier_test
…14MB) (#1135)

* feat(fonts): bundle CJK/non-Latin subset fonts for offline rendering

Ship the interface-charset subset of Noto Sans CJK (SC/TC/HK/JP/KR) plus
full Thai/Arabic/Latin-ext and Roboto, declared eager under pubspec `fonts:`
so the CanvasKit fallback manager finds them locally and never probes the CDN.
CJK drops from 12.7MB to 2.14MB (84%) while every language keeps correct glyphs.

- assets/fonts/fallback/: 8 subset/full woff2 + Roboto (engine default fallback)
- pubspec: eager `fonts:` declarations (packages/ui_kit_library/* + bare Roboto),
  ui_kit bumped to v2.28.1 (adds injectable LocaleFallbackFont hook)
- lib/localization/fallback_font_resolver.dart: single source of the
  locale to family mapping; install() injects it into ui_kit at startup
- app.dart: add per-locale fallback to ThemeData.textTheme (covers raw Text)
- language_tile.dart: per-item Localizations.override so the picker renders
  every language's native name with the correct family
- flutter_bootstrap.js: fontFallbackBaseUrl stays on CDN so online, rare
  user-typed glyphs outside the subset are still fetched on demand

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(fonts): remove redundant full CJK engine-fallback fonts (~13MB)

These full Noto woff2 chunk sets were mirrored locally for CanvasKit's
engine-level fallback. They are now superseded by the eager-bundled subset
fonts (previous commit): interface text is covered by the subset, and rare
user-typed glyphs are filled from the CDN when online. Removing them saves
~13MB of source tree / product weight with no offline regression (verified:
all locales still render offline via the bundled subsets).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* chore(tools): add CJK subset font regeneration tooling

Build-time tool that regenerates the bundled CJK subset fonts from the current
interface charset. MUST be re-run after any change that adds CJK glyphs (ARB
strings, language names, hardcoded literals) — otherwise the subset silently
misses them (offline tofu / online CDN fetch).

- regenerate.sh: one-command pipeline (download full OTFs, extract charset,
  subset, deploy to assets/fonts/fallback/)
- extract_charset.py: unions ARB values + CJK punctuation blocks + picker
  native names + hardcoded CJK in Dart source
- make_test_page.py: renders per-locale samples for glyph-correctness eyeballing
- README documents the "when to re-run" maintenance rule
- .venv/full_fonts/out are reproducible intermediates (gitignored)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
#1138)

* fix(dashboard): hide link-local IPv6 in LAN Information widget (#1129)

The LAN Information widget surfaced the interface's link-local IPv6
address (fe80::/10, scope link) when the LAN bridge held only a
link-local address and no global/ULA prefix. A link-local address is
only valid on a single link and is not a meaningful LAN IPv6 address.

Filter fe80::/10 addresses at the source in
UspLanDataService._fetchLanIpv6Addresses(), so all consumers (dashboard
card, PDF report, AI section) render consistently. When IPv6 is enabled
but no meaningful address remains, the card now shows '-' (consistent
with the DNS field) instead of the link-local address or a bare
'Enabled' label.

Refs #1129

* test(local_network): add fec0::1 boundary test for IPv6 link-local filter (#1129)

W-4 review-fix: fec0::1 is the first address just above the fe80::/10
range (link-local spans fe80::-febf::). It is a deprecated site-local
address (RFC 3513), not link-local, so the filter must keep it. The
suite already covered febf::1 (top of range) but not fec0 (first
address outside), leaving the upper boundary of fe80::/10 untested.
…1128) (#1139)

* fix(internet-settings): show global IPv6 not link-local on WAN status (#1128)

The WAN Network Status widget displays ipv6Addresses.first. TR-181 returns
Device.IP.Interface.2.IPv6Address in instance order, where instance 1 is the
link-local fe80::/10 address, so the dashboard showed a non-routable fe80::
address instead of the global unicast WAN IPv6.

Add lib/core/utils/ipv6_address.dart to classify IPv6 scope (global / ULA /
link-local / other) and reorder addresses so global unicast surfaces first
(stable within scope, preserving instance order as tie-breaker). Apply in
UspWanDataService so ipv6Addresses.first is the routable address.

Refs #1128

* refactor(ipv6): extract shared byte-range predicates + add 6bone guard (review W-1, #1128)

Resolves review warning W-1 on PR #1139:

- Extract the IPv6 high-order byte-range checks into a single source of
  truth, lib/core/utils/ipv6_ranges.dart, and have both call sites
  (classifyIpv6Scope in ipv6_address.dart and IPv6WithReservedRule in
  validator_rules/rules.dart) import from it, eliminating the duplicated
  magic constants.
- Add the 3FFE::/16 (6bone, RFC 3701) and 5F00::/12..7F reserved-range
  exclusions to classifyIpv6Scope, matching the guards already in
  IPv6WithReservedRule. Previously 3FFE:: was misclassified as global
  unicast by the new helper.
- Add regression coverage for the reserved-range exclusions.

No behavior change to rules.dart (49/49 validator tests green);
ipv6_address tests 11/11 green.
* fix(wifi): use OWE token for Enhanced Open security mode (#1073)

The codebase used the string 'Enhanced-Open' for the Enhanced Open
(OWE) security mode, but firmware only accepts the TR-181 token 'OWE'
and rejects 'Enhanced-Open' as an invalid enumeration (verified on
device). This produced two latent app-side bugs, independent of the
firmware issue where OWE is accepted with success:true yet not
committed:

- 6 GHz open mode sent 'Enhanced-Open', which firmware rejects with
  "Out of range or invalid enumeration".
- OWE networks were never recognized as open, so the UI spuriously
  required a password (WifiQuickSetupSettings.isPasswordRequired) and
  blocked save.

Replace all three 'Enhanced-Open' occurrences with 'OWE':
- WifiNetworkUIModel.isOpenSecurity
- WifiQuickSetupSettings.isPasswordRequired
- _securityModeFor6GHz (6 GHz override + comment)

Fix the test fixture that asserted the old invalid token and add an
isOpenSecurity test group covering None / empty / OWE / WPA variants.

* test(wifi): cover 6 GHz OWE security override (#1073)

_securityModeFor6GHz had no direct test. Add a saveQuickSetup group
asserting the exact ModeEnabled value sent to firmware:
- 6 GHz + OWE / None (open) → 'OWE'
- 6 GHz + WPA3-Personal → 'WPA3-Personal'
- 2.4/5 GHz → selected mode written verbatim

Guards against regressing to the invalid 'Enhanced-Open' token.

* fix(wifi): apply 6 GHz security override in saveAdvanced (#1073)

saveAdvanced wrote curr.securityMode verbatim while saveQuickSetup routed
it through _securityModeFor6GHz. The two save paths could therefore send
different security modes to firmware, and on 6 GHz saveAdvanced could send
a non-WPA3 (invalid) mode.

- saveAdvanced: route securityModeEnabled through _securityModeFor6GHz
  (band from curr.band), matching saveQuickSetup. Still gated on
  securityChanged so enable-only toggles never re-write the mode.
- wifiDisplayValue: add an 'OWE' case returning 'Enhanced Open' so the
  status card and selector show the Wi-Fi standard label instead of the
  raw token (like WPA2/WPA3-Personal, it is a technical term, not l10n'd).
- tests: the 6 GHz "WPA2-Personal to WPA3-Personal" case passed
  'WPA3-Personal' as input, an identity that would pass even without the
  override; pass 'WPA2-Personal' to exercise the non-open to WPA3 coercion.
  Add a saveAdvanced 6 GHz override group, and use a distinct baseline mode
  so the security diff always fires.
…ge (#1146)

* fix(dashboard/detail): unify IPv6 link-local display with a scope badge

Across every view that surfaces an IPv6 address (Dashboard Network Status
& LAN Information cards, Device Detail, Node Detail), a link-local
(fe80::/10) address is now always shown but tagged with an Ipv6ScopeBadge
(public_off icon + tooltip) rather than hidden or dropped. Data services
keep every address and reorder so a globally routable one is preferred as
the representative value; the UI marks link-local instead of filtering it.

This supersedes the earlier filter/`-` behavior from #1128 (#1139) and
#1129 (#1138): a WAN/LAN with no global/ULA prefix now shows its
link-local address with the badge instead of a bare "-".

- ipv6_address.dart: add public isLinkLocalIpv6() (shared classifier)
- WAN/LAN data services: keep all addresses + preferGlobalIpv6First()
- Ipv6ScopeBadge + InfoGridItem.labelTrailing + DetailCopyableTile.leading
- Detail views swap the leading icon for the badge; cards tag the label
- add ipv6ScopeLinkLocal string across all 26 locales
- update WAN/LAN service tests for the keep-and-reorder behavior

Refs #1128 #1129

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* test(golden): add IPv6 link-local/global screenshot states

Screenshot-test coverage for the IPv6 scope badge across all four views that
surface an IPv6 address. Each view now generates screenshots for both a global
(no badge) and a link-local (badge) state, confirming the badge renders in the
correct state:

- dashboard cards (WAN Network Status, LAN Information): add link_local_ipv6
  state (existing online_dhcp/dhcp_enabled already cover global/ULA)
- Device Detail: add global_ipv6 + link_local_ipv6 states
- Node Detail: add global_ipv6 + link_local_ipv6 states

Baselines are regenerated by the screenshot suite (gitignored, not committed).
This also refreshes the node_detail slave_with_devices screenshot, whose
on-disk baseline predated the backhaul card.

Refs #1128 #1129

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 <noreply@anthropic.com>
@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 →

@AustinChangLinksys
AustinChangLinksys requested review from HankYuLinksys and PeterJhongLinksys and removed request for PeterJhongLinksys July 16, 2026 06:18
@AustinChangLinksys
AustinChangLinksys merged commit 5beff30 into usp Jul 16, 2026
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.

3 participants