chore: merge dev-2.6.0 to usp - #1147
Merged
Merged
Conversation
…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.
…uide + constitution
…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>
#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…
…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>
…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 reviews are paused for this user.Troubleshooting steps vary by plan Learn more → On a Teams plan? Using GitHub Enterprise Server, GitLab Self-Managed, or Bitbucket Data Center? |
AustinChangLinksys
requested review from
HankYuLinksys and
PeterJhongLinksys
and removed request for
PeterJhongLinksys
July 16, 2026 06:18
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Merge dev-2.6.0 to usp
Promotes the completed 2.6.0 work from
dev-2.6.0into theusp(2.x main) branch for release.dev-2.5.0Highlights
Verification
dart formatclean (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)uspHEAD is the merge-base of this diffDiff base:
dev-2.5.0…dev-2.6.0.Closes #1125