Skip to content

refactor(dhcp): centralize reservation device-option data source - #1109

Merged
AustinChangLinksys merged 2 commits into
dev-2.6.0from
peter/improve-dhcp-provider
Jul 9, 2026
Merged

refactor(dhcp): centralize reservation device-option data source#1109
AustinChangLinksys merged 2 commits into
dev-2.6.0from
peter/improve-dhcp-provider

Conversation

@PeterJhongLinksys

Copy link
Copy Markdown
Collaborator

Summary

Refactors the DHCP reservation cards so the client-device data lookup lives in
the notifier (data layer) rather than the views, and wires device autocomplete
options into the local-network add dialog. Also completes localization for two
duplicate-address validation strings and includes a small hooks fix.

Changes

  • Data/UI split: extract the cross-provider client-device read out of the
    reservation cards 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 — the
    cross-provider read stays in the notifier, the UI projection stays in the view.
  • Local-network dialog: pass device autocomplete options into the add
    reservation dialog (previously created without them).
  • l10n: add duplicateMacAddress / duplicateIpAddress across all 26
    locales (previously only en).
  • Hooks: use $CLAUDE_PROJECT_DIR absolute paths in the PreToolUse Bash
    hooks so they work regardless of the shell's working directory.

Testing

  • Added unit tests for deviceOptions(): mapping, name fallback
    (friendlyName → hostName → mac), mesh-node (master/slave) exclusion, empty case.
  • flutter analyze clean on changed files.
  • Affected functional tests pass (28 tests: notifier + dhcp_data_provider).
  • flutter gen-l10n succeeds; generated getters verified.

🤖 Generated with Claude Code

PeterJhongLinksys and others added 2 commits July 9, 2026 11:39
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>
…fier

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>
@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 left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

🤖 Automated Review — Round 1 · 9028ec6..6acf4e5 (full)

Verdict: ✅ APPROVE — Refactor direction is correct; 0 Critical. Six warnings flagged for author follow-up.

Conf. Where Issue (one-liner)
⚠️ High usp_dhcp_reservations_notifier.dart:185 [both reviewers] valueOrNull silently returns [] during AsyncLoading — autocomplete shows blank with no UX feedback
⚠️ High detail_card.dart:106 + card.dart:150 _buildDeviceOptions projection logic fully duplicated in two card widgets (DRY violation)
⚠️ High (no card test files) _buildDeviceOptions filter/mapping logic in cards has no widget or unit test coverage
⚠️ Med usp_dhcp_reservations_notifier.dart:13 DHCP AutoDispose L2 directly reads Devices L1 (devicesDataProvider) — cross-domain coupling
⚠️ Med usp_dhcp_reservations_notifier.dart:183 deviceOptions() mixes UI-hint concern into reservation-CRUD Notifier — single-responsibility tension
⚠️ Med lib/l10n/app_*.arb duplicateMacAddress/duplicateIpAddress were referenced in base but translations only arrive in this PR — base had broken l10n state
💡 High .claude/settings.json `cd ...
💡 Med usp_dhcp_reservations_notifier.dart:183 deviceOptions() name too generic; consider clientDeviceSuggestions()

Confidence: High = code-verified · Med = located + reasoned, not fully confirmed · Low = speculative, please double-check.
Items marked [both reviewers] were independently flagged by two agents → higher confidence.

⚠️ Warning Details

W1 — valueOrNull silent empty list during AsyncLoading High · [both reviewers]

  • usp_dhcp_reservations_notifier.dart:185
final devices =
    ref.read(devicesDataProvider).valueOrNull?.clientDevices ?? [];

devicesDataProvider is an AsyncNotifier; while it is in AsyncLoading (startup, SSE-triggered refetch), valueOrNull returns null and deviceOptions() silently returns []. The user opening the Add Reservation dialog will see blank MAC/IP autocomplete fields with no loading indicator.

Fix: In the card's _showAddDhcpDialog, check ref.read(devicesDataProvider).hasValue before opening the dialog and show a loading state if not; OR make deviceOptions() an async method that awaits devicesDataProvider.future.


W2 — _buildDeviceOptions duplicated in two cards High

  • lib/page/dhcp/views/components/usp_dhcp_reservations_detail_card.dart:106–127
  • lib/page/local_network/cards/usp_dhcp_reservations_card.dart:150–175

Both methods contain identical logic: call deviceOptions(), filter by mac.isNotEmpty / ip.isNotEmpty, map to AppAutoCompleteOption, return a ({mac, ip}) record. The PR centralised the data source but left the projection duplicated.

Fix: Extract a top-level function or extension (e.g., in lib/page/dhcp/views/) shared by both cards.


W3 — No tests for card-layer _buildDeviceOptions High

No widget or unit tests exist for the filter (mac.isNotEmpty, ip.isNotEmpty) or AppAutoCompleteOption mapping logic in either card. Constitution Article I §1.1 requires test coverage for all business logic.

Fix: Add widget tests (or unit tests extracting the helper) verifying: empty MAC excluded, empty IP excluded, correct label/value/subtitle mapping.


W4 — Cross-domain coupling: DHCP L2 reads Devices L1 Med

  • lib/page/dhcp/providers/usp_dhcp_reservations_notifier.dart:13
import 'package:privacy_gui/page/devices/providers/devices_data_provider.dart';

UspDhcpReservationsNotifier is an AutoDisposeNotifier in the DHCP domain. It now directly imports and ref.reads devicesDataProvider (Devices domain L1). While Article IV Rule 2 allows L2 to ref.read L1, the intent is intra-domain coordination. Cross-domain horizontal reads accumulate coupling over time.

Fix (preferred): Introduce a reservationDeviceOptionsProvider in a shared layer that reads devicesDataProvider and is consumed directly by the two cards — removing the cross-domain dependency from the Notifier entirely.


W5 — Single-responsibility tension in Notifier Med

  • lib/page/dhcp/providers/usp_dhcp_reservations_notifier.dart:183–196

UspDhcpReservationsNotifier core responsibility is DHCP reservation CRUD state. deviceOptions() provides UI autocomplete hints — a UI concern mixed into a state-management class. Views call ref.read(uspDhcpReservationsProvider.notifier).deviceOptions() directly, bypassing normal Provider state consumption.

Fix: Same as W4. If kept in the Notifier, add a doc comment marking it as a View-helper exception.


W6 — l10n translations missing from base branch Med

  • lib/l10n/app_*.arb (26 locale files)

dhcp_reservation_edit_dialog.dart at base SHA 9028ec6e (lines 120, 125, 137, 138) already references loc(context).duplicateMacAddress and loc(context).duplicateIpAddress. The ARB translations arrive only in this PR, meaning the base branch had broken l10n (missing generated getters) until now.

Fix: Ensure future PRs that add loc(context).xxx references include all ARB file updates atomically in the same commit.

✅ What looks good
  • Refactor direction: Moving device data reads out of View widgets into the Notifier is the right layering step; Views now consume a clean ReservationDeviceOption typedef instead of raw ClientDevice.
  • ReservationDeviceOption typedef: Clean named-record design with only the fields the View needs — good data encapsulation.
  • Test suite additions: Four new deviceOptions() tests are well-structured, covering happy path, fallback naming (hostName then mac), mesh-node exclusion (via clientDevices which already filters master/slave), and empty-device edge case.
  • l10n completeness: All 26 locale files updated consistently, inserted in alphabetically correct positions.
  • .claude/settings.json hook fixes: Using $CLAUDE_PROJECT_DIR for absolute paths is correct and portable.
  • No security or correctness issues found in the production diff.

Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.

@AustinChangLinksys
AustinChangLinksys merged commit fbe38ca into dev-2.6.0 Jul 9, 2026
2 checks passed
@AustinChangLinksys
AustinChangLinksys deleted the peter/improve-dhcp-provider branch July 9, 2026 05:25
AustinChangLinksys added a commit that referenced this pull request Jul 16, 2026
* docs(error-handling): consolidate guides + align constitution + fix shared-helper localization (#997)

* docs(error-handling): add existing research and planning docs as baseline

Snapshot the six error-handling & localization docs before reorganizing them
into a best-practices guide. Committing first so subsequent deletions are
diff-trackable.

* docs(error-handling): strip general hardcoded-strings content

Narrow the docs to error-handling scope only, ahead of writing the
error-handling best-practices guide:

- Delete 05 (844-entry general hardcoded-strings tracking table) — no
  error-handling content.
- Slim 03 down to error-relevant parts only: i18n framework decision
  (no slang migration), ServiceError diagnostic-field groundwork, and
  the "Service-layer text is not localized" criterion. Removed the
  ~460-entry general string audit (sections 1/2/4/5/7 + appendix).
- README: update 03/04 entries and status board (04 done via PR #953,
  drop the "non-error strings" follow-up line).
- 04: drop the two bullets pointing at non-error hardcoded strings.

* docs(error-handling): consolidate into reference + implementation guide

Reorganize the error-handling docs into the two intended deliverables:

- NEW error-handling-implementation-guide.md — the "how": per-layer
  (Service/Provider/View) patterns for implementing error handling in a
  USP feature, what to show vs. hide, localization, gotchas, and a
  pre-PR checklist. All examples verified against the current codebase
  (post PR #953), notably the two fetch-display patterns (ServiceErrorView
  for state.error pages, _buildError+localizeServiceError for AsyncValue
  pages) and the try/catch save path.
- RENAME 01-usp-error-roundtrip-reference.md → usp-error-handling-reference.md
  (the "why": full round-trip background). Drop its section 4 (localization
  plan, now implemented) and refresh the progress notes.
- DELETE 02/03/04 — their content is absorbed into the two docs above.
- README: index just the two docs with a "how vs. why" reading guide.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(error-handling): reframe in reference voice, not progress-tracker

These are reference docs, not a sprint board. Drop "done/todo/current
work" status language in favor of describing the state of things:

- Describe where implementations live (PR #953) rather than marking
  items "completed".
- Correct the reference's section 3, which still described the
  pre-PR#953 state as the present (Provider stringifies '$e', View has
  no mapper). Reframe those as "pain points before PR #953" that
  motivated the refactor; point to the implementation guide for how the
  code reads now.
- Update the flow diagram's Provider/View boxes to current behavior.
- Phrase the GET 9999->9998 bug and the missing contract test as known
  issues / TODO, not checkboxes.
- Drop volatile count snapshots ("36 guards") in favor of the rule.

* fix(l10n): localize error in performUspMutation shared helper

The shared dashboard-card mutation helper showed failures with a raw
`'Error: $e'`, bypassing the central `localizeServiceError` mapper. This
slipped past PR #953 because the raw string lived inside the helper, not
at the call sites the audit grepped for.

Route the caught error through `localizeServiceError` like the feature
views do. This brings all 8 cards that use `performUspMutation`
(internet_settings renew lease, local_network reservations, admin time,
port_forwarding, wifi ×2, devices) into the localized error pipeline in
one change.

Verified: flutter analyze clean; internet_settings suite (127 tests)
passes; no test asserted the old string.

* docs(error-handling): cover performUspMutation + fix two inaccuracies

- Add §3.3: dashboard cards trigger mutations via the shared
  performUspMutation helper, which now localizes failures internally —
  framed as a convenience entry point, NOT a third localization strategy.
  Note successMessage is shown as-is (caller must pass a loc()'d string).
  Add matching PR-checklist line.
- Fix two claims found during codebase verification:
  - The _localizeFaultCode <-> _mapProtocolError sync reminder is
    one-directional in code; reworded accordingly.
  - The save-snackbar example showed only showFailedSnackBar; noted the
    ScaffoldMessenger+SnackBar variant some pages use — the API is
    secondary, the string must come from localizeServiceError.

* docs(constitution): align Article XIII with post-PR#953 error handling

Update the error-handling articles to match the current codebase and add
the missing UI-layer principle. Keep it principle-level; details point to
the implementation guide.

- §13.2: ServiceError now carries diagnostic code/detail; drop the
  deleted OTP/admin subtype examples; note code/detail are diagnostic-only.
- §13.4.2: performFetch stores the typed `error: e`, not `errorMessage: '$e'`.
- §13.4.1: use an existing subtype (InvalidInputError) in the example.
- §3.3.5: error classes extend the sealed ServiceError (no AuthError).
- §13.1: UI layer localizes via the central localizeServiceError mapper.
- Add §13.6 UI Layer Error Display — states the principle and links to
  doc/error-handling-localization/error-handling-implementation-guide.md.
- Bump Last Amended to 2026-06-29 (version unchanged).

* fix(l10n): localize success messages in card mutations

The 7 hardcoded English successMessage strings passed to performUspMutation
(and showRecoveryDialog) were the success-side counterpart to the error
leak already fixed — all on dashboard cards / shell, missed by PR #953.

- Reuse existing parametrized key for DHCP renew: leaseRenewed('DHCP').
- Add 6 new keys (reservationAdded, reservationDeleted, reconnectedToRouter,
  timeSettingsSaved, ruleAdded, channelUpdated), translated across all 26
  locales. Translations follow each locale's existing style anchors
  (reservationReleased, wifiSettingsSaved, timezoneUpdated…) and noun usage
  (e.g. zh 信道 / zh_TW 通道 / ja チャネル for "channel"; per-locale "router").
- Update the 7 call sites to loc(context).xxx.

Verified: 26/26 locales carry each key, all arb files valid JSON,
flutter analyze clean, touched-feature tests pass (498).

* docs(error-handling): translate the three docs to English

The committed/shared docs must be English. Translate the README, the
implementation guide, and the round-trip reference from Traditional
Chinese to English in place.

Pure translation — code blocks, identifiers, file paths, fault codes,
ASCII diagrams, links, and section structure are preserved byte-for-byte;
only prose was translated. Verified: 0 CJK characters remain, code fences
balanced, sibling links resolve.

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(dashboard): resolve card clipping and add mascot outside dismiss

- Remove ClipRect from dashboard cards to prevent shadow/border truncation
- Add dismissible barrier to all mascot interactive dialogs
- Bump ui_kit_library to v2.26.0

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

* feat(components): add optional secondary action to ServiceErrorView

Add `secondaryLabel` + `onSecondary` (both optional, null by default) so a
page that cannot load can offer an escape hatch (e.g. "Log out") below the
retry button. Behavior is unchanged when not provided. Covered by two new
widget tests.

* refactor(l10n): migrate AsyncValue error pages to shared ServiceErrorView

Replace the per-page private `_buildError` / inline error widgets on the
AsyncValue (AsyncNotifier) pages with the shared `ServiceErrorView`,
narrowing `Object error` via `error is ServiceError ? error : null`.
This unifies fetch-failure display across both page architectures
(state.error pages already used ServiceErrorView).

- system_log, instant_privacy, admin: straight swap (retry = ref.invalidate)
- dashboard: retry = notifier.refreshAll(); keeps its "Log out" escape hatch
  via the new ServiceErrorView secondary action
- topology: previously an inline error with a hardcoded `unableToLoadTopology`
  title and no localized detail — now gets the localized detail for free.
  Removed the now-orphaned `unableToLoadTopology` key from all 26 locales.

Verified: flutter analyze clean on changed files; affected non-golden tests
pass; all 26 .arb files valid JSON.

* refactor(l10n): migrate diagnostics error pages to ServiceErrorView; localize apps error

Continue the AsyncValue error-display migration (rounds 2-3 of the 9 targets):

- speed_test_view, diagnostic_manual_tools_view, usp_speed_test_card:
  replace private error widgets (which showed `error.toString()` or a
  hardcoded key) with the shared `ServiceErrorView`, narrowing
  `Object error` via `error is ServiceError ? error : null`. The diagnostics
  providers throw ServiceError/TimeoutError, so failures now localize.
- Removed the now-orphaned keys: unableToLoadSpeedTest,
  unableToLoadDiagnostics, errorLoadingSpeedTest (all 26 locales).

apps page is intentionally NOT migrated: it fetches lighttpd static JSON
(not USP/TR-181) and throws plain `Exception`, not `ServiceError`, so
`ServiceErrorView` would only show a generic title. It keeps its own error
widget but no longer surfaces the raw exception — shows the localized
`unableToLoadApps` instead. Also aligned `unableToLoadApps` to the plural
"Apps" brand word for locales that use that spelling (en/da/de/es_ar/nl/pt/it/zh_TW).

Verified: flutter analyze clean on changed files; diagnostics + apps tests
pass (234); all 26 .arb valid JSON; no dangling key references.

* docs(error-handling): unify AsyncValue pages on ServiceErrorView in guide + constitution

* fix(l10n): address PR review — error titles, compact card, missed call-sites

Round-1 review fixes for the ServiceErrorView migration:

- ServiceErrorView: add optional `title` (defaults to neutral errorUnexpected)
  so non-settings pages no longer show "Failed to load settings"; add assert
  for paired secondary action; unwrap onSecondary via local promotion. (C-1/W-1/W-3)
- Restore the 4 error-title ARB keys (topology/speedTest/diagnostics) to their
  original positions across 26 locales; every ServiceErrorView caller now
  passes a context-appropriate title. (C-1)
- speed_test_card: revert to a compact card-sized error widget instead of the
  full-page ServiceErrorView (avoids DashboardCardTemplate overflow); localizes
  via localizeServiceError; drop now-orphaned errorLoadingSpeedTest. (C-2)
- device_list: raw '$e' → ServiceErrorView (unableToGatherDeviceInfo). (C-3)
- health_status_view: hardcoded English → new localized unableToLoadHealthData
  (26 locales), kept as the existing inline widget (not full-page). (C-4)
- l10n polish: fix unableToLoadApps capitalization to each locale's in-sentence
  convention (de stays capitalized); align pt/ja/th/fr terminology.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(error-handling): sync implementation guide with review fixes (title param, compact widgets)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* feat(wifi): add channel dropdown to edit dialog (#1023) (#1027)

* feat(wifi): add channel dropdown to edit dialog (#1023)

Replace manual channel number entry (AppTextField) with a dropdown
(AppDropdown) in the Dashboard WiFi Status edit-channel dialog.

- Enrich WifiRadioUIModel with possibleChannels (populated at dashboard
  fetch by UspWifiDataService, so the dialog renders synchronously with
  no per-dialog fetch/loading/error state).
- Dialog offers Auto (recommended) + the band's possible channels; Auto
  switch and dropdown stay consistent; DFS channels annotated.
- Add 4 ARB keys across all 26 locales.
- Unit tests for model, data-service enrichment, and dialog behaviour.

* fix(wifi): lock channel dropdown in Auto mode + always show current channel (#1023)

* fix(wifi): normalize band for DFS + filter invalid channels from PossibleChannels (#1023)

* refactor(wifi): drop redundant IgnorePointer, bump UI-kit v2.26.0->v2.26.1 (#1023)

UI-kit v2.26.1 gates the AppDropdown tap gesture when onChanged is null
(app_dropdown.dart:138,183), so the consumer-side IgnorePointer workaround
added for upstream privacyGUI-UI-kit#2 is now redundant. Remove it and rely
on onChanged==null to disable the control. Fix#1 interaction tests converted
from widget-tree (IgnorePointer.ignoring) to behavior (onChanged null/menu
does not open) assertions; all 16 tests pass.

* fix(dashboard): unify card rows + navigation fixes (#1017)

* fix(dashboard): unify card rows + navigation fixes (#1014, #1012, #1009, #1002)

- Add ToggleRow, NetworkRow, ProtocolBadge components to row_blocks.dart
- Refactor Port Forwarding card to use ToggleRow + ProtocolBadge
- Refactor DHCP Reservations card to use ToggleRow + DeviceRow
- Refactor WiFi Networks card to use NetworkRow component
- Add View Details navigation: Topology card → topology, Device Info → node detail
- Add Statistics page tab parameter for System Status/Traffic Analysis cards
- Remove "off" option from polling interval cards (#1012)
- Remove "admin" username from password card (#1009)
- Fix uptime not updating by including uptimeSeconds in SystemSnapshot
- Fix Network Diagnostics back button returning to dashboard (#1002)
  - Move diagnostics route as child of menu route
  - Use context.pop() instead of goNamed for proper back navigation

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

* fix: address PR review feedback

- Guard instancePath null in DHCP reservation toggle (W-1)
- Add canPop() guard before pop() in diagnostics view (W-3)
- Guard empty deviceId in device info card footer (W-4)
- Replace native widgets with UI Kit components in row_blocks (W-6)
  - _GuestBadge → AppBadge
  - _ShareButton → AppIconButton
  - ProtocolBadge → AppBadge

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

---------

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

* fix(dashboard): consistent device counts excluding mesh nodes (#1020, #1022, #1024)

Issues fixed:
- #1020: Dashboard "Devices" stat now uses clientDevices (excludes routers)
- #1022: Device Analytics excludes mesh nodes from counts and activity heatmap
- #1024: Network Topology signal indicator now uses 1:1 RSSI→LinkQuality mapping
        consistent with UspSignalStrengthIndicator (getWifiSignalLevel SSoT)

Key changes:
- Use clientDevices instead of deviceModels across dashboard, mascot triggers,
  PDF export, and feature dropdowns (port forwarding, DHCP, IPv6 port service)
- Add serial number scoping to analytics persistence to prevent data mixing
- Filter router MACs from persisted history on load (legacy data cleanup)
- Fix _rssiToLinkQuality() mapping: good→good, fair→fair, poor→unknown

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

* test(topology): add defensive test for RSSI→LinkQuality SSoT consistency (#1024)

Ensures UspTopologyBuilder's LinkQuality mapping stays in sync with
getWifiSignalLevel() from wifi.dart. Tests all RSSI boundary values
to catch future mapping drift that caused #1024.

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

* fix(dashboard): child node client signal + trend Y-axis (#1043, #1044)

#1044: Clients connected to child mesh nodes now show signal strength.
- Add `clientSignalMap` to MeshTopologyInfo (MAC → RSSI from DataElements)
- MeshTopologyBuilder extracts STA.SignalStrength (RCPI→RSSI conversion)
- Use as fallback in _toDeviceUIModel when WifiClients has no data

#1043: Trend chart Y-axis no longer shows duplicate numbers.
- Add explicit yAxis with calculated max and interval
- Ensures clean labels when device count is small (e.g., 0, 1, 2)

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

* fix(analytics): address review feedback for device analytics (#1053)

- Guard _persistState() against race condition: don't persist before
  _historyLoaded is set (avoids writing to legacy key when _serialNumber
  is still null)
- Reset instance state (_historyLoaded, _serialNumber) in build() to
  handle provider invalidation correctly
- Use isMeshNode getter instead of raw deviceRole string comparison in
  _getRouterMacs() for SSoT consistency
- Use isClientDevice getter instead of raw deviceRole string comparison
  in usp_pdf_service.dart for SSoT consistency

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

* style: dart format usp_device_analytics_notifier.dart

* fix(analytics): set _historyLoaded=true on load failure to unblock persistence (#1053)

W-NEW-1: _loadPersistedHistory() only set _historyLoaded=true inside the try
block. Any exception (SharedPreferences cold-start race, corrupted JSON, etc.)
left it false, and the PR's new '_persistState() { if (!_historyLoaded) return; }'
guard then silently dropped every subsequent write for the provider's lifetime.
Set the flag in the catch block so a one-off load failure no longer permanently
gates persistence.

Refs #1053

* feat(auth): replace password storage with session token persistence

Security improvement: password is no longer stored locally. Instead,
session token is persisted in sessionStorage (cleared on browser close)
and used for session restoration via refreshToken(token?) API.

Key changes:
- Add UspTokenStorage with Web (sessionStorage) and stub implementations
- UspAuthCoordinator.restoreSession() uses token-only strategy
- Add reloginWithNewPassword() for admin password change flow
- Add isRecovering flag to suppress force logout during recovery
- Remove localPassword from AuthState
- Update WASM client to usp-client v0.12.0 with refreshToken(token?)

Test coverage: 47 tests for UspAuthCoordinator including:
- reloginWithNewPassword flow and error handling
- Null UspClient guards for all public methods
- Token persistence and restoration scenarios

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

* fix(auth): address review feedback for session token persistence

- dashboard_orchestrator: use isRecovering: true to prevent double
  navigation (restoreSession's onForceLogout + NotAuthenticatedError)
- usp_token_storage_web: add logging for storage failures to aid
  debugging in private browsing or quota-exceeded scenarios

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

* style: format dashboard_orchestrator.dart

* fix(auth): handle relogin failure after password change (#1013)

Address Hank's review feedback:

- #2: Separate error handling for updatePassword and reloginWithNewPassword.
  If password change succeeds but relogin fails, trigger logout instead of
  reporting "update failed" — the password IS changed, user just needs to
  re-enter it.

- #3: Add comment in auth_provider.init() explaining the init order
  dependency with onForceLogout callback (currently safe but relies on
  sseManagerProvider not being watched yet).

- #4: Add test for relogin failure scenario to verify logout is triggered.

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

* chore: bump version to 2.6.0

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

* fix(wifi): unify guest WiFi detection on canonical alias rule

Guest WiFi detection was implemented three different ways across the app,
which disagreed with each other and caused the guest card to be
misclassified as main (wrong title, hidden password/security fields).

- Add shared helper `isGuestSsid` in _shared/utils/wifi_guest_detection.dart
  as the single source of truth: an SSID is guest when its TR-181 Alias
  ends with `-guest`.
- Converge all three call sites on the helper:
  - usp_wifi_settings_service: replace inline alias check
  - usp_wifi_data_service: drop per-radio instance-ordering + dead
    _ssidInstanceIndex helper
  - pnp_service: drop radio-occupancy heuristic
- wifi_network_card: allow guest networks to edit security mode too,
  consistent with the Quick Setup card.
- Tests: give shared WiFi test data proper `-guest` aliases; add guest
  detection assertions to data service and pnp service tests.

* fix(wifi): warn when no SSID matches the -guest alias rule

Address PR review: alias-based guest detection fails silently when
firmware omits or doesn't follow the `-guest` alias convention (all
SSIDs fall back to main). Emit a warning log — including the observed
aliases — when multiple SSIDs exist but none match, so the failure is
diagnosable on-device. Detection strategy is unchanged.

* fix(wifi): write both SSID.Enable and AccessPoint.Enable for network toggle (#971, #972)

Problem:
- WiFi Settings page only wrote SSID.Enable when toggling networks
- Dashboard WiFi Status card wrote Radio.Enable (different layer)
- This caused inconsistency between Dashboard and Settings (#971)
- SSID.Enable alone did not stop AP broadcasting (#972)

Solution:
- Write both SSID.Enable and AccessPoint.Enable together for all
  network enable/disable operations
- Remove radio-level toggle from Dashboard WiFi Status card (now
  read-only status display)
- Dashboard WiFi Networks card uses toggleSsidsByName (per-SSID-name)
- WiFi Settings uses saveQuickSetup/saveAdvanced (per-network)
- PnP Guest WiFi uses same dual-layer write pattern

Changes:
- codegen: Add writable flag to AccessPoint.Enable in YAML
- WifiAccessPointUIModel: Add ssidInstancePath, accessPointInstancePath
- usp_wifi_settings_service: All save/toggle methods write both layers
- usp_wifi_status_card: Remove AP row toggle, keep as status display
- pnp_service: Add _throwIfNotSuccess checks, write AP.Enable for guest
- Remove dead code: toggleNetwork (replaced by toggleSsidsByName)

Tested: Firmware correctly sets Status=Down/Disabled when both layers
are written, and WiFi scanner confirms SSID disappears.

* fix(wifi): ensure L1 cache invalidation on partial failure

- Move wifiDataProvider read inside withLock() in toggleSsidsByName to
  avoid TOCTOU race with concurrent mutations
- Wrap ref.refresh/invalidate in finally blocks to ensure L1 cache is
  always refreshed even when mutations partially fail, keeping UI in
  sync with firmware state
- Apply same fix to updateRadioChannel for consistency

* fix(web): move validation to unfocus to prevent TextField focus loss (#1059) (#1099)

On Flutter Web, calling setState during onChanged causes widget tree
changes that break TextField's TextInputConnection. This manifests as:
- Auto-unfocus when validation error appears/disappears
- Cannot delete all text (one character remains)

Fix: trigger validation on unfocus instead of onChange for affected pages:
- Instant Privacy (Add MAC dialog)
- DHCP Reservation (Edit dialog)
- Local Network (all IPv4 fields)
- DMZ (Destination IP)

For AppIpv4TextField, use onFocusChanged callback which only fires
(null, false) when focus leaves the entire field, not between segments.

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

* fix(dhcp): reject duplicate MAC/IP in reservation dialog (#1070) (#1078)

* fix(dhcp): reject duplicate MAC/IP in reservation dialog (#1070)

The DHCP reservation Add/Edit dialog validated only MAC format, IPv4
format, and the reserved-IP rule; it never compared the entered MAC/IP
against existing reservations, so a duplicate was accepted and sent as a
USP ADD request (backend accepts it).

Add duplicate detection to the dialog's _validate(): the caller now
passes the current reservation list via existingReservations, and the
entered MAC (case-insensitive) / IP is rejected if it collides with any
other reservation. When editing, the reservation being edited is
excluded so it can keep its own address. Wire both detail-card callers
(add + edit) to supply the list, and add duplicateMacAddress /
duplicateIpAddress l10n keys.

* fix(dhcp): exclude self by stable instancePath in reservation duplicate check

The DHCP reservation edit dialog excluded the edited reservation from the
duplicate MAC/IP check via Equatable value-equality (r != widget.reservation).
Because DhcpReservationUIModel.props includes the non-key 'enable' field, an
SSE-driven re-fetch that toggles the edited entry's enable flag while the
dialog is open makes value-equality fail to match self, so the user's own
unchanged MAC/IP is falsely flagged as a duplicate and Save is disabled.

Exclude self by stable instancePath identity instead; fall back to
identical() for not-yet-saved local reservations (null instancePath).

Adds a regression test reproducing the SSE enable-drift scenario (fails on
the old value-equality filter, passes with the identity-based fix).

* fix(dhcp): validate reservations added from Dashboard card (#1067) (#1077)

* fix(dhcp): validate reservations added from Dashboard card (#1067)

The Dashboard DHCP card invoked the unvalidated DhcpReservationDialog,
which only silently no-op'd on empty MAC/IP and applied no format
validation. Malformed/empty reservations were therefore accepted and
persisted via AddInstance.

Point the Dashboard card at the existing DhcpReservationEditDialog
(already used by the DHCP detail page), which enforces MAC-address and
IPv4 format rules plus reserved-IP checks and disables the Add button
until the form is valid. Remove the now-orphaned DhcpReservationDialog.

Refs #1067

* chore: trigger CI (empty commit)

Re-trigger pull_request CI for #1077 (base changed to dev-2.6.0 did not fire sync).
No file changes.

* refactor(dhcp): centralize reservation device-option data source (#1109)

* fix(hooks): use absolute paths in PreToolUse hooks

The PreToolUse Bash hooks used a relative path (.claude/hooks/pr_gate.py)
and relied on the shell's working directory. When the working directory
changed (e.g. to a subdirectory), the hook could no longer locate the
script and blocked all Bash commands.

Use $CLAUDE_PROJECT_DIR to resolve the pr_gate.py path, and cd into the
project root in the dart-format hook so staged file paths resolve
correctly regardless of the current working directory.

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

* refactor(dhcp): move device option data source into reservations notifier

Extract the client-device data lookup out of the reservation cards and
into UspDhcpReservationsNotifier.deviceOptions(), which returns pure data
(ReservationDeviceOption records) instead of UI Kit types. The cards now
map that data to AppAutoCompleteOption at the call site, keeping the
cross-provider read in the notifier and the UI projection in the view.

- Wire device autocomplete options into the local-network add dialog.
- Add duplicateMacAddress / duplicateIpAddress strings across all 26
  locales.
- Add unit tests for deviceOptions() (mapping, name fallback, mesh-node
  exclusion, empty case).

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

---------

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

* feat(internet): support PPTP, L2TP, and Bridge WAN connection types (#1094)

* chore: bump version to 2.5.0 and add test report generation

- Update version from 2.4.0 to 2.5.0 in pubspec.yaml
- Add --report flag to run_tests.sh for markdown test report output
- Add standalone tools/test_report.sh for generating categorized reports

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

* test(firmware-update): add golden tests for all firmware update states

Add comprehensive golden test coverage for the firmware update page,
covering all 16 visual states across phone and desktop viewports.

Firmware Update View states (12):
- idle_no_file, idle_file_selected
- picking, validating, uploading
- triggering, installing, rebooting, verifying
- done, failed, banks_empty

Recovery Dialog states (4):
- waiting_initial, waiting_unreachable
- wifi_warning, serial_mismatch

Files added:
- mock_firmware_update.dart: Provider overrides for golden tests
- firmware_update_test_data.dart: Test fixtures and state builders
- firmware_update_view_test.dart: Main view golden tests
- firmware_recovery_dialog_test.dart: Recovery dialog golden tests

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

* feat(skill): add golden test coverage check to review-pr-readiness

Add Phase 4.5 to review-pr-readiness skill for checking golden test
coverage when View files are changed. The new checks include:

- 4.5.1: Golden test file existence for changed views
- 4.5.2: Golden test freshness when views are modified
- 4.5.3: Deep analysis of view states vs golden test coverage
- 4.5.4: Mock and fixture file existence
- 4.5.5: Optional golden test execution verification

This ensures PR reviewers are prompted to add/update golden tests
when visual changes are made, maintaining screenshot test coverage.

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

* refactor(pnp): unify troubleshoot flow UI patterns and behavior (#907)

Review pass over the PnP no-internet troubleshoot flow (no-internet hub,
unplug-modem, modem-lights-off, waiting-modem, isp selection, pppoe,
static IP). Establishes consistent layout patterns and fixes save-flow
behavior gaps so the troubleshoot pages feel like the rest of the app.

Layout — onboarding/wizard pages
- Wrap content in `withSliver` + `Center` + `ConstrainedBox(maxWidth: 480)`
  + `EdgeInsets.all(xl)` so cards stop stretching across desktop viewports.
- Replace sticky `UiKitBottomBarConfig` with inline `AppButton.primary`
  at the bottom of the column — onboarding flows are linear, the next
  action belongs in content flow, not a sticky save bar.
- Standardize illustration width (160px) and crossAxisAlignment.

Layout — full-screen status overlays (saving / countdown / checking)
- Switch from `withSliver` to plain `UiKitPageView` with
  `useMainPadding: false`, then wrap content in `Center`. `withSliver`
  collapses children to intrinsic height (so MainAxisAlignment.center has
  no room) and `useMainPadding: true` applies grid pageMargin (which
  pushes overlays off-center on wide screens).

Save flow
- Extract `PnpIspSavingProgress` shared widget so DHCP, PPPoE, and Static
  IP all show the same three-step progress UI.
- Add proper localization keys for the save-step labels (previously
  borrowed unrelated strings like "Save" and the ISP-type page title).
- Surface save failures via SnackBar — `errorMessage` was being written
  to state but no view consumed it.
- Disable the form Save button until required fields are filled.
- Show button loading state on the no-internet "Try again" button.

Save responsibility boundary
- Convert `PnpIspSettingsView` to `ConsumerStatefulWidget`. DHCP save is
  driven by a local `_dhcpSaving` flag and a one-shot post-await phase
  read; the page no longer keeps a `ref.listen` on the global PnP phase.
  This prevents the parent index page from reacting to save outcomes
  triggered by its child form pages (PPPoE / Static IP), which would
  otherwise cause double-fired SnackBars and unnecessary rebuilds.

* feat(firmware-update): add OTA firmware update support (#917)

* feat(firmware-update): add OTA firmware update support

Add cloud API integration to check for available firmware updates and
trigger OTA download directly to router. OTA and local manual upload
share the same flow from FirmwareImage.Download() onwards (flash →
reboot → verify).

- Add FirmwareOtaCheckService for cloud API integration
- Add FirmwareOtaInfo model for API response parsing
- Add checkingOta phase and OTA state fields
- Add OTA check UI card with "Check for Updates" button
- Add triggerOtaDownload() for remote firmware URL

* test(firmware-update): add tests for OTA update functionality

- Add FirmwareOtaInfo model tests (JSON parsing, toQueryParams)
- Add FirmwareOtaCheckService tests (HTTP calls, error handling)
- Add triggerOtaDownload service tests
- Add checkForOtaUpdate and triggerOtaInstall notifier tests

* refactor(firmware-update): move OTA param building to notifier layer

Address code review feedback:
- Fix: Remove PII (MAC/IP) from log by only logging URI path
- Fix: Move OTA check param building logic from View to Notifier
- buildOtaCheckParams(), _formatMacAddress(), _parseHardwareVersion()
- View now only calls notifier methods, no business logic

* fix(firmware-update):

1. Make releaseDate nullable instead of using DateTime.now() fallback
   - Prevents non-deterministic behavior in Equatable comparison
   - null semantics correctly represent "not provided"

2. Add clearOtaInfo flag to FirmwareUpdateState.copyWith()
   - Allows resetting otaInfo to null when needed
   - Pattern: copyWith(clearOtaInfo: true)

* chore: upgrade Flutter 3.38.5 → 3.44.0 and dependencies (#914)

* chore: upgrade Flutter 3.38.5 → 3.44.0

- Update .fvmrc to pin Flutter 3.44.0 (Dart 3.12.0)
- Update vendored CanvasKit for offline web deployment

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

* chore(deps): remove unused packages and upgrade for SPM support

Remove unused packages:
- connectivity_plus (not used in codebase)
- network_info_plus (not used in codebase)
- flutter_local_notifications (not used in codebase)

Upgrade packages for Swift Package Manager support:
- flutter_secure_storage: 9.2.2 → 10.3.1
- device_info_plus: 9.1.2 → 11.1.0
- package_info_plus: 4.1.0 → 8.1.0
- share_plus: 7.1.0 → 10.1.0
- printing: 5.13.1 → 5.14.3

SPM warnings reduced from 11 to 4.

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

* chore(deps): upgrade go_router 14.2.8 → 17.0.0

No breaking changes affecting current codebase.
All 2630 tests pass.

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

* chore(deps): remove unused permission_handler

- Remove lib/util/permission.dart (Permissions mixin never used)
- Remove permission_handler dependency from pubspec.yaml

SPM warnings reduced from 4 to 3.

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

* chore(deps): upgrade ui_kit_library v2.20.0 → v2.21.1

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

* feat(mascot): integrate mascot overlay with dashboard (#922)

* feat(mascot): integrate mascot overlay with random speech

- Add mascot integration to USP dashboard shell
- Implement DashboardDialogProvider with FAQ, diagnostics, print report
- Add random speech timer (10-30s interval, auto-hide after 5s)
- Add mascot toggle in GeneralSettingsWidget
- Redesign GeneralSettingsWidget layout (unified row height, AppSwitch)
- Fix ThemeModeTile to use dialog selection pattern
- Fix Theme Studio persistence with keepAlive
- Fix popup dismiss behavior with TapRegion groupId
- Show mascot only after dashboard data is ready
- Upgrade ui_kit_library v2.21.1 → v2.23.1

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

* refactor(mascot): simplify coordinator and add unit tests

- Rename mascotRandomSpeechProvider → mascotCoordinatorProvider
- Move startup logic from shell to MascotCoordinatorNotifier.build()
- Remove complex ref.listen/Future.microtask from shell
- Delete unused network_health_score.dart
- Add dashboard_dialog_provider_test.dart (13 tests)
- Add mascot_coordinator_notifier_test.dart (4 tests)

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

---------

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

---------

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

* feat: mesh topology enhancement with layout system refactor (#915)

* feat(codegen): update usp-codegen with resolveBy and regenerate .g.dart files

- Update tools/usp-codegen binary to v0.15.2
- Add resolveBy feature for dynamic WAN/LAN interface resolution via Alias
- Fix absolute path handling bug (DHCP paths no longer incorrectly prefixed)
- Add new MeshNode backhaul fields: BackhaulDeviceID, BackhaulMACAddress,
  LinkType, MACAddress, LastDataDownlinkRate
- Regenerate all .g.dart files with new codegen
- Update mesh_topology_builder_test.dart for new MeshNode fields

Closes #909

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

* feat(topology): mesh topology enhancement phase 2 with diagnostics integration

- Consolidate RSSI thresholds to single source (wifi.dart)
- Merge wifi_performance_helpers.dart into wifi.dart/wifi_ui.dart
- Add DataElements enrichment fields to NodeUIModel for backhaul diagnostics
- Add mesh backhaul check to unified diagnostics service
- Fix signal strength display with text labels and proper units
- Change diagnostic results from GridView to Wrap for flexible height
- Use dialog instead of bottom sheet for diagnostic details (desktop UX)
- Update stale threshold from 5 to 10 minutes
- Update codegen to v0.15.3 with resolveBy fix for updateOrdered
- Fix test mocks for _resolveInstance get calls

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

* refactor(ui): unify speed formatting and enhance backhaul card design

- Consolidate speed formatting to NetworkUtils.formatSpeed/formatSpeedWithUnit
  - Single source of truth for kbps → human-readable conversion
  - Gbps: 2 decimal places, Mbps: 0 decimals, kbps: no decimals
- Update DetailSpeedCard to use speedKbps parameter (TR-181 standard unit)
- Enhance BackhaulSignalIndicator with visual bar design matching Device Detail
- Fix PHY Rate display to use unified formatting (Mbps → Gbps when applicable)
- Remove duplicate formatSpeed from wifi.dart and node_detail_popup.dart
- Fix rate field comments (was incorrectly documented as bps, actually kbps)

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

* fix(ui): unify upload speed card color to tertiary

Device Detail was using secondary for upload while Node Detail used
tertiary. Unified to tertiary (upload) and primary (download) across
both views for visual consistency.

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

* refactor(ui): introduce layout blocks system and unify dashboard cards

- Create reusable layout blocks library (lib/page/_shared/components/layout_blocks/)
  - Block: universal base wrapper with consistent background styling
  - CardHeader: fixed 36px height for consistent title alignment
  - StatusBlock, AlertBanner: status indicators
  - InfoGrid, InfoList, ListPreview: data display blocks
  - HighlightValue, DualMetric, StatTile: metric blocks
  - NetworkRow, DeviceRow, DataRow, StatusRow: row blocks
  - ProgressBlock, QuotaBlock, RangeBlock: data visualization blocks

- Redesign all dashboard cards using Block-based layout patterns:
  - Hero block + metric tiles + InfoGrid design pattern
  - Consistent visual styling across all cards
  - All progress bars now use UI Kit AppLoader (linear variant)

- Fix multi-interface device detection in Ethernet ports card
  - Now correctly shows wired connections from devices with both WiFi and Ethernet

- Update dashboard presets and widget specs for proper card sizing

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

* refactor(ui): apply Block layout pattern across all feature pages

Consistently apply Block component pattern throughout the application
for unified visual hierarchy and semantic grouping within AppCard
containers. Also update tests to match model changes from prior sessions.

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

* chore: remove design showcase page and routes

The Block layout pattern has been applied across all feature pages,
so the showcase page is no longer needed for development reference.

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

* refactor(layout-blocks): consolidate design system and remove unused components

- Add BlockConstants for unified alpha, padding, borderRadius values
- Extract SwitchBlock, SettingBlock, NavLinkBlock, FormFieldBlock
- Remove unused: StatusBlock, AlertBanner, IpAddressBlock, ProgressBlock,
  QuotaBlock, RangeBlock, HighlightValue, DualMetric, VersionBlock,
  ComparisonBlock, ListPreview, NetworkRow, DataRow, StatusRow,
  ToggleListItem, SplitRow, SectionDivider, CountBadge
- Apply SwitchBlock to Firewall view (removes _switchRow helper)
- Apply SettingBlock to WiFi Network Card (removes _SettingBlock)

Reduces layout_blocks from 8 files (~850 lines) to 6 files (~400 lines)
while adding reusable patterns for common UI elements.

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

* docs(constitution): add Article XIV Layout Composition Patterns

Define project-level layout conventions that complement UI Kit:
- Block pattern: visual grouping container (surfaceContainerHighest @ 50%)
- Three usage patterns: Card+Block, Block alone, Card alone
- Shared components: SwitchBlock, SettingBlock, NavLinkBlock, DeviceRow
- Implementation rules and file organization

Renumber UI Kit Library Principle to Article XV.

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

* feat(dashboard): adjust traffic monitor timer to Off/10s/30s/60s

Change the Traffic Monitor refresh interval options from Off/2s/5s/10s
to Off/10s/30s/60s, with 10s as the new default.

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

* refactor(DeviceRow): use AppListTile from UI Kit

Replace custom Row/Container layout with AppListTile to comply with
UI Kit First principle. Preserves icon container styling via leading
parameter.

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

* refactor(architecture): fix constitution compliance and clean up core layer

Article XIII compliance:
- Add error mapping to PnpService (6 methods)
- Create DiagnosticsScopeService to wrap NetworkDiagnosticsExecutor
- Remove usp_error.dart imports from provider layer (speed_test_notifier,
  manual_tools_notifier)

SSoT fixes:
- Consolidate MeshBackhaulSeverityBucket into MeshBackhaulSeverity enum
- Remove switch conversion in unified_diagnostics_notifier

Core layer cleanup (remove Flutter Material imports):
- Move device_classifier.dart to lib/page/_shared/utils/
- Move recovery_dialog_helper.dart to lib/page/_shared/helpers/
- Extract DeviceConnectionTypeExt to lib/page/_shared/extensions/

Test updates:
- Move device_classifier_test.dart to test/page/_shared/utils/
- Add DiagnosticsScopeService unit tests (20 test cases)
- Update test imports and enum references

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

* fix(review): address PR #915 review comments

- Fix speed row UX: show only available directions instead of '--'
- Extract shared MetricTile to layout_blocks (remove duplication)
- Refactor NetworkBadgeWidget to use AppBadge from UI Kit
  (icon support pending #916)

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

* refactor(layout-blocks): rename Block to LayoutBlock

Avoid name collision with go_router.Block

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

---------

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

* feat(usp): update WASM client with UspClientBuilder support

Add builder pattern for creating UspClient with custom configuration:
- authToken(): set Bearer token (skip login flow)
- endpoint(): set custom USP endpoint path
- extraHeader(): add custom HTTP headers
- build(): create UspClient instance

Required for Remote Assistance integration via Guardian API.

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

* feat(usp): add Remote Assistance POC via Guardian proxy

Add support for Remote Assistance mode that allows remote control of
router via Guardian API using temporary access token.

Changes:
- Add UspClientBuilderJS WASM binding for builder pattern
- Add UspClientWeb.fromJsClient() and UspClient.fromBuilder() factories
- Add RemoteAssistanceProvider for RA state management
- Add RemoteAssistanceConfirmView for token input UI
- Add /remoteAssistance route with ?sessionId query param
- Add URL detection: /?ra_session=xxx redirects to RA confirm page

Usage:
1. Navigate to http://localhost:5000/?ra_session=test-session-123
2. Enter temporary access token on confirm page
3. Click Connect to initialize Guardian-proxied USP client
4. Dashboard loads with USP operations routed through Guardian

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

* feat(build): add force=remote build mode for Remote Assistance

- Add ForceCommand.remote enum value
- Add BuildConfig.isRemote() helper
- Redirect to /remoteAssistance when force=remote is set

Usage: flutter run --dart-define force=remote
Or use VSCode "linksys - Web (Remote)" launch config.

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

* feat(config): add GlobalConfig with ThemeConfig integration

- Rename feature_flags.json to app_config.json with new structure
- Add ThemeConfig to GlobalConfig for CI/CD theme configuration
- Extract ThemeSource enum to separate file to avoid circular import
- ThemeConfigLoader now reads from GlobalConfig.theme if configured
- Add app_config.json.template with full schema documentation

The theme section in app_config.json is optional - when absent,
ThemeConfigLoader falls back to dart-define environment variables.

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

* fix(usp): add conditional export for UspClientBuilderJS

- Create usp_client_builder.dart as platform-agnostic entry point
- Add usp_client_builder_stub.dart for VM/tests
- Add usp_client_builder_web.dart to re-export from WASM
- Fix test failures caused by unconditional WASM import

The previous direct export of UspClientBuilderJS from usp_client_wasm.dart
caused dart:js_interop to be imported on non-web platforms, breaking tests.

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

* test: update tests for auth check architecture change

- Remove isAuthenticated getter tests from service tests
- Update notifier tests to use appConnectionStateProvider for auth check
- Remove obsolete unauthenticated service test from internet settings

Auth checks moved from Service layer (isAuthenticated getter) to
Provider layer (appConnectionStateProvider). Services are now stateless
and trust the upper layer handles auth before navigation.

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

* feat(remote): complete Remote Assistance mode implementation

Remote Assistance mode allows support agents to view router status
through Guardian proxy without direct network access.

Changes:
- Add RemoteAccessProvider with sessionStorage persistence for refresh
- Add GlobalConfig.remote for centralized UI/feature restrictions
- Skip SSE in Remote mode (Guardian proxy limitation)
- Fix router redirect loop after Connect
- Use fixed remote preset layout for Dashboard
- Add topology card to remote preset
- Remove isAuthenticated checks from Services (moved to Provider layer)
- Simplify General Settings in Remote mode (hide Legal, Logout)
- Add RemoteSessionChip for session info display

Known limitations:
- Operate-based diagnostics (Ping/Traceroute) don't work in Remote mode
  due to SSE dependency for OperationComplete events

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

* fix(remote): improve Remote Assistance UX

- Change query param key from 'sessionId' to 'session'
- Show expiry time instead of countdown in popup (fixed value)
- Add session polling every 30s to sync remaining time with server
- Fix End Session redirect to show session ended view
- Use go() instead of goNamed() to clear navigation history

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

* fix(remote): fix End Session navigation race condition

- Capture GoRouter before async gap to avoid context unmount issue
- Delay logout() until after navigation completes via postFrameCallback
- Clean up unused cloud_const.dart constants (30+ unused entries removed)
- Add guardianDomain constant for future domain migration

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

* feat(dashboard): unify card templates and enhance AI assistant (#931)

* refactor(dashboard): unify card layout with DashboardCardTemplate

Extract common card structure (header, scrollable content, footer) into
a reusable template supporting three modes:
- Single content: standard cards
- Multi-section: composite cards (DHCP, Port Forwarding)
- Tabbed: cards with tab navigation (System Status, Analytics)

Migrated 18 dashboard cards to use the template, reducing ~225 lines
of duplicated layout code while ensuring consistent visual appearance.

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

* feat(ai): add modular Section architecture and new AI commands

Router AI Assistant enhancements:

## Modular Section Architecture
- Add 9 domain sections: WanSection, LanSection, WifiSection, DevicesSection,
  SystemSection, FirewallSection, EthernetSection, DhcpSection, PortForwardingSection
- Add 2 advanced sections: TopologySection (with tap-to-popup), DiagnosticsSection
- Add 3 chart sections: LineChartSection, BarChartSection, PieChartSection
- Add utilities: SectionHeader, AiInfoRow, AppDivider
- Total: 37 components (16 data sections, 9 legacy cards, 12 basic)

## New AI Commands (15 total)
- getSystemInfo, getConnectedDevices, getWifiSettings, getWanStatus
- getNetworkOverview, getLanInfo, getDhcpInfo, getEthernetPorts
- getFirewallStatus, getPortForwarding, getTimeSettings
- getTrafficStats (with history for charts)
- getSystemMonitor (CPU/Memory history)
- getDeviceAnalytics (device distribution stats)
- getWifiStatus (Tx power, bit rate, channel, bandwidth per radio)

## TopologySection Features
- Tap-to-show-details popup with MAC, IP, signal, speed
- Animation enabled via theme override
- Supports extenders and clients with metadata

## Infrastructure
- RouterChatController with A2UI v0.9 protocol support
- UspCommandProvider reads from L1 dashboard providers
- ComponentCatalog with sync tests for registry/prompt alignment
- System prompt caching support (~80-90% token savings)

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

* fix: update ComponentBuilder import for ui_kit 2.25.0 compatibility

Add generative_ui import for ComponentBuilder type which is now
exported from gen_ui_contracts instead of ui_kit_library directly.

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

* fix(mascot): hide dismiss button for random idle messages

Random speech bubbles now use showDismissButton: false so users
can only dismiss them by tapping the bubble or waiting for autoHide.

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

* feat(mascot): add dynamic message provider with L2 architecture

Introduce MascotMessageProvider for generating context-aware random messages:

## Message Categories
- **Guidance** (20%): Feature discovery tips (WiFi settings, diagnostics, etc.)
- **Status** (50%): Dynamic system state (CPU, memory, devices, mesh, WAN)
- **Tips** (30%): Network security and knowledge sharing

## Architecture
- `mascot_message_templates.dart`: Template definitions with conditions
- `mascot_message_provider.dart`: L2 Provider reading from L1 data providers
- Templates use `MascotMessageContext` for dynamic text generation
- Conditional templates only show when their condition is met

## Data Sources (L1 Providers)
- systemInfoDataProvider: CPU%, Memory%, uptime
- devicesDataProvider: online/total count, mesh nodes
- wifiDataProvider: radio enabled count
- wanDataProvider: connection status

## Extensibility
- Add new templates: just add to the corresponding List
- Add new category: add enum, create List, update weights
- Add new context field: extend MascotMessageContext, read in _buildContext()

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

* refactor(cards): integrate layout_blocks primitives with DashboardCardTemplate

Merge both design systems:
- Keep DashboardCardTemplate as outer wrapper (header/footer/scroll)
- Use layout_blocks primitives inside content (LayoutBlock, MetricTile, InfoGrid, etc.)

Files updated: 12 dashboard cards across admin, dashboard, devices, firewall,
internet_settings, local_network, port_forwarding, topology modules.

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

* fix(devices-card): remove nested Expanded in scrollable content

DashboardCardTemplate already wraps content in Expanded + ScrollView,
so the device list shouldn't add another Expanded + SingleChildScrollView.

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

* style: format usp_ethernet_ports_card.dart

* fix: address code review findings

Critical fixes:
- Fix type safety: buildRouterContext now accepts WidgetRef instead of dynamic
- Fix password exposure: use full masking ('********') instead of partial

Major fixes:
- Remove debug prints from TopologySection.build()
- Add Semantics wrapper to DashboardCardTemplate footer link for accessibility
- Add documentation for intentional Navigator.push usage in mascot animation

Tech debt:
- Remove unused onViewAll parameter from UspConnectedDevicesCard
- Remove unused onViewAll parameter from UspWifiNetworksCard

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

* fix(ai): use ProviderReader type for buildRouterContext

Changed buildRouterContext to accept a ProviderReader function type
instead of WidgetRef, allowing both WidgetRef.read and ProviderContainer.read
to be passed. This fixes test compatibility while maintaining type safety.

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

* fix(ai): update buildRouterContext call to use ref.read

Pass ref.read function to match ProviderReader type signature.

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

* refactor(ai): use UI Kit components in router_assistant_view dialogs

- Replace Text with AppText in confirmation and config dialogs
- Replace TextButton/FilledButton with AppButton.text/AppButton.primary
- Add unit tests for routerCommandProviderProvider

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

* refactor(ai): replace debugPrint with logger.d for release exclusion

Use project logger instead of debugPrint to ensure AI debug logs
are excluded from release builds and properly masked for sensitive data.

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

---------

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

* test(remote): add unit tests for Remote Assistance feature

- Add RemoteAssistanceService tests (19 tests)
- Add RemoteAccessNotifier tests (20 tests)
- Add RemoteAssistanceNotifier tests (18 tests)
- Add RemoteClientNotifier tests (22 tests)

Also:
- Add poll failure tracking with hasPollError state
- Add 15s timeout to session validation API call
- Extract magic numbers to named constants
- Add lint ignore reason comment

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

* fix(cloud): remove /cloud prefix from Guardian RA endpoints

Guardian API endpoints don't use the /cloud prefix.

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

* refactor(ui): use AppSurface and AppText in RemoteSessionChip

- Replace Container with AppSurface for theme-aware styling
- Replace raw Text with AppText.labelMedium/labelSmall
- Use semantic colors (urgency indicated by text/icon color, not background)

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

* feat: golden test framework consolidation and HTML report tooling (#925)

* docs: add golden test verification report design spec

Design spec for automated HTML report generation after golden test
verification runs. Covers report structure, failure image comparison,
coverage scanning, and self-contained HTML output.

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

* docs: add golden test verification report implementation plan

Six-task plan covering: test result parser enhancement, coverage
scanning, HTML template rewrite, verify script creation, snapshot
script simplification, and end-to-end smoke testing.

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

* feat: extract failure image paths in test result parser

Add extractFailureImages() to parse golden test failure messages and
extract expected/actual/diff image paths into a failureImages field.

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

* feat: add coverage scanning and --embed flag to combine_results

Scans lib/page/*/views/usp_*_view.dart against test/usp_test/page/*/
to calculate golden test coverage. The --embed flag converts failure
images to base64 data URIs for self-contained CI artifacts.

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

* feat: rewrite HTML report template with modern UI

Self-contained HTML with embedded CSS/JS. Includes donut chart,
coverage panel, filter bar, collapsible feature groups, and
three-way image comparison for failures. Supports dark mode.

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

* feat: add run_golden_verify.sh for verification-mode testing

Runs golden tests without --update-goldens, parses results, and
generates an HTML report with pass/fail stats, failure image
comparison, and coverage analysis. Supports --embed for CI artifacts.

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

* refactor: remove report generation from snapshot update script

Report generation is now handled exclusively by run_golden_verify.sh.
The update script focuses solely on regenerating golden baseline files.

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

* fix: add FVM detection, remove --tags=loc, handle empty test results

- Add FVM detection to run_golden_verify.sh (consistent with run_tests.sh)
- Remove --tags=loc since golden tests don't use tag annotations
- Use test/usp_test/ directory path for full-mode test targeting
- Fix test_result_parser.dart crash on empty results (use fold instead
  of reduce, handle null suites/result gracefully)

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

* fix: update extractInfo regex for new golden test name format

The new golden framework produces test names like:
"viewName - state - device - locale (variant: macOS)"
instead of the legacy format. Add new regex pattern matching first,
fall back to legacy format for backwards compatibility.

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

* fix: resolve smoke test issues with failure image extraction

- Add || true to test_result_parser calls (parser exits 1 on failures,
  which would halt the script under set -e)
- Move extractFailureImages to onDone phase (test metadata like tsName
  isn't populated during message events)
- Add Strategy 2: infer failure image paths from test metadata when
  Alchemist doesn't include paths in error messages
- Fix testCaseFilePath leading slash for relative path resolution

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

* fix: prepend ../ to failure image paths for correct report resolution

Report lives in snapshots/ subdirectory, so failure image paths
(relative to project root) need ../ prefix to resolve correctly
when viewing the HTML report in a browser.

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

* feat: support --dart-define locale/screen override in golden runner

Allow command-line control of which locales and screen sizes to run
via --dart-define=locales and --dart-define=screens, without requiring
changes to individual test files.

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

* chore: remove unused verify_golden_coverage.sh

Coverage scanning is already handled by scanCoverage() in
combine_results.dart. This script was never referenced by any
CI workflow.

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

* feat: consolidate golden tests to test/golden_test/ with gallery report

- Move golden tests from test/usp_test/ to test/golden_test/
- Add generate_gallery_report.dart for visual golden gallery
- Simplify run_generate_loc_snapshots.sh (fvm detection, remove snapshots/ copy)
- Update run_golden_verify.sh to output report in test/golden_test/
- Fix combine_results.dart json filter and relative path logic
- Fix test_result_parser.dart to write output alongside input

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

* feat: add coverage ignore list for views without golden tests

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

* feat: add lightbox, comparison view, and thumbnail sizing to gallery report

- Lightbox with keyboard navigation (←/→/Esc) and section position indicator
- Compare mode: same state side-by-side across locales for quick l10n review
- Thumbnail size toggle (S/M/L) for adjustable grid density

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

* feat: improve golden framework stability and report UX

- Replace naive 5×pump loop with pumpAndSettle + timeout fallback
- Add precacheImages config for views with async asset images
- Add search, lightbox, overlay slider to verify report
- Add search box and Components device grouping to gallery report
- Add golden test report usage guide
- Update spec to document new settle/precache mechanisms

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

* fix: replace hardcoded find.text with locale-independent finders

Interaction steps using find.text('English string') fail in non-English
locales. Replace with find.byType(Tab).at(index), find.byType(AppButton),
find.byIcon, etc. Also fix _resolveDevices to not override custom device
configs, and add finder rules to spec.

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

* feat: add overflow error detection and reporting to golden tests

Collect RenderFlex overflow warnings during golden test execution and
write them to goldens/overflow_warnings.json. Both gallery and verify
reports now display overflow badges and support overflow-only filtering.

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

* feat: enhance report UI with filters, zoom, back-to-top, and fix page heights

- Add select all/none toggle for feature, locale, and device filters
- Use CSS grid layout for filter groups to prevent overlap
- Add fixed back-to-top button (visible after 400px scroll)
- Add lightbox zoom with scroll-wheel zoom and drag-to-pan
- Increase golden test heights for admin, device_list, dhcp, dashboard,
  unified_diagnostics, menu, and statistics pages
- Update clear_goldens.sh to only delete PNGs and clear overflow artifacts

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

* chore: consolidate golden tests under test/golden_test/ and fix doc naming

- Move firmware_update tests from test/usp_test/ to test/golden_test/page/
- Update all path references in golden_test_specification.md
- Rename golden-test-report-guide.md to golden_test_report_guide.md

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

* feat: replace checkbox filters with chip-style toggles in report UI

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

* style: format golden_runner.dart

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

* fix: propagate test failure exit code in run_golden_verify.sh

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

* fix: remove set -e to ensure verify report is always generated

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

* fix: replace hardcoded find.text with locale-independent finders in wifi_settings

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

* Fix format

---------

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

* refactor(cloud): replace LinksysCloudRepository with GuardianApiClient

- Create GuardianApiClient as single point for Guardian API calls
- Remove LinksysCloudRepository (only Remote Assistance was using it)
- Delete 7 unused service files (asset, auth, device, event, ping, smart_device, user)
- Delete 10 unused model files (cloud_account, cloud_phone, etc.)
- Clean up ~30 unused constants from cloud_const.dart
- Update RemoteAssistanceService to use GuardianApiClient
- Update tests to mock GuardianApiClient

Files removed: 18
Lines removed: ~2500

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

* feat(connection): auto-detect unexpected disconnection and enter wait-for-recovery (#932)

* feat(connection): auto-detect unexpected disconnection and enter wait-for-recovery

When SSE reconnection fails 2 consecutive times (indicating the device
has likely moved out of router range), automatically enter the
wait-for-recovery flow instead of waiting for all 5 retries to exhaust.
Recovery probe takes over with lightweight health checks every 10s,
and shows a modal dialog informing the user of the disconnection.

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

* fix(connection): skip redundant enterWaiting when shell shows natural recovery dialog

The auto-detect path already transitions to waitingForRecovery via
_onSseReconnectFailed; the shell listener only needs to display the
dialog without re-entering the state.

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

* chore(skill): use fvm dart format in review-pr-readiness skill

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

* fix(connection): address code review findings from PR #932

- Add reentrancy guard in _scheduleReconnect after onReconnectFailed
  callback to prevent timer/state mutation after intentional disconnect
- Add state check + try/finally in _showNaturalRecoveryDialog to prevent
  stuck dialog on race condition or exception
- Clear _recoveryContext on recovered, serialMismatch, and exitToLogout
  so the public getter accurately reflects "null when not in recovery"

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

* fix(connection): update recovery_dialog_helper import path after merge

The file was moved from lib/core/connection/helpers/ to
lib/page/_shared/helpers/ in dev-2.5.0; update the import in
usp_dashboard_shell.dart accordingly.

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

* refactor(connection): address PR review suggestions

- Move state check before setting _recoveryDialogShowing flag for
  more intuitive flow in…
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants