Skip to content

fix(devices): MeshNetwork architecture + fix #1043 #1044 #1047 #1048#1068

Open
AustinChangLinksys wants to merge 9 commits into
dev-2.6.0from
fix/device-analytics-display-1043-1044
Open

fix(devices): MeshNetwork architecture + fix #1043 #1044 #1047 #1048#1068
AustinChangLinksys wants to merge 9 commits into
dev-2.6.0from
fix/device-analytics-display-1043-1044

Conversation

@AustinChangLinksys

@AustinChangLinksys AustinChangLinksys commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator

Summary

Changes

New Architecture (Phase 1-5)

  • MeshNetwork container with MasterNode, SlaveNode (sealed), ClientDevice
  • MeshNetworkBuilder — unified builder from ConnectedDevices + DataElements
  • Removed legacy DeviceUIModel / NodeUIModel after migrating all consumers

Key Files

  • lib/page/_shared/models/mesh_network.dart — SSoT container
  • lib/page/_shared/models/node_entity.dart — sealed MasterNode/SlaveNode
  • lib/page/_shared/models/client_device.dart — with parentNodeName field
  • lib/page/_shared/utils/mesh_network_builder.dart — builder logic
  • lib/page/dashboard/views/components/usp_device_analytics_card.dart — Y-axis fix

WiFi Performance Card Enhancement

  • Use meshNetwork.allClients as data source (includes slave node clients)
  • Add clientBandSsidMap to MeshTopologyInfo for slave client band/SSID resolution
  • Add buildBssidToBandMap() to resolve BSSID → band via SSID LowerLayers → Radio
  • Truncate long client names in Speed tab chart to prevent label overlap

Signal Resolution

  • WiFi clients on master: from WiFi.AccessPoint.*.AssociatedDevice
  • WiFi clients on slave: from DataElements.Network.Device.*.Radio.*.BSS.*.STA.SignalStrength (RCPI→RSSI)

Test plan

  • Dashboard Device Analytics card — Trend tab Y-axis shows proper intervals
  • Dashboard Device Analytics card — Signal tab shows correct distribution
  • Connected Devices card — parent node name displays correctly
  • Topology page — nodes and clients render correctly
  • WiFi Performance card — Signal/Speed/Channels tabs show slave node clients
  • WiFi Performance card — Speed tab labels don't overlap
  • flutter analyze passes
  • ./run_tests.sh passes (3067 tests)

Related Issues

Closes #1043, #1044, #1047, #1048

🤖 Generated with Claude Code

AustinChangLinksys and others added 4 commits July 3, 2026 08:40
…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>
- 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>
#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>
Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
@qodo-code-review

Copy link
Copy Markdown

PR Summary by Qodo

fix(devices): MeshNetwork SSoT architecture + fix Y-axis & child node signal

✨ Enhancement 🐞 Bug fix 🕐 40+ Minutes

Grey Divider

AI Description

• Introduce MeshNetwork sealed-class SSoT for nodes and clients, replacing legacy UI models
• Build mesh topology via MeshNetworkBuilder using ConnectedDevices + DataElements signal mapping
• Fix #1043 Trend Y-axis duplicate labels with robust interval calculation
• Fix #1044 child-node client signal resolution via DataElements clientSignalMap fallback
Diagram

graph TD
  MTB["MeshTopologyBuilder"] --> MTI["MeshTopologyInfo\n(clientToNodeMap, clientSignalMap)"] --> MNB["MeshNetworkBuilder"] --> MNW["MeshNetwork (SSoT)"]
  UDDS["UspDevicesDataService"] --> MNB --> MNW --> DDP["devicesDataProvider\n(DevicesData.meshNetwork)"]
  DDP --> Topology["Topology\n(UspTopologyBuilder/Card/View)"]
  DDP --> Analytics["Device Analytics\n(Notifier/Card)"]
  DDP --> PDF["PDF Report\n(PdfReportData/UspPdfService)"]
  DDP --> AI["AI Context\n(UspCommandProvider)"]
  MNW --> Node["NodeEntity\n(MasterNode/SlaveNode)"]
  MNW --> Client["ClientDevice"]

  subgraph Legend
    direction LR
    _b(["Builder/Service"]) ~~~ _m["Model"] ~~~ _ui["UI/Consumer"]
  end
Loading
High-Level Assessment

The following are alternative approaches to this PR:

1. Introduce MeshNetwork behind a compatibility layer (dual-write)
  • ➕ Lower short-term regression risk by keeping existing DeviceUIModel/NodeUIModel consumers intact
  • ➕ Easier rollback path if issues appear in production
  • ➖ Prolongs dual-model maintenance and increases complexity
  • ➖ Can cause inconsistency if both representations diverge
2. Adopt a generated DTO layer for DataElements/Hosts normalization
  • ➕ Centralizes parsing/mapping logic and reduces ad-hoc conversions (e.g., RCPI→RSSI)
  • ➕ Potentially reusable across other feature areas
  • ➖ More upfront work and new layer to maintain
  • ➖ May be overkill if MeshNetworkBuilder is sufficient

Recommendation: Overall strategy (sealed-class MeshNetwork SSoT) is solid and aligns with reducing duplicated/fragile UI model mapping. Main follow-up: device_credentials_provider.dart appears to regress by setting deviceUUID to master.deviceId as a fallback; verify remote assistance requirements and restore the true Hosts UUID if needed.

Files changed (29) +2128 / -3053

Enhancement (8) +1439 / -22
network_entity.dartAdd shared NetworkEntity abstraction for nodes + clients +22/-0

Add shared NetworkEntity abstraction for nodes + clients

• Introduces a common interface for identity, display name, online state, and IP addressing. Used as the base for NodeEntity and ClientDevice to standardize downstream handling.

lib/page/_shared/models/network_entity.dart

node_entity.dartAdd sealed NodeEntity with MasterNode/SlaveNode +314/-0

Add sealed NodeEntity with MasterNode/SlaveNode

• Defines the new node hierarchy: MasterNode (gateway) and SlaveNode (extender) with BackhaulInfo. Provides list helpers (master/slaves/hasMesh) for consumer convenience.

lib/page/_shared/models/node_entity.dart

backhaul_info.dartIntroduce BackhaulInfo value object +77/-0

Introduce BackhaulInfo value object

• Adds a dedicated immutable container for backhaul metadata (link type/media type, rates, RSSI, parent identifiers). Consolidates previously scattered node backhaul fields.

lib/page/_shared/models/backhaul_info.dart

wifi_connection_info.dartIntroduce WifiConnectionInfo value object +73/-0

Introduce WifiConnectionInfo value object

• Adds a dedicated WiFi info container including RSSI, band, SSID, and rates. Provides normalized signal quality and level calculations used by analytics UI.

lib/page/_shared/models/wifi_connection_info.dart

client_device.dartAdd ClientDevice model (multi-interface + parent node metadata) +302/-0

Add ClientDevice model (multi-interface + parent node metadata)

• Introduces ClientDevice as the new client representation with WifiConnectionInfo, connection type, multi-interface support, and parentNodeId/parentNodeName for mesh attribution. Replaces legacy DeviceUIModel usage for clients across the app.

lib/page/_shared/models/client_device.dart

mesh_network.dartAdd MeshNetwork SSoT container with lookups and stats +132/-0

Add MeshNetwork SSoT container with lookups and stats

• Creates the top-level mesh container holding master, slaves, and unassigned clients, with helper methods (findNode/findClient, clientsByNode, counts). Serves as the single source of truth for consumers.

lib/page/_shared/models/mesh_network.dart

mesh_network_builder.dartAdd MeshNetworkBuilder to unify Hosts + DataElements into SSoT +476/-0

Add MeshNetworkBuilder to unify Hosts + DataElements into SSoT

• Implements the unified builder: separates nodes vs clients, groups multi-interface clients, assigns clients to nodes via clientToNodeMap, patches parentNodeName, and uses clientSignalMap as signal fallback for child-node clients.

lib/page/_shared/utils/mesh_network_builder.dart

usp_device_analytics_notifier.dartCompute analytics distribution from ClientDevice categories +43/-22

Compute analytics distribution from ClientDevice categories

• Uses DevicesData.clientDevices (excluding mesh nodes) and introduces categorization that groups child-node clients by parentNodeName. Updates signal quality aggregation to match the new Signal tab approach.

lib/page/_shared/providers/usp_device_analytics_notifier.dart

Bug fix (3) +277 / -256
mesh_topology_info.dartExtend MeshTopologyInfo with NodeEntity + clientSignalMap +19/-4

Extend MeshTopologyInfo with NodeEntity + clientSignalMap

• Migrates nodes to NodeEntity and adds clientSignalMap (STA signal strength) populated from DataElements. Enables accurate signal data for clients on slave nodes (fix #1044).

lib/page/_shared/models/mesh_topology_info.dart

mesh_topology_builder.dartPopulate clientSignalMap and build NodeEntity from DataElements +60/-38

Populate clientSignalMap and build NodeEntity from DataElements

• Updates parsing to create MasterNode/SlaveNode and to capture STA.SignalStrength per client, converting RCPI→RSSI. Improves backhaul metrics mapping including downlink rate.

lib/page/_shared/utils/mesh_topology_builder.dart

usp_device_analytics_card.dartRework analytics UI and fix Trend Y-axis interval (#1043) +198/-214

Rework analytics UI and fix Trend Y-axis interval (#1043)

• Reorders tabs to Overview/Signal/Trend/Activity, adds signal distribution bar chart, and updates overview donut. Trend switches to a line chart and computes yMax/yInterval to avoid duplicated Y labels for small device counts.

lib/page/dashboard/views/components/usp_device_analytics_card.dart

Refactor (12) +228 / -1079
usp_devices_data_service.dartSwitch devices fetch/rebuild to produce MeshNetwork +16/-354

Switch devices fetch/rebuild to produce MeshNetwork

• Replaces legacy DeviceUIModel/NodeUIModel construction and rebuild paths with MeshNetworkBuilder. DevicesDataFetchResult now carries meshNetwork as the primary output.

lib/page/devices/services/usp_devices_data_service.dart

devices_data_provider.dartMake DevicesData.meshNetwork the primary model +40/-42

Make DevicesData.meshNetwork the primary model

• Removes deviceModels/nodeModels and exposes derived accessors ('clientDevices', 'nodes', 'master', 'slaves', counts) from MeshNetwork. Updates provider rebuild logic to update meshNetwork on WiFi and mesh updates.

lib/page/devices/providers/devices_data_provider.dart

device_ui_model.dartRemove legacy DeviceUIModel +0/-314

Remove legacy DeviceUIModel

• Deletes the old UI model after migrating all consumers to ClientDevice/MeshNetwork.

lib/page/_shared/models/device_ui_model.dart

node_ui_model.dartRemove legacy NodeUIModel +0/-134

Remove legacy NodeUIModel

• Deletes the old node UI model after migrating topology and consumers to NodeEntity and BackhaulInfo.

lib/page/topology/models/node_ui_model.dart

usp_topology_builder.dartAdd buildFromMeshNetwork() and migrate topology construction +98/-142

Add buildFromMeshNetwork() and migrate topology construction

• Topology generation now uses MeshNetwork master/slaves and SlaveNode.backhaul for parent resolution and signal level. Deprecates the old build path relying on legacy models.

lib/page/topology/helpers/usp_topology_builder.dart

usp_network_topology_card.dartConsume MeshNetwork in topology card +11/-15

Consume MeshNetwork in topology card

• Updates card API and internal logic to build topology from MeshNetwork and use MeshNetwork counts. Removes dependence on DeviceUIModel/NodeUIModel lists.

lib/page/topology/cards/usp_network_topology_card.dart

usp_node_detail_view.dartMigrate node detail view to NodeEntity/SlaveNode backhaul model +34/-33

Migrate node detail view to NodeEntity/SlaveNode backhaul model

• Switches view rendering from NodeUIModel to NodeEntity; uses pattern matching for SlaveNode backhaul display and aligns role labels with localization.

lib/page/topology/views/usp_node_detail_view.dart

pdf_report_data.dartUpdate PDF report data model to ClientDevice/NodeEntity +8/-8

Update PDF report data model to ClientDevice/NodeEntity

• Replaces deviceModels/nodeModels with clientDevices/nodes fields aligned to the new MeshNetwork architecture.

lib/page/_shared/models/pdf_report_data.dart

usp_pdf_service.dartMigrate PDF rendering to ClientDevice/NodeEntity +6/-6

Migrate PDF rendering to ClientDevice/NodeEntity

• Updates connected device and topology sections to use new model types and accessors (isOnline, connectedClients).

lib/page/_shared/services/usp_pdf_service.dart

usp_command_provider.dartSwitch AI router context to MeshNetwork slaves/backhaul +8/-10

Switch AI router context to MeshNetwork slaves/backhaul

• Uses DevicesData.slaves and BackhaulInfo for extender reporting, removing reliance on nodeModels fields.

lib/ai/providers/usp_command_provider.dart

device_credentials_provider.dartUpdate credentials provider to use DevicesData.master +7/-11

Update credentials provider to use DevicesData.master

• Simplifies master lookup via MeshNetwork. Note: sets 'deviceUUID' to 'master.deviceId' as a fallback; verify this matches remote assistance expectations.

lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart

device_ui_extensions.dartRemove DeviceUIModel extensions +0/-10

Remove DeviceUIModel extensions

• Deletes extensions that are no longer relevant after the DeviceUIModel removal.

lib/page/_shared/extensions/device_ui_extensions.dart

Other (6) +184 / -1696
mesh_network_test_helper.dartAdd MeshNetwork test helper fixtures +44/-0

Add MeshNetwork test helper fixtures

• Introduces reusable fixture builders for MeshNetwork, nodes, and clients to simplify tests after the model migration.

test/test_helpers/mesh_network_test_helper.dart

usp_device_analytics_notifier_test.dartUpdate analytics notifier tests for ClientDevice/MeshNetwork +111/-26

Update analytics notifier tests for ClientDevice/MeshNetwork

• Refactors tests to use ClientDevice-based fixtures and validates updated distribution/signal behavior.

test/page/_shared/providers/usp_device_analytics_notifier_test.dart

mesh_topology_builder_test.dartUpdate mesh topology builder tests for NodeEntity + clientSignalMap +29/-23

Update mesh topology builder tests for NodeEntity + clientSignalMap

• Adjusts tests to assert NodeEntity output and clientSignalMap population from DataElements station data.

test/page/_shared/utils/mesh_topology_builder_test.dart

device_ui_model_test.dartRemove legacy DeviceUIModel tests +0/-454

Remove legacy DeviceUIModel tests

• Deletes tests for removed DeviceUIModel; coverage shifts to MeshNetwork/ClientDevice-focused tests.

test/page/_shared/models/device_ui_model_test.dart

node_ui_model_test.dartRemove legacy NodeUIModel tests +0/-311

Remove legacy NodeUIModel tests

• Deletes tests for removed NodeUIModel after migrating to NodeEntity.

test/page/topology/models/node_ui_model_test.dart

usp_topology_builder_test.dartRemove old topology builder tests tied to legacy models +0/-882

Remove old topology builder tests tied to legacy models

• Deletes large legacy test suite after topology builder migration; golden and new fixture-based tests cover new path.

test/page/topology/helpers/usp_topology_builder_test.dart

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

@qodo-code-review

Copy link
Copy Markdown

Code Review by Qodo

🐞 Bugs (2) 📘 Rule violations (1) 📎 Requirement gaps (0) 📜 Skill insights (0)

Context used

Grey Divider


Action required

1. Test data helper in wrong folder 📘 Rule violation ⚙ Maintainability
Description
A new reusable test data builder file was added under test/test_helpers/, but compliance requires
test data builders be centralized under test/mocks/test_data/ using the [feature]_test_data.dart
convention. Keeping builders outside the standard location increases duplication and makes test data
harder to discover and reuse consistently.
Code

test/test_helpers/mesh_network_test_helper.dart[R1-44]

+import 'package:privacy_gui/page/_shared/models/client_device.dart';
+import 'package:privacy_gui/page/_shared/models/mesh_network.dart';
+import 'package:privacy_gui/page/_shared/models/node_entity.dart';
+import 'package:privacy_gui/page/devices/providers/devices_data_provider.dart';
+
+/// Creates an empty MeshNetwork for testing purposes.
+MeshNetwork createEmptyMeshNetwork() {
+  return MeshNetwork(
+    master: MasterNode(
+      deviceId: 'GATEWAY',
+      model: 'Test Router',
+      connectedClients: [],
+    ),
+  );
+}
+
+/// Creates a MeshNetwork with the given clients for testing purposes.
+MeshNetwork createMeshNetworkWithClients(List<ClientDevice> clients) {
+  return MeshNetwork(
+    master: MasterNode(
+      deviceId: 'GATEWAY',
+      model: 'Test Router',
+      connectedClients: clients,
+    ),
+  );
+}
+
+/// Creates a MeshNetwork with both master and slave nodes for testing purposes.
+MeshNetwork createMeshNetworkWithNodes({
+  required MasterNode master,
+  List<SlaveNode> slaves = const [],
+}) {
+  return MeshNetwork(master: master, slaves: slaves);
+}
+
+/// Creates an empty DevicesData for testing purposes.
+DevicesData createEmptyDevicesData() {
+  return DevicesData(meshNetwork: createEmptyMeshNetwork());
+}
+
+/// Creates DevicesData with the given clients for testing purposes.
+DevicesData createDevicesDataWithClients(List<ClientDevice> clients) {
+  return DevicesData(meshNetwork: createMeshNetworkWithClients(clients));
+}
Evidence
PR Compliance ID 12 requires reusable test data builders/factories to be centralized under
test/mocks/test_data/ as [feature]_test_data.dart. The PR adds a new file under
test/test_helpers/ containing reusable MeshNetwork/DevicesData factory helpers, which violates the
required location/naming convention.

CLAUDE.md: Test data builders must be centralized under test/mocks/test_data/ as [feature]_test_data.dart
test/test_helpers/mesh_network_test_helper.dart[1-44]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
A reusable test data builder was added at `test/test_helpers/mesh_network_test_helper.dart`, but compliance requires such builders to live under `test/mocks/test_data/` following the `[feature]_test_data.dart` naming convention.

## Issue Context
This helper provides reusable factories (`createEmptyMeshNetwork()`, `createDevicesDataWithClients()`, etc.), which qualifies as centralized test data/builder code.

## Fix Focus Areas
- test/test_helpers/mesh_network_test_helper.dart[1-44]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. Wrong RA deviceUUID 🐞 Bug ≡ Correctness
Description
deviceCredentialsProvider sets DeviceCredentials.deviceUUID to master.deviceId, which is the
node identifier (typically MAC-like) rather than the Hosts DeviceID/UUID previously used for
Guardian Remote Assistance calls. This will break Remote Assistance session lookup/PIN creation
because requests are sent with an incorrect deviceUUID.
Code

lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart[R27-28]

+    macAddress: master.deviceId,
+    deviceUUID: master.deviceId, // fallback - may need refinement
Evidence
The provider explicitly assigns deviceUUID from master.deviceId, while the Hosts DeviceID/UUID
is still available in the system as ConnectedDevice.deviceId and is already carried into
ClientDevice.hostsDeviceId, demonstrating that the correct UUID exists but is not used for RA
credentials.

lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart[19-29]
lib/page/_shared/utils/mesh_network_builder.dart[295-314]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Remote Assistance credentials currently set `deviceUUID` to `master.deviceId`, which is not the Hosts DeviceID/UUID needed by Guardian API calls.

## Issue Context
- The UUID previously came from the Hosts table (`ConnectedDevice.deviceId`), and the codebase still preserves that value for clients as `ClientDevice.hostsDeviceId`.
- The new `MasterNode` model does not currently retain the Hosts DeviceID, so the provider can’t fetch it.

## Fix Focus Areas
- lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart[19-29]
- lib/page/_shared/models/node_entity.dart[84-190]
- lib/page/_shared/utils/mesh_network_builder.dart[75-125]

## Suggested fix
1. Add a `hostsDeviceId` (or similarly named) field to `MasterNode` (and optionally `NodeEntity` if you want it shared).
2. Populate it in `MeshNetworkBuilder._buildMasterNode` from the Hosts record (`masterDevice?.deviceId`).
3. Update `deviceCredentialsProvider` to use `devicesData.master.hostsDeviceId` for `deviceUUID` and return null if it’s missing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

3. Wired devices miscategorized 🐞 Bug ≡ Correctness
Description
UspDeviceAnalyticsNotifier._getDeviceCategory classifies any device with parentNodeName != null
and band == null as a “child node client”, but MeshNetworkBuilder patches parentNodeName onto
*all* master clients and wired devices always have band == null. This causes master wired clients
to be bucketed under the gateway name instead of "Wired", skewing bandDistribution and any UI/PDF
that depends on it.
Code

lib/page/_shared/providers/usp_device_analytics_notifier.dart[R176-184]

+  String _getDeviceCategory(ClientDevice d) {
+    final isChildNodeClient = d.parentNodeName != null && d.band == null;
+    if (isChildNodeClient) {
+      return d.parentNodeName!;
+    }
+    if (d.isWifi) {
+      return d.band ?? 'WiFi';
+    }
+    return 'Wired';
Evidence
The categorization condition uses parentNodeName + band == null, while the builder forcibly sets
parentNodeName for master clients and ClientDevice.band is derived from WiFi info only (so wired
devices have band == null). Together, this guarantees wired master clients satisfy the “child node
client” condition and are bucketed under the master name.

lib/page/_shared/providers/usp_device_analytics_notifier.dart[171-185]
lib/page/_shared/utils/mesh_network_builder.dart[105-125]
lib/page/_shared/models/client_device.dart[172-188]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
Device analytics categorization treats `(parentNodeName != null && band == null)` as “child node client”, which incorrectly captures master wired devices because:
- wired => `band == null`
- builder patches `parentNodeName` for master clients

## Issue Context
This directly impacts `DeviceDistribution.bandDistribution` (now used as category distribution) and can mislabel wired totals.

## Fix Focus Areas
- lib/page/_shared/providers/usp_device_analytics_notifier.dart[171-185]
- lib/page/_shared/utils/mesh_network_builder.dart[105-125]
- lib/page/_shared/models/client_device.dart[172-188]

## Suggested fix
Update `_getDeviceCategory` to handle wired devices first, then use band/name only for WiFi, e.g.:
```dart
String _getDeviceCategory(ClientDevice d) {
 if (!d.isWifi) return 'Wired';
 if (d.band != null && d.band!.isNotEmpty) return d.band!;
 if (d.parentNodeName != null && d.parentNodeName!.isNotEmpty) return d.parentNodeName!;
 return 'WiFi';
}
```
This matches the method’s own comment (“Master Wired: 'Wired'”) and prevents master wired devices from being categorized under the gateway display name.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

ⓘ You are approaching your monthly quota for Qodo. Upgrade your plan

Qodo Logo

Comment on lines +1 to +44
import 'package:privacy_gui/page/_shared/models/client_device.dart';
import 'package:privacy_gui/page/_shared/models/mesh_network.dart';
import 'package:privacy_gui/page/_shared/models/node_entity.dart';
import 'package:privacy_gui/page/devices/providers/devices_data_provider.dart';

/// Creates an empty MeshNetwork for testing purposes.
MeshNetwork createEmptyMeshNetwork() {
return MeshNetwork(
master: MasterNode(
deviceId: 'GATEWAY',
model: 'Test Router',
connectedClients: [],
),
);
}

/// Creates a MeshNetwork with the given clients for testing purposes.
MeshNetwork createMeshNetworkWithClients(List<ClientDevice> clients) {
return MeshNetwork(
master: MasterNode(
deviceId: 'GATEWAY',
model: 'Test Router',
connectedClients: clients,
),
);
}

/// Creates a MeshNetwork with both master and slave nodes for testing purposes.
MeshNetwork createMeshNetworkWithNodes({
required MasterNode master,
List<SlaveNode> slaves = const [],
}) {
return MeshNetwork(master: master, slaves: slaves);
}

/// Creates an empty DevicesData for testing purposes.
DevicesData createEmptyDevicesData() {
return DevicesData(meshNetwork: createEmptyMeshNetwork());
}

/// Creates DevicesData with the given clients for testing purposes.
DevicesData createDevicesDataWithClients(List<ClientDevice> clients) {
return DevicesData(meshNetwork: createMeshNetworkWithClients(clients));
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Test data helper in wrong folder 📘 Rule violation ⚙ Maintainability

A new reusable test data builder file was added under test/test_helpers/, but compliance requires
test data builders be centralized under test/mocks/test_data/ using the [feature]_test_data.dart
convention. Keeping builders outside the standard location increases duplication and makes test data
harder to discover and reuse consistently.
Agent Prompt
## Issue description
A reusable test data builder was added at `test/test_helpers/mesh_network_test_helper.dart`, but compliance requires such builders to live under `test/mocks/test_data/` following the `[feature]_test_data.dart` naming convention.

## Issue Context
This helper provides reusable factories (`createEmptyMeshNetwork()`, `createDevicesDataWithClients()`, etc.), which qualifies as centralized test data/builder code.

## Fix Focus Areas
- test/test_helpers/mesh_network_test_helper.dart[1-44]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +27 to +28
macAddress: master.deviceId,
deviceUUID: master.deviceId, // fallback - may need refinement

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Wrong ra deviceuuid 🐞 Bug ≡ Correctness

deviceCredentialsProvider sets DeviceCredentials.deviceUUID to master.deviceId, which is the
node identifier (typically MAC-like) rather than the Hosts DeviceID/UUID previously used for
Guardian Remote Assistance calls. This will break Remote Assistance session lookup/PIN creation
because requests are sent with an incorrect deviceUUID.
Agent Prompt
## Issue description
Remote Assistance credentials currently set `deviceUUID` to `master.deviceId`, which is not the Hosts DeviceID/UUID needed by Guardian API calls.

## Issue Context
- The UUID previously came from the Hosts table (`ConnectedDevice.deviceId`), and the codebase still preserves that value for clients as `ClientDevice.hostsDeviceId`.
- The new `MasterNode` model does not currently retain the Hosts DeviceID, so the provider can’t fetch it.

## Fix Focus Areas
- lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart[19-29]
- lib/page/_shared/models/node_entity.dart[84-190]
- lib/page/_shared/utils/mesh_network_builder.dart[75-125]

## Suggested fix
1. Add a `hostsDeviceId` (or similarly named) field to `MasterNode` (and optionally `NodeEntity` if you want it shared).
2. Populate it in `MeshNetworkBuilder._buildMasterNode` from the Hosts record (`masterDevice?.deviceId`).
3. Update `deviceCredentialsProvider` to use `devicesData.master.hostsDeviceId` for `deviceUUID` and return null if it’s missing.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +176 to +184
String _getDeviceCategory(ClientDevice d) {
final isChildNodeClient = d.parentNodeName != null && d.band == null;
if (isChildNodeClient) {
return d.parentNodeName!;
}
if (d.isWifi) {
return d.band ?? 'WiFi';
}
return 'Wired';

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Remediation recommended

3. Wired devices miscategorized 🐞 Bug ≡ Correctness

UspDeviceAnalyticsNotifier._getDeviceCategory classifies any device with parentNodeName != null
and band == null as a “child node client”, but MeshNetworkBuilder patches parentNodeName onto
*all* master clients and wired devices always have band == null. This causes master wired clients
to be bucketed under the gateway name instead of "Wired", skewing bandDistribution and any UI/PDF
that depends on it.
Agent Prompt
## Issue description
Device analytics categorization treats `(parentNodeName != null && band == null)` as “child node client”, which incorrectly captures master wired devices because:
- wired => `band == null`
- builder patches `parentNodeName` for master clients

## Issue Context
This directly impacts `DeviceDistribution.bandDistribution` (now used as category distribution) and can mislabel wired totals.

## Fix Focus Areas
- lib/page/_shared/providers/usp_device_analytics_notifier.dart[171-185]
- lib/page/_shared/utils/mesh_network_builder.dart[105-125]
- lib/page/_shared/models/client_device.dart[172-188]

## Suggested fix
Update `_getDeviceCategory` to handle wired devices first, then use band/name only for WiFi, e.g.:
```dart
String _getDeviceCategory(ClientDevice d) {
  if (!d.isWifi) return 'Wired';
  if (d.band != null && d.band!.isNotEmpty) return d.band!;
  if (d.parentNodeName != null && d.parentNodeName!.isNotEmpty) return d.parentNodeName!;
  return 'WiFi';
}
```
This matches the method’s own comment (“Master Wired: 'Wired'”) and prevents master wired devices from being categorized under the gateway display name.

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

@AustinChangLinksys

AustinChangLinksys commented Jul 3, 2026

Copy link
Copy Markdown
Collaborator Author

🤖 Automated Review — Oversize PR

This round's changes exceed the automated-review limit (6,692 lines / 72 files, limit 4,000 lines / 60 files); AI review was not run. Manual review recommended.

First 15 changed files (for a quick scan):

lib/ai/providers/usp_command_provider.dart
lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart
lib/page/_shared/extensions/device_ui_extensions.dart
lib/page/_shared/models/backhaul_info.dart
lib/page/_shared/models/client_device.dart
lib/page/_shared/models/device_ui_model.dart
lib/page/_shared/models/mesh_network.dart
lib/page/_shared/models/mesh_topology_info.dart
lib/page/_shared/models/network_entity.dart
lib/page/_shared/models/node_entity.dart
lib/page/_shared/models/pdf_report_data.dart
lib/page/_shared/models/wifi_connection_info.dart
lib/page/_shared/providers/usp_device_analytics_notifier.dart
lib/page/_shared/services/usp_pdf_service.dart
lib/page/_shared/utils/mesh_network_builder.dart

AustinChangLinksys and others added 2 commits July 6, 2026 10:44
Resolve conflicts:
- Use ClientDevice model (this branch) instead of DeviceUIModel
- Keep AppLineChart for trend view (this branch's design choice)
- Remove duplicate variable definitions in analytics card
- Use deviceId for router MAC filtering (NodeEntity API)
- Delete obsolete usp_topology_builder_test.dart (uses old DeviceUIModel API)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
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>
@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

🤖 Automated Review — Oversize PR

This round's changes exceed the automated-review limit (4738 lines / 58 files, limit 4000 lines / 60 files); AI review was not run. Manual review recommended.

First 15 changed files (for a quick scan):

lib/constants/pref_key.dart
lib/core/connection/services/recovery_probe_service.dart
lib/core/usp/providers/usp_auth_coordinator.dart
lib/core/usp/providers/usp_token_storage.dart
lib/core/usp/providers/usp_token_storage_stub.dart
lib/core/usp/providers/usp_token_storage_web.dart
lib/core/usp/services/usp_client.dart
lib/core/usp/stub/usp_client_stub.dart
lib/core/usp/web/usp_client_wasm.dart
lib/demo/providers/demo_overrides.dart
lib/demo/usp/demo_usp_service.dart
lib/page/_shared/components/layout_blocks/row_blocks.dart
lib/page/_shared/models/system_monitor_state.dart
lib/page/_shared/providers/device_analytics_persistence.dart
lib/page/_shared/providers/usp_device_analytics_notifier.dart

- 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>
@AustinChangLinksys AustinChangLinksys changed the title fix(devices): MeshNetwork architecture + fix #1043 #1044 fix(devices): MeshNetwork architecture + fix #1043 #1044 #1047 #1048 Jul 6, 2026

@AustinChangLinksys AustinChangLinksys left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 Automated Review — Round 3 · 936cdda..784d17c (incremental)

Verdict: 💬 Self-review (comment only) — The clientBandSsidMap band/SSID fallback for slave node clients is well-designed and correctly wired end-to-end. One Warning about a sibling SNR bug and one about a stale-snapshot race; several Suggestions.

Conf. Where Issue (one-liner)
⚠️ High stats_wifi_channels_section.dart:80 [single, NEW] SNR fix not applied to sibling file — c.client.signalStrength/noise still used; part of the same #1043/#1044 fix context
⚠️ High devices_data_provider.dart:172,286 [single, NEW] Stale wifiData snapshot used in fire-and-forget _fetchMeshAndUpdate; SSE WiFi updates can overtake the mesh fetch
💡 Med usp_wifi_data_service.dart:407 _ensureTrailingDot("") returns "" — empty instancePath can cause phantom radio path match for malformed USP responses
💡 Med devices_data_provider.dart (diff call site) No log warning when bssidToBandMap is empty due to WiFi data timeout; silent degradation
💡 Med mesh_topology_builder.dart (diff) bss.bssid not guarded for empty before map lookup (harmless today but asymmetric with buildBssidToBandMap)
High mesh_topology_builder.dart + usp_wifi_data_service.dart BSSID normalization is consistent in both paths — both use trim().toUpperCase()
High usp_wifi_data_service.dart:buildBssidToBandMap Empty BSSID guard (if (bssid.isEmpty) continue) is correct and covers the null-BSSID codegen case
High usp_wifi_performance_card.dart:347 SNR fix applied correctly — c.signalStrength, c.noise at PR head (verified via git show 784d17c)
High test/...mesh_network_builder_test.dart 12-scenario test suite comprehensively covers builder paths including signal fallback, band/SSID fallback, precedence, MAC normalization

Confidence: High = code-verified · Med = located + reasoned, not fully confirmed · Low = speculative, please double-check.

Warning Details

W-1: SNR fix not applied to sibling file [single, High]

lib/page/statistics/views/sections/stats_wifi_channels_section.dart:80 (NOT in this PR's diff):

final snr = computeSNR(c.client.signalStrength, c.client.noise);  // c.client.* still present

usp_wifi_performance_card.dart was fixed in this PR to use c.signalStrength, c.noise (confirmed via git show 784d17cf). Both files have a local _ClientInfo class that wraps WifiClientUIModel client. If the intent of this PR is to flatten _ClientInfo to expose signalStrength/noise directly (eliminating the .client nesting), then stats_wifi_channels_section.dart:80 must be updated in the same PR, or the two files diverge in their SNR computation model.

Trigger condition: Always — every render of the WiFi channels statistics view calls this path.

Fix: Either apply the same _ClientInfo flattening to stats_wifi_channels_section.dart in this PR, or document that the two files intentionally use different data models and the stats file is out-of-scope.


W-2: Stale wifiData used in fire-and-forget mesh fetch [single, High]

lib/page/devices/providers/devices_data_provider.dart:172:

_fetchMeshAndUpdate(svc, wifiData, gatewayName, sysData, result);
//                       ^^^^^^^^ snapshot from before the mesh fetch starts

_fetchMeshAndUpdate is called fire-and-forget (no await). By the time fetchMeshTopology completes (potentially slow on large mesh networks), wifiDataProvider may have already emitted a fresh SSE-triggered state. The wifiData passed in will be stale, causing the rebuilt device models to use outdated SSID names, bands, and connection details for the current mesh topology.

The same issue exists at line 286 in _refetchPreservingMesh.

Trigger condition: SSE WiFi update fires between the start of _fetch() and completion of the background fetchMeshTopology() call.

Fix: Re-read WiFi data from Riverpod after the mesh fetch completes inside _fetchMeshAndUpdate:

// After: final meshTopology = await svc.fetchMeshTopology(...)
final freshWifi = ref.read(wifiDataProvider).valueOrNull;
final effectiveWifi = freshWifi ?? wifiData;  // use freshest available
Suggestion Details

S-1: _ensureTrailingDot("") phantom match [Med]

lib/page/wifi_settings/services/usp_wifi_data_service.dart:407-409:

static String _ensureTrailingDot(String path) {
  if (path.isEmpty) return path;  // returns "" unchanged
  return path.endsWith('.') ? path : '$path.';
}

If both radio.instancePath and ssid.lowerLayers are empty strings, both map to "" and will incorrectly match each other in buildBssidToBandMap, associating an SSID with a band from a radio with an empty path. This is malformed USP data but worth guarding.

Fix: if (path.isEmpty) return path; // keep returning "", but also add guard at call site: if (path.isEmpty) continue;


S-2: No log warning on empty bssidToBandMap [Med]

When wifiDataProvider times out, buildBssidToBandMap is called with WiFiSsids(items: []) and returns {}. Band enrichment silently degrades to no-op. There is no diagnostic trace of why slave clients show no band.

Fix:

if (bssidToBandMap.isEmpty) {
  logger.w('[DevicesData]: bssidToBandMap is empty — slave client band/SSID enrichment skipped');
}

S-3: bss.bssid not guarded for empty in MeshTopologyBuilder [Med]

lib/page/_shared/utils/mesh_topology_builder.dart (new code):

final bssidUpper = bss.bssid.trim().toUpperCase();
final band = bssidToBandMap[bssidUpper] ?? '';  // empty lookup miss — harmless

Empty BSSID causes a harmless map miss. However, it's asymmetric with buildBssidToBandMap which explicitly guards empty BSSIDs. For clarity and symmetry:

Fix: Add if (bssidUpper.isEmpty) continue; after normalizing bssidUpper.

What looks good
  • clientBandSsidMap design — clean record type ({String band, String ssid}) stored by uppercase MAC; the connectionDetailMap precedence over clientBandSsidMap is correctly implemented and tested.
  • BSSID normalization consistency — both buildBssidToBandMap and MeshTopologyBuilder use trim().toUpperCase() for lookup keys. No mismatch.
  • buildBssidToBandMap static method — correctly uses _ensureTrailingDot for consistent path normalization; skips empty BSSIDs and empty bands.
  • Data flow end-to-endDevicesDataNotifierbuildBssidToBandMapfetchMeshTopology(bssidToBandMap:)MeshTopologyBuilder.build(bssidToBandMap:)clientBandSsidMapMeshNetworkBuilderWifiConnectionInfo.band is a coherent, traceable chain.
  • Test suite (mesh_network_builder_test.dart, mesh_topology_builder_test.dart, usp_wifi_data_service_test.dart) — 12-scenario MeshNetworkBuilder test suite covers signal fallback, band/SSID fallback, precedence, multi-interface merging, MAC normalization, wired/WiFi detection. MeshTopologyBuilder test covers clientBandSsidMap population and empty band case.
  • WifiData.codegenContext in equality (from PR 1075)DevicesDataNotifier correctly reads wifiData.codegenContext.raw to build the BSSID map; not bypassing the codegen boundary from the provider layer.
  • No architecture layer violationsUspWifiDataService.buildBssidToBandMap is a static utility method callable from other services; DevicesDataNotifier reads wifiDataProvider via ref.read (correct for a Notifier); UspDevicesDataService stays in the service layer.

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

@PeterJhongLinksys PeterJhongLinksys left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Manual review confirms the two blocking issues already flagged inline by qodo — both should be resolved before merge:

  1. RA deviceUUID regression (device_credentials_provider.dart) — deviceUUID is now set to the master's MAC (master.deviceId) instead of the Hosts DeviceID/UUID that the legacy path used. MasterNode carries no field retaining the Hosts UUID, and the inline // fallback - may need refinement confirms it's unfinished. This identifier flows into every Guardian Remote Assistance call, so RA session lookup / PIN creation will receive the wrong value — a functional regression.

  2. Wired-client miscategorization (usp_device_analytics_notifier.dart, _getDeviceCategory) — parentNodeName != null && band == null is treated as "child-node client", but MeshNetworkBuilder patches parentNodeName onto all master clients and wired devices always have band == null, so master wired clients get bucketed under the gateway name instead of "Wired", skewing bandDistribution.

Otherwise the migration looks sound (l10n keys present, MeshNetwork.master non-null so no null-crash, no dead _getRouterMacs()). Holding approval on these two.

…, 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>
@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback — commit 7f1c12f5

Verified all flagged issues against the code and pushed fixes.

✅ Fixed in this PR

1. RA deviceUUID regression (blocking) — device_credentials_provider.dart
Confirmed: the new MasterNode carried no Hosts UUID, so deviceUUID fell back to master.deviceId (the MAC), which would break Guardian RA session lookup / PIN creation.
Fix: added a hostsDeviceId field to MasterNode, plumbed it through MeshNetworkBuilder._buildMasterNode (and the patchedMaster rebuild), and the provider now uses master.hostsDeviceId. When no UUID is available it returns null — matching the legacy behavior, so we never send a wrong deviceUUID.

2. Wired-client miscategorization (blocking) — usp_device_analytics_notifier.dart
Confirmed: MeshNetworkBuilder patches parentNodeName onto all clients (including the master's — this is the intended "show connected node name for all devices" feature), and wired devices always have band == null, so master wired clients were bucketed under the gateway name instead of "Wired".
Fix: _getDeviceCategory now keys off parentNodeId (null only for master clients) instead of parentNodeName. The builder's patch behavior is unchanged, so the node-name UI feature is preserved.

3. Test data helper in wrong folder (rule violation) — test/test_helpers/mesh_network_test_helper.dart
Confirmed, plus: the file was unused dead code (zero references across test/) and duplicated the MeshNetwork factories already in DevicesTestData. Deleted it.

✅ Also fixed — band/SSID fallback logic bug (found during review)

4. Empty-string band masks the DataElements fallbackmesh_network_builder.dart
ClientConnectionDetail.band/ssidName are non-nullable Strings that are '' when AP→SSID→radio resolution fails. Because detail?.band is only null when detail itself is null, detail?.band ?? bandSsid?.band returned '' and never fell through to the DataElements value. Fixed by treating empty strings as absent (_nonEmpty() helper).

🔀 Split out to a follow-up issue — #1118

Slave-node WiFi client band display can't work today: buildBssidToBandMap() is built only from the master's local Device.WiFi.SSID.* table, which never contains slave BSSIDs. A proper fix needs a codegen (YAML) change in linksys/usp_framework to expose WiFi.Radio.MACAddress and DataElements Radio.ID, so I split it into #1118 (design doc doc/usp/dataelements-sta-band-enhancement.md will land with that work). The fallback fix above is a prerequisite and is included here.

Tests

  • Added test/core/cloud/providers/remote_assistance/device_credentials_provider_test.dart (UUID used correctly; null when unavailable/empty).
  • Updated the analytics notifier test to model the real patched shape (master clients now carry parentNodeName) as a regression guard — verified it fails under the old logic.
  • Added an empty-string fallback case in mesh_network_builder_test.dart and a MasterNode.hostsDeviceId equality test.

Verification: dart format clean · flutter analyze no errors/warnings on changed files · ./run_tests.sh3074 passed.

@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

Addressed review feedback — commit 7f1c12f5

Verified all flagged issues against the code and pushed fixes.

✅ Fixed in this PR

1. RA deviceUUID regression (blocking) — device_credentials_provider.dart
Confirmed: the new MasterNode carried no Hosts UUID, so deviceUUID fell back to master.deviceId (the MAC), which would break Guardian RA session lookup / PIN creation.
Fix: added a hostsDeviceId field to MasterNode, plumbed it through MeshNetworkBuilder._buildMasterNode (and the patchedMaster rebuild), and the provider now uses master.hostsDeviceId. When no UUID is available it returns null — matching the legacy behavior, so we never send a wrong deviceUUID.

2. Wired-client miscategorization (blocking) — usp_device_analytics_notifier.dart
Confirmed: MeshNetworkBuilder patches parentNodeName onto all clients (including the master's — this is the intended "show connected node name for all devices" feature), and wired devices always have band == null, so master wired clients were bucketed under the gateway name instead of "Wired".
Fix: _getDeviceCategory now keys off parentNodeId (null only for master clients) instead of parentNodeName. The builder's patch behavior is unchanged, so the node-name UI feature is preserved.

3. Test data helper in wrong folder (rule violation) — test/test_helpers/mesh_network_test_helper.dart
Confirmed, plus: the file was unused dead code (zero references across test/) and duplicated the MeshNetwork factories already in DevicesTestData. Deleted it.

✅ Also fixed — band/SSID fallback logic bug (found during review)

4. Empty-string band masks the DataElements fallbackmesh_network_builder.dart
ClientConnectionDetail.band/ssidName are non-nullable Strings that are '' when AP→SSID→radio resolution fails. Because detail?.band is only null when detail itself is null, detail?.band ?? bandSsid?.band returned '' and never fell through to the DataElements value. Fixed by treating empty strings as absent (_nonEmpty() helper).

🔀 Split out to a follow-up issue — #1118

Slave-node WiFi client band display can't work today: buildBssidToBandMap() is built only from the master's local Device.WiFi.SSID.* table, which never contains slave BSSIDs. A proper fix needs a codegen (YAML) change in linksys/usp_framework to expose WiFi.Radio.MACAddress and DataElements Radio.ID — tracked in #1118. The empty-string fallback fix above is a prerequisite and is included here.

Tests

  • Added test/core/cloud/providers/remote_assistance/device_credentials_provider_test.dart (UUID used correctly; null when unavailable/empty).
  • Updated the analytics notifier test to model the real patched shape (master clients now carry parentNodeName) as a regression guard — verified it fails under the old logic.
  • Added an empty-string fallback case in mesh_network_builder_test.dart and a MasterNode.hostsDeviceId equality test.

Verification: dart format clean · flutter analyze no errors/warnings on changed files · ./run_tests.sh3074 passed.

@AustinChangLinksys AustinChangLinksys left a comment

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

🤖 Automated Review — Round 4 · 784d17c..7f1c12f (incremental)

Verdict: 💬 Self-review (comment only) — GitHub prohibits self-approval. The two blocking issues from Round 3 (RA deviceUUID regression and wired-client miscategorization) are correctly fixed in this round. One carry-over Warning (stale wifiData snapshot) remains open; band/SSID empty-string fallback bug is now fixed.

Conf. Where Issue (one-liner)
⚠️ High devices_data_provider.dart:176,294 [CARRY-OVER W-2] Stale wifiData snapshot passed into fire-and-forget _fetchMeshAndUpdate — pre-existing, not introduced in this diff
High device_credentials_provider.dart:24-30 [FIXED] deviceUUID regression resolved — hostsDeviceId from Hosts table used; null/empty guard prevents wrong value to Guardian API
High usp_device_analytics_notifier.dart:248 [FIXED] Wired-client miscategorization resolved — parentNodeId correctly discriminates master vs slave clients; master wired no longer falls into gateway-name bucket
High mesh_network_builder.dart:293-294 [FIXED] Empty-string band/SSID fallback fixed — _nonEmpty() helper prevents empty-string from masking ?? fallback to DataElements value
High test/.../device_credentials_provider_test.dart New regression guard: 5 cases covering UUID used, null when no UUID, null when empty, null when no session, null when no devices
💡 Med node_entity.dart:158 copyWith(hostsDeviceId:) cannot explicitly set field to null (nullable-param-without-sentinel pattern, consistent with rest of codebase) — minor limitation

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

Warning Details

W-2 [CARRY-OVER, not in this diff] · High — Stale wifiData in fire-and-forget _fetchMeshAndUpdate

lib/page/devices/providers/devices_data_provider.dart:176 and :294

// Line 176: fire-and-forget — no await
_fetchMeshAndUpdate(svc, wifiData, gatewayName, sysData, result);
//                       ^^^^^^^^ snapshot from before the mesh fetch starts

// Line 294 (_refetchPreservingMesh): same pattern
_fetchMeshAndUpdate(svc, wifiData, gatewayName, sysData, result);

_fetchMeshAndUpdate is called without await. By the time fetchMeshTopology() completes (potentially slow on larger mesh networks), wifiDataProvider may have emitted a fresh SSE-triggered state. The stale wifiData snapshot causes rebuilt device models to use outdated SSID names, bands, and connection details. Not introduced in this diff (pre-existing).

Austin split slave-band-display enhancement to #1118 (requires USP framework codegen changes). This stale-snapshot issue is a separate correctness concern affecting all mesh fetches, not dependent on #1118.

Fix (same as Round 3 suggestion): Re-read WiFi data after the mesh fetch completes inside _fetchMeshAndUpdate:

// After: final meshTopology = await svc.fetchMeshTopology(...)
final freshWifi = ref.read(wifiDataProvider).valueOrNull;
final effectiveWifi = freshWifi ?? wifiData;  // prefer freshest available
What looks good
  • deviceUUID fix is correct end-to-end: ConnectedDevice.deviceId in the Hosts table is the Hosts DeviceID/UUID (not the MAC — that is macAddress). _buildMasterNode correctly maps hostsDeviceId: masterDevice?.deviceId. The provider adds a null/empty guard before returning credentials. Regression test device_credentials_provider_test.dart explicitly verifies deviceUUID != macAddress.
  • patchedMaster propagation: mesh_network_builder.dart:125 correctly carries hostsDeviceId: master.hostsDeviceId through the patchedMaster rebuild, ensuring the fix is not lost in the multi-pass build.
  • parentNodeId discrimination: The comment in usp_device_analytics_notifier.dart:243-246 correctly explains why parentNodeName is unreliable (patched onto all clients by the builder) and why parentNodeId is the correct discriminant (null only for master clients). Test regression guard with parentNodeName: 'MyGateway' on master clients explicitly validates bandDistribution.containsKey('MyGateway') is false.
  • _nonEmpty() helper: Clean and documented. mesh_network_builder_test.dart regression test correctly validates the fallback chain with empty-string ClientConnectionDetail.
  • mesh_network_test_helper.dart deletion: Confirmed unused (zero references across test/); duplication with DevicesTestData factories correctly identified.
  • hostsDeviceId in Equatable props: Correctly participates in equality (line 192). copyWith null-passthrough pattern is consistent with all other nullable fields in MasterNode.
  • 3074 tests passing (per commit note). flutter analyze clean on changed files.

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

@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

Merged latest dev-2.6.0 and resolved conflicts — commit c7557aa2

dev-2.6.0 shipped a device multi-select filter + DeviceCategory / private-MAC feature that was still built on the legacy DeviceUIModel / NodeUIModel, which this branch removed in favor of the MeshNetwork (ClientDevice / NodeEntity) architecture. Integrated that feature onto the new models.

Conflict resolution

  • device_filter_state.dart / device_filter_provider.dart — kept ClientDevice / NodeEntity; ported dev-2.6.0's multi-select + DeviceCategory + private-MAC filtering onto ClientDevice. DeviceConnectionTypeConnectionType.
  • usp_device_filter_panel.dartDeviceConnectionTypeConnectionType; hide ConnectionType on the ui_kit import to avoid the name clash.
  • device_filter_provider_test.dart — rebuilt dev-2.6.0's 49-test multi-select suite (incl. category / private-MAC / cross-dimension AND) on ClientDevice fixtures.
  • Ripple test files migrated off the deleted models: dhcp_data_provider_test, usp_dhcp_reservations_notifier_test, dhcp_reservation_edit_dialog_test, device_filter_state_test.

Verification

  • flutter analyze: 0 errors.
  • ./run_tests.sh: 3209 tests passed.
  • Device filter (status / connection / signal / node / SSID / band / category / private-MAC, multi-select + cross-dimension AND) verified end-to-end via main_demo.

PR is MERGEABLE again.

Integrate the dev-2.6.0 device multi-select filter + DeviceCategory feature
onto this branch's MeshNetwork architecture (which removed DeviceUIModel /
NodeUIModel / device_ui_extensions).

Conflict resolutions:
- device_filter_state.dart / device_filter_provider.dart: keep ClientDevice /
  NodeEntity types; port dev-2.6.0's multi-select + DeviceCategory + private-MAC
  filtering onto ClientDevice. DeviceConnectionType → ConnectionType.
- usp_device_filter_panel.dart: DeviceConnectionType → ConnectionType, hide
  ui_kit's ConnectionType to avoid the name clash.
- device_filter_provider_test.dart: rebuilt dev-2.6.0's multi-select test suite
  (49 tests incl. category / private-MAC / cross-dimension AND) on ClientDevice
  fixtures.
- dhcp_data_provider_test / usp_dhcp_reservations_notifier_test /
  dhcp_reservation_edit_dialog_test / device_filter_state_test: migrated
  DeviceUIModel fixtures + DevicesData.deviceModels to MeshNetwork/ClientDevice;
  dropped const on non-const model constructors.

flutter analyze: 0 errors. ./run_tests.sh: 3209 tests passed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
@AustinChangLinksys AustinChangLinksys force-pushed the fix/device-analytics-display-1043-1044 branch from c7557aa to dcd9bb2 Compare July 10, 2026 14:55
@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

🤖 Automated Review — Oversize PR

This round's changes exceed the automated-review limit (8563 lines / 135 files, limit 6000 lines / 100 files); AI review was not run. Manual review recommended.

First 15 changed files (for a quick scan):

.claude/settings.json
doc/usp/vendored-artifacts.md
lib/ai/providers/usp_command_provider.dart
lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart
lib/generated/gre_tunnel.g.dart
lib/generated/index.dart
lib/generated/l2tp_tunnel.g.dart
lib/generated/ppp_interface.g.dart
lib/generated/wi_fi_access_points.g.dart
lib/l10n/app_ar.arb
lib/l10n/app_da.arb
lib/l10n/app_de.arb
lib/l10n/app_el.arb
lib/l10n/app_en.arb
lib/l10n/app_es.arb

1 similar comment
@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

🤖 Automated Review — Oversize PR

This round's changes exceed the automated-review limit (8563 lines / 135 files, limit 6000 lines / 100 files); AI review was not run. Manual review recommended.

First 15 changed files (for a quick scan):

.claude/settings.json
doc/usp/vendored-artifacts.md
lib/ai/providers/usp_command_provider.dart
lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart
lib/generated/gre_tunnel.g.dart
lib/generated/index.dart
lib/generated/l2tp_tunnel.g.dart
lib/generated/ppp_interface.g.dart
lib/generated/wi_fi_access_points.g.dart
lib/l10n/app_ar.arb
lib/l10n/app_da.arb
lib/l10n/app_de.arb
lib/l10n/app_el.arb
lib/l10n/app_en.arb
lib/l10n/app_es.arb

@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

🤖 Automated Review — Oversize PR

This round's changes exceed the automated-review limit (8563 lines / 135 files,
limit 6000 lines / 100 files); AI review was not run. Manual review recommended.

First 15 changed files (for a quick scan):

.claude/settings.json
doc/usp/vendored-artifacts.md
lib/ai/providers/usp_command_provider.dart
lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart
lib/generated/gre_tunnel.g.dart
lib/generated/index.dart
lib/generated/l2tp_tunnel.g.dart
lib/generated/ppp_interface.g.dart
lib/generated/wi_fi_access_points.g.dart
lib/l10n/app_ar.arb
lib/l10n/app_da.arb
lib/l10n/app_de.arb
lib/l10n/app_el.arb
lib/l10n/app_en.arb
lib/l10n/app_es.arb

@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

🤖 Automated Review — Oversize PR

This round's changes exceed the automated-review limit (8563 lines / 135 files, limit 6000 lines / 100 files); AI review was not run. Manual review recommended.

First 15 changed files (for a quick scan):

.claude/settings.json
doc/usp/vendored-artifacts.md
lib/ai/providers/usp_command_provider.dart
lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart
lib/generated/gre_tunnel.g.dart
lib/generated/index.dart
lib/generated/l2tp_tunnel.g.dart
lib/generated/ppp_interface.g.dart
lib/generated/wi_fi_access_points.g.dart
lib/l10n/app_ar.arb
lib/l10n/app_da.arb
lib/l10n/app_de.arb
lib/l10n/app_el.arb
lib/l10n/app_en.arb
lib/l10n/app_es.arb

@AustinChangLinksys

Copy link
Copy Markdown
Collaborator Author

🤖 Automated Review — Oversize PR

This round's changes exceed the automated-review limit (8563 lines / 135 files,
limit 6000 lines / 100 files); AI review was not run. Manual review recommended.

First 15 changed files (for a quick scan):

.claude/settings.json
doc/usp/vendored-artifacts.md
lib/ai/providers/usp_command_provider.dart
lib/core/cloud/providers/remote_assistance/device_credentials_provider.dart
lib/generated/gre_tunnel.g.dart
lib/generated/index.dart
lib/generated/l2tp_tunnel.g.dart
lib/generated/ppp_interface.g.dart
lib/generated/wi_fi_access_points.g.dart
lib/l10n/app_ar.arb
lib/l10n/app_da.arb
lib/l10n/app_de.arb
lib/l10n/app_el.arb
lib/l10n/app_en.arb
lib/l10n/app_es.arb

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

need-decide Automated review left findings needing Austin to decide (fix/defer/dismiss).

Projects

None yet

2 participants