fix(devices): show node display name in Node filter, not model (#1157) - #1171
Conversation
The Devices Node filter chips (and the active-filter summary chips)
showed the mesh node's model name (e.g. "M60TB-EU") instead of the
user-assigned node name.
Root cause: deviceFilterOptionsProvider fed the filter with
`meshTopology.nodes` — the DataElements-derived NodeEntity list, which
carries no friendlyName/hostName — so NodeEntity.displayName always fell
back to the model. The "Connected to/via" field was already correct
because it resolves names from the fully-built mesh nodes (Hosts data).
Fix: introduce a lightweight `NodeFilterOption {id, label}` view model,
mirroring the other filter dimensions (ssids/bands) instead of leaking a
full domain entity into the filter layer. The provider now resolves each
option's label from the built mesh nodes (data.nodes), matched back via
`dataElementsId`, while keeping the DataElements node id as the selection
key so filtering/reconciliation are unchanged. Falls back to the model
name only when no display name exists.
Tests: add coverage proving the label resolves to the display name (id
stays the DataElements id) and the model-name fallback path.
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? |
Code ReviewReviewed against the PR head ( ✅ Core logic verified
🟢 Informational (non-blocking)
LGTM. |
HankYuLinksys
left a comment
There was a problem hiding this comment.
LGTM. Verified against PR head — root cause is correct, the dataElementsId back-mapping holds in the builder, the selection key is unchanged so filtering behavior is preserved, and the type change is fully migrated across all consumers. Tests cover both the friendlyName and model-fallback paths.
AustinChangLinksys
left a comment
There was a problem hiding this comment.
🤖 Automated Review — Round 1 · c45ee12..ba65c27 (full)
Verdict: 💬 Self-review (comment only) — Austin's own PR; automated review provided for reference only (no self-approve). 0 Critical · 2 Warning · 4 Suggestion.
Standards (Reviewer A — Security/Correctness + Reviewer B — Architecture/Maintainability)
| Conf. | Where | Issue (one-liner) | |
|---|---|---|---|
| 🟡Med | device_filter_provider.dart:244 |
Cross-map key contract not type-enforced: if dataElementsId is absent on any data.nodes entry, lookup silently misses and falls back to meshTopology node's displayName (= model) — silently re-introducing #1157 |
|
| 🟡Med | test/.../device_filter_provider_test.dart |
No test covers the dataElementsId == null partial-miss case (some nodes enriched, some not) — silent degradation path untested |
|
| 💡 | 🟢High | usp_device_filter_panel.dart:132–141 |
[Duplicated Code] options.nodes.map((n) => n.id).toList() computed twice in same build; extract to local final nodeIds |
| 💡 | 🟡Med | device_filter_state.dart:120 |
NodeFilterOption (view model) lives in device_filter_state.dart (provider-state file); consider moving to _shared/models/ for cleaner layering (no 3-layer violation, just clarity) |
Confidence: 🟢High = code-verified · 🟡Med = located + reasoned, not fully confirmed · ⚪Low = speculative, please double-check.
[X] = 1 agent · [XX] = 2 agents flagged independently → higher confidence (Critical only).
Spec (#1157 — "Connected via/to should show node name, not model")
| Conf. | Where | Issue (one-liner) | |
|---|---|---|---|
| 🟡Med | usp_device_detail_view.dart:252, usp_device_list_tile.dart:183 |
Spec step: "Select a client" path — device detail/list-tile "Connected to/via" uses device.parentNodeName which has its own fallback-to-model in MeshNetworkBuilder; PR doesn't touch this path, may still show model on certain firmware |
|
| 💡 | 🟡Med | usp_node_detail_view.dart:316 |
Topology "Connected to" shows parentNode.model directly — not in spec steps but spec title says broadly "Connected via/to info" |
| 💡 | ⚪Low | device_filter_state.dart |
Scope creep (minor): new NodeFilterOption model is an API-surface change not required by spec; benign given 3332 tests passing |
Confidence: 🟢High = code-verified · 🟡Med = located + reasoned, not fully confirmed · ⚪Low = speculative, please double-check.
🔴 Critical findings — none
No Critical issues found in this round.
⚠️ Warning details
W-1 · 🟡Med — Silent dataElementsId miss in cross-map lookup
lib/page/devices/providers/device_filter_provider.dart:237–244
final labelByDataElementsId = {
for (final n in data.nodes)
if (n.dataElementsId != null) n.dataElementsId!: n.displayName,
};
final nodeOptions = data.meshTopology.nodes
.map((n) => NodeFilterOption(
id: n.deviceId,
label: labelByDataElementsId[n.deviceId] ?? n.displayName, // fallback
))
.toList();The mapping relies on the invariant that meshTopology.nodes[i].deviceId == data.nodes[j].dataElementsId. If any data.nodes entry has dataElementsId == null (filtered out of the map at line 239), the lookup at line 244 returns null and falls back to n.displayName on the meshTopology.nodes entry — which only has model, not friendlyName. This silently re-introduces #1157 for that node with no error signal.
No evidence this currently happens in production (the PR + tests show the invariant holds for supported firmware). Recommend adding a debug-mode assertion:
assert(
data.meshTopology.nodes.every((n) => labelByDataElementsId.containsKey(n.deviceId)),
'Some topology nodes have no enriched counterpart — label will fall back to model.',
);Fix: add assertion in debug builds; optionally log a warning in release.
W-2 · 🟡Med — Missing test for partial-miss degradation path
test/page/devices/providers/device_filter_provider_test.dart
Existing tests cover:
- ✅ All nodes:
dataElementsIdset → labels resolve tofriendlyName - ✅ All nodes: no
dataElementsId→ labels fall back to model
Missing:
- ❌ Mixed: node A has
dataElementsIdset, node B hasdataElementsId == null— the per-node fallback behavior is not independently verified.
W-3 (Spec) · 🟡Med — "Select a client" detail/list-tile path not verified
lib/page/devices/views/usp_device_detail_view.dart:252, lib/page/devices/views/components/usp_device_list_tile.dart:183
These display device.parentNodeName, assigned in MeshNetworkBuilder._buildClientDevice() via a MAC-embedded Hosts↔DataElements cross-reference. If that lookup fails (firmware variation), the builder falls back to matchingNode?.model. This PR does not touch that path. PR description says it was already correct; worth a manual confirmation on 3-node hardware given the spec reproduces on specific firmware.
✅ What looks good
- Correct root-cause fix:
deviceFilterOptionsProviderpreviously fed rawmeshTopology.nodes(DataElements, nofriendlyName) directly to the filter; now resolves labels viadata.nodes(Hosts-enriched). The data-flow logic is sound. - Key identity preserved: filtering still uses
NodeFilterOption.id(= DataElements node ID =ClientDevice.parentNodeId), so filter selection/reconciliation logic is unchanged. - View properly decoupled:
usp_device_filter_panel.dartnow only knows{id, label}— consistent withssids/bandspattern; removed leakedNodeEntitydependency from view. - Fallback handled:
?? n.displayNamegracefully covers nodes with no enriched match. - Tests well-structured: the two new provider tests explicitly document the
dataElementsId-based cross-reference invariant and the model-name fallback; fullDevicesDatafixture is written out (not stubbed). - 3332 tests passing,
flutter analyzeclean.
Cross-reviewed by two independent agents (security+correctness / architecture+maintainability). Automated — please sanity-check before merge.
Summary
The Node filter (device list page) showed the router model name (e.g.
MR7500) instead of the user-assigned display name (e.g.Living Room). Fixes #1157.Root cause
The filter options were built directly from
meshTopology.nodes(DataElements), whoseNodeEntityentries carry emptyfriendlyName/hostName— sodisplayNamefell back tomodel. The friendlyName only lives on the fully-built mesh nodes (data.nodes), sourced from the Hosts table.An earlier attempt to swap
n.model→n.displayNamein the view alone did not work, because the underlying data source still had no name to resolve.Fix
NodeFilterOption { id, label }view model — mirroring the other filter dimensions (ssids/bands) so the view never needs to know how a node name is derived.deviceFilterOptionsProvider, resolve the label by matchingmeshTopology.nodes[i].deviceIdback to the built node'sdisplayNameviadataElementsId, falling back tomodelwhen no display name exists.NodeFilterOption.id == meshTopology.nodes[i].deviceId), which is exactly whatClientDevice.parentNodeIdmatches against. Only the display label changed — filtering behavior is unchanged.Testing
flutter analyzeon the 4 changed files — no issues.fvm dart format— no changes needed.friendlyNamewhileidstays the DataElements node id.modelwhen no display name exists.run_tests.sh) — 3332 tests passed.