@matter-server/dashboard 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +76 -0
- package/dist/esm/pages/components/node-details.js +1 -1
- package/dist/esm/pages/components/node-details.js.map +1 -1
- package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-cluster-view.js +9 -4
- package/dist/esm/pages/matter-cluster-view.js.map +1 -1
- package/dist/esm/pages/matter-endpoint-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-endpoint-view.js +8 -2
- package/dist/esm/pages/matter-endpoint-view.js.map +1 -1
- package/dist/esm/pages/matter-node-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-node-view.js +17 -3
- package/dist/esm/pages/matter-node-view.js.map +1 -1
- package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/base-network-graph.js +10 -2
- package/dist/esm/pages/network/base-network-graph.js.map +1 -1
- package/dist/esm/pages/network/device-icons.js +1 -1
- package/dist/esm/pages/network/network-details.d.ts +31 -1
- package/dist/esm/pages/network/network-details.d.ts.map +1 -1
- package/dist/esm/pages/network/network-details.js +233 -15
- package/dist/esm/pages/network/network-details.js.map +1 -1
- package/dist/esm/pages/network/network-types.d.ts +6 -0
- package/dist/esm/pages/network/network-types.d.ts.map +1 -1
- package/dist/esm/pages/network/network-utils.d.ts +26 -0
- package/dist/esm/pages/network/network-utils.d.ts.map +1 -1
- package/dist/esm/pages/network/network-utils.js +71 -3
- package/dist/esm/pages/network/network-utils.js.map +1 -1
- package/dist/esm/pages/network/thread-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/thread-graph.js +5 -2
- package/dist/esm/pages/network/thread-graph.js.map +1 -1
- package/dist/esm/pages/network/update-connections-dialog.d.ts +55 -0
- package/dist/esm/pages/network/update-connections-dialog.d.ts.map +1 -0
- package/dist/esm/pages/network/update-connections-dialog.js +284 -0
- package/dist/esm/pages/network/update-connections-dialog.js.map +6 -0
- package/dist/esm/pages/network/wifi-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/wifi-graph.js +5 -3
- package/dist/esm/pages/network/wifi-graph.js.map +1 -1
- package/dist/esm/util/format_hex.d.ts +18 -0
- package/dist/esm/util/format_hex.d.ts.map +1 -1
- package/dist/esm/util/format_hex.js +21 -1
- package/dist/esm/util/format_hex.js.map +1 -1
- package/dist/web/js/{commission-node-dialog-B1_khzZb.js → commission-node-dialog-CcMuttYO.js} +5 -5
- package/dist/web/js/{commission-node-existing-RpdajrwF.js → commission-node-existing-CqTRDMAr.js} +2 -2
- package/dist/web/js/{commission-node-thread-5f2itkTG.js → commission-node-thread-DgwtTVwK.js} +2 -2
- package/dist/web/js/{commission-node-wifi-DZ_pWqsa.js → commission-node-wifi-XaN2SEnE.js} +2 -2
- package/dist/web/js/{dialog-box-DEUxM4B1.js → dialog-box-COpDD8i7.js} +2 -2
- package/dist/web/js/{fire_event-BczBMT8E.js → fire_event-mDYWi2sw.js} +1 -1
- package/dist/web/js/{log-level-dialog-Cr3PfX1X.js → log-level-dialog-Bc32kZVw.js} +2 -2
- package/dist/web/js/main.js +1 -1
- package/dist/web/js/{matter-dashboard-app-BuCe_Jxf.js → matter-dashboard-app-CrBHT4fT.js} +1824 -208
- package/dist/web/js/{node-binding-dialog-DMiHNDLA.js → node-binding-dialog-C8fqOJiB.js} +2 -2
- package/dist/web/js/prevent_default-D-ohDGsN.js +8 -0
- package/package.json +4 -4
- package/src/pages/components/node-details.ts +1 -1
- package/src/pages/matter-cluster-view.ts +11 -4
- package/src/pages/matter-endpoint-view.ts +10 -3
- package/src/pages/matter-node-view.ts +19 -4
- package/src/pages/network/base-network-graph.ts +17 -3
- package/src/pages/network/device-icons.ts +1 -1
- package/src/pages/network/network-details.ts +281 -16
- package/src/pages/network/network-types.ts +6 -0
- package/src/pages/network/network-utils.ts +109 -0
- package/src/pages/network/thread-graph.ts +7 -1
- package/src/pages/network/update-connections-dialog.ts +327 -0
- package/src/pages/network/wifi-graph.ts +4 -3
- package/src/util/format_hex.ts +39 -0
- package/dist/web/js/prevent_default-D4FX_PIh.js +0 -774
|
@@ -297,6 +297,38 @@ export function parseRouteTable(node: MatterNode): ThreadRoute[] {
|
|
|
297
297
|
});
|
|
298
298
|
}
|
|
299
299
|
|
|
300
|
+
/**
|
|
301
|
+
* Find a route table entry for a specific destination by extended address.
|
|
302
|
+
* Returns the route entry if found, undefined otherwise.
|
|
303
|
+
*/
|
|
304
|
+
export function findRouteByExtAddress(node: MatterNode, targetExtAddr: bigint): ThreadRoute | undefined {
|
|
305
|
+
const routes = parseRouteTable(node);
|
|
306
|
+
return routes.find(route => route.extAddress === targetExtAddr && route.linkEstablished);
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Count the number of routable destinations for a node (from route table).
|
|
311
|
+
* Only counts entries where allocated=true and linkEstablished=true.
|
|
312
|
+
* This is typically only meaningful for router nodes.
|
|
313
|
+
*/
|
|
314
|
+
export function getRoutableDestinationsCount(node: MatterNode): number {
|
|
315
|
+
const routes = parseRouteTable(node);
|
|
316
|
+
return routes.filter(route => route.allocated && route.linkEstablished).length;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
/**
|
|
320
|
+
* Calculate combined bidirectional LQI from route table entry.
|
|
321
|
+
* Returns average of lqiIn and lqiOut if both are non-zero.
|
|
322
|
+
*/
|
|
323
|
+
export function getRouteBidirectionalLqi(route: ThreadRoute): number | undefined {
|
|
324
|
+
if (route.lqiIn > 0 && route.lqiOut > 0) {
|
|
325
|
+
return Math.round((route.lqiIn + route.lqiOut) / 2);
|
|
326
|
+
}
|
|
327
|
+
if (route.lqiIn > 0) return route.lqiIn;
|
|
328
|
+
if (route.lqiOut > 0) return route.lqiOut;
|
|
329
|
+
return undefined;
|
|
330
|
+
}
|
|
331
|
+
|
|
300
332
|
/**
|
|
301
333
|
* Gets the RLOC16 (short address) for a Thread node.
|
|
302
334
|
* Uses attribute 0/53/64 (Rloc16, 0x0040).
|
|
@@ -437,6 +469,21 @@ export function getSignalColor(neighbor: ThreadNeighbor): string {
|
|
|
437
469
|
return SIGNAL_COLOR_WEAK;
|
|
438
470
|
}
|
|
439
471
|
|
|
472
|
+
/**
|
|
473
|
+
* Get signal color based on LQI value alone.
|
|
474
|
+
* Used for route table entries where only LQI is available.
|
|
475
|
+
* @param lqi Link Quality Indicator (0-255, higher is better)
|
|
476
|
+
*/
|
|
477
|
+
export function getSignalColorFromLqi(lqi: number): string {
|
|
478
|
+
if (lqi > LQI_STRONG_THRESHOLD) {
|
|
479
|
+
return SIGNAL_COLOR_STRONG;
|
|
480
|
+
}
|
|
481
|
+
if (lqi > LQI_MEDIUM_THRESHOLD) {
|
|
482
|
+
return SIGNAL_COLOR_MEDIUM;
|
|
483
|
+
}
|
|
484
|
+
return SIGNAL_COLOR_WEAK;
|
|
485
|
+
}
|
|
486
|
+
|
|
440
487
|
/**
|
|
441
488
|
* Gets a human-readable display name for a node.
|
|
442
489
|
* Format: nodeLabel || productName (serialNumber)
|
|
@@ -634,12 +681,58 @@ export function buildThreadConnections(
|
|
|
634
681
|
}
|
|
635
682
|
seenConnections.add(connectionKey);
|
|
636
683
|
|
|
684
|
+
// Look up route table entry for supplementary data (pathCost, bidirectional LQI)
|
|
685
|
+
const routeEntry = findRouteByExtAddress(node, neighbor.extAddress);
|
|
686
|
+
const bidirectionalLqi = routeEntry ? getRouteBidirectionalLqi(routeEntry) : undefined;
|
|
687
|
+
|
|
688
|
+
// Always use neighbor table RSSI/LQI for signal color (most accurate link quality)
|
|
637
689
|
connections.push({
|
|
638
690
|
fromNodeId,
|
|
639
691
|
toNodeId,
|
|
640
692
|
signalColor: getSignalColor(neighbor),
|
|
641
693
|
lqi: neighbor.lqi,
|
|
642
694
|
rssi: neighbor.avgRssi ?? neighbor.lastRssi,
|
|
695
|
+
pathCost: routeEntry?.pathCost,
|
|
696
|
+
bidirectionalLqi,
|
|
697
|
+
});
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
// Check route table for connections not in neighbor table (supplementary data)
|
|
701
|
+
// This helps when neighbor table is stale or incomplete
|
|
702
|
+
const routes = parseRouteTable(node);
|
|
703
|
+
for (const route of routes) {
|
|
704
|
+
if (!route.linkEstablished || !route.allocated) continue;
|
|
705
|
+
|
|
706
|
+
let toNodeId: string | undefined = extAddrMap.get(route.extAddress);
|
|
707
|
+
if (toNodeId === undefined) {
|
|
708
|
+
toNodeId = unknownExtAddrMap.get(route.extAddress);
|
|
709
|
+
}
|
|
710
|
+
if (toNodeId === undefined || toNodeId === fromNodeId) continue;
|
|
711
|
+
|
|
712
|
+
const connectionKey = `${fromNodeId}-${toNodeId}`;
|
|
713
|
+
const reverseKey = `${toNodeId}-${fromNodeId}`;
|
|
714
|
+
|
|
715
|
+
// Only add if we don't already have this connection from neighbor table
|
|
716
|
+
if (seenConnections.has(connectionKey) || seenConnections.has(reverseKey)) {
|
|
717
|
+
continue;
|
|
718
|
+
}
|
|
719
|
+
seenConnections.add(connectionKey);
|
|
720
|
+
|
|
721
|
+
const bidirectionalLqi = getRouteBidirectionalLqi(route);
|
|
722
|
+
const signalColor =
|
|
723
|
+
bidirectionalLqi !== undefined
|
|
724
|
+
? getSignalColorFromLqi(bidirectionalLqi)
|
|
725
|
+
: "var(--md-sys-color-outline, grey)"; // Unknown signal
|
|
726
|
+
|
|
727
|
+
connections.push({
|
|
728
|
+
fromNodeId,
|
|
729
|
+
toNodeId,
|
|
730
|
+
signalColor,
|
|
731
|
+
lqi: bidirectionalLqi ?? 0,
|
|
732
|
+
rssi: null,
|
|
733
|
+
pathCost: route.pathCost,
|
|
734
|
+
bidirectionalLqi,
|
|
735
|
+
fromRouteTable: true,
|
|
643
736
|
});
|
|
644
737
|
}
|
|
645
738
|
}
|
|
@@ -666,6 +759,10 @@ export interface NodeConnection {
|
|
|
666
759
|
isOutgoing: boolean;
|
|
667
760
|
/** Whether this is an unknown/external device */
|
|
668
761
|
isUnknown: boolean;
|
|
762
|
+
/** Path cost from route table (1 = direct, higher = multi-hop). Only available for routers. */
|
|
763
|
+
pathCost?: number;
|
|
764
|
+
/** Bidirectional LQI from route table (average of lqiIn and lqiOut) */
|
|
765
|
+
bidirectionalLqi?: number;
|
|
669
766
|
}
|
|
670
767
|
|
|
671
768
|
/**
|
|
@@ -705,6 +802,10 @@ export function getNodeConnections(
|
|
|
705
802
|
|
|
706
803
|
seenConnectedIds.add(displayId);
|
|
707
804
|
|
|
805
|
+
// Look up route table entry for enhanced data
|
|
806
|
+
const routeEntry = findRouteByExtAddress(node, neighbor.extAddress);
|
|
807
|
+
const bidirectionalLqi = routeEntry ? getRouteBidirectionalLqi(routeEntry) : undefined;
|
|
808
|
+
|
|
708
809
|
connections.push({
|
|
709
810
|
connectedNodeId: displayId,
|
|
710
811
|
connectedNode,
|
|
@@ -714,6 +815,8 @@ export function getNodeConnections(
|
|
|
714
815
|
rssi: neighbor.avgRssi ?? neighbor.lastRssi,
|
|
715
816
|
isOutgoing: true,
|
|
716
817
|
isUnknown,
|
|
818
|
+
pathCost: routeEntry?.pathCost,
|
|
819
|
+
bidirectionalLqi,
|
|
717
820
|
});
|
|
718
821
|
}
|
|
719
822
|
|
|
@@ -734,6 +837,10 @@ export function getNodeConnections(
|
|
|
734
837
|
const otherExtAddr = getThreadExtendedAddress(otherNode);
|
|
735
838
|
const extAddrHex = otherExtAddr ? otherExtAddr.toString(16).toUpperCase().padStart(16, "0") : "Unknown";
|
|
736
839
|
|
|
840
|
+
// Look up route table entry from the other node's perspective
|
|
841
|
+
const routeEntry = findRouteByExtAddress(otherNode, thisExtAddr);
|
|
842
|
+
const bidirectionalLqi = routeEntry ? getRouteBidirectionalLqi(routeEntry) : undefined;
|
|
843
|
+
|
|
737
844
|
connections.push({
|
|
738
845
|
connectedNodeId: otherNodeId,
|
|
739
846
|
connectedNode: otherNode,
|
|
@@ -743,6 +850,8 @@ export function getNodeConnections(
|
|
|
743
850
|
rssi: reverseEntry.avgRssi ?? reverseEntry.lastRssi,
|
|
744
851
|
isOutgoing: false,
|
|
745
852
|
isUnknown: false,
|
|
853
|
+
pathCost: routeEntry?.pathCost,
|
|
854
|
+
bidirectionalLqi,
|
|
746
855
|
});
|
|
747
856
|
}
|
|
748
857
|
}
|
|
@@ -115,6 +115,12 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
115
115
|
// Create edge data for vis.js
|
|
116
116
|
const graphEdges: NetworkGraphEdge[] = connections.map((conn, index) => {
|
|
117
117
|
const isToUnknown = typeof conn.toNodeId === "string" && conn.toNodeId.startsWith("unknown_");
|
|
118
|
+
|
|
119
|
+
// Check if either endpoint is offline - connection data may be stale
|
|
120
|
+
const fromNode = this.nodes[String(conn.fromNodeId)];
|
|
121
|
+
const toNode = this.nodes[String(conn.toNodeId)];
|
|
122
|
+
const hasOfflineEndpoint = fromNode?.available === false || toNode?.available === false;
|
|
123
|
+
|
|
118
124
|
return {
|
|
119
125
|
id: `edge_${index}`,
|
|
120
126
|
from: conn.fromNodeId,
|
|
@@ -125,7 +131,7 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
125
131
|
},
|
|
126
132
|
width: 2,
|
|
127
133
|
title: conn.rssi !== null ? `RSSI: ${conn.rssi} dBm, LQI: ${conn.lqi}` : `LQI: ${conn.lqi}`,
|
|
128
|
-
dashes: isToUnknown, // Dashed lines to unknown devices
|
|
134
|
+
dashes: isToUnknown || hasOfflineEndpoint, // Dashed lines to unknown or offline devices
|
|
129
135
|
};
|
|
130
136
|
});
|
|
131
137
|
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import "@material/web/button/filled-button";
|
|
8
|
+
import "@material/web/button/text-button";
|
|
9
|
+
import "@material/web/checkbox/checkbox";
|
|
10
|
+
import "@material/web/dialog/dialog";
|
|
11
|
+
import type { MatterClient, MatterNode } from "@matter-server/ws-client";
|
|
12
|
+
import { mdiLoading } from "@mdi/js";
|
|
13
|
+
import { LitElement, css, html, nothing, svg } from "lit";
|
|
14
|
+
import { customElement, property, state } from "lit/decorators.js";
|
|
15
|
+
import { getNetworkType } from "./network-utils.js";
|
|
16
|
+
|
|
17
|
+
declare global {
|
|
18
|
+
interface HTMLElementTagNameMap {
|
|
19
|
+
"update-connections-dialog": UpdateConnectionsDialog;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/** Thread network attributes to read */
|
|
24
|
+
const THREAD_ATTRIBUTE_PATHS = ["0/53/7", "0/53/8", "0/51/0"]; // NeighborTable, RouteTable, NetworkInterfaces
|
|
25
|
+
|
|
26
|
+
/** WiFi network attributes to read */
|
|
27
|
+
const WIFI_ATTRIBUTE_PATHS = ["0/54/0", "0/54/3", "0/54/4"]; // BSSID, Channel, RSSI
|
|
28
|
+
|
|
29
|
+
@customElement("update-connections-dialog")
|
|
30
|
+
export class UpdateConnectionsDialog extends LitElement {
|
|
31
|
+
@property({ type: Object })
|
|
32
|
+
public client!: MatterClient;
|
|
33
|
+
|
|
34
|
+
@property({ type: Object })
|
|
35
|
+
public nodes: Record<string, MatterNode> = {};
|
|
36
|
+
|
|
37
|
+
@property({ type: String })
|
|
38
|
+
public selectedNodeType: "online" | "offline" | "unknown" = "online";
|
|
39
|
+
|
|
40
|
+
@property({ type: String })
|
|
41
|
+
public selectedNodeName: string = "";
|
|
42
|
+
|
|
43
|
+
@property()
|
|
44
|
+
public selectedNodeId: number | string | null = null;
|
|
45
|
+
|
|
46
|
+
@property({ type: Array })
|
|
47
|
+
public onlineNeighborIds: string[] = [];
|
|
48
|
+
|
|
49
|
+
@state()
|
|
50
|
+
private _includeNeighbors: boolean = false;
|
|
51
|
+
|
|
52
|
+
@state()
|
|
53
|
+
private _isUpdating: boolean = false;
|
|
54
|
+
|
|
55
|
+
/** Timeout ID for auto-close */
|
|
56
|
+
private _timeoutId: ReturnType<typeof setTimeout> | null = null;
|
|
57
|
+
|
|
58
|
+
/** Track if we've already dispatched close event to prevent double-firing */
|
|
59
|
+
private _hasClosedEvent: boolean = false;
|
|
60
|
+
|
|
61
|
+
override firstUpdated(): void {
|
|
62
|
+
// Open dialog when component is first rendered
|
|
63
|
+
const dialog = this.shadowRoot?.querySelector("md-dialog") as HTMLElement & { show: () => void };
|
|
64
|
+
dialog?.show();
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
override disconnectedCallback(): void {
|
|
68
|
+
super.disconnectedCallback();
|
|
69
|
+
// Clean up timeout when component is removed
|
|
70
|
+
if (this._timeoutId) {
|
|
71
|
+
clearTimeout(this._timeoutId);
|
|
72
|
+
this._timeoutId = null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Get the number of nodes that will be updated.
|
|
78
|
+
*/
|
|
79
|
+
private get _updateCount(): number {
|
|
80
|
+
if (this.selectedNodeType === "online") {
|
|
81
|
+
return this._includeNeighbors ? 1 + this.onlineNeighborIds.length : 1;
|
|
82
|
+
}
|
|
83
|
+
// offline and unknown: update neighbors only
|
|
84
|
+
return this.onlineNeighborIds.length;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Get the attribute paths to read for a node based on its network type.
|
|
89
|
+
*/
|
|
90
|
+
private _getAttributePathsForNode(nodeId: string): string[] {
|
|
91
|
+
const node = this.nodes[nodeId];
|
|
92
|
+
if (!node) return [];
|
|
93
|
+
|
|
94
|
+
const networkType = getNetworkType(node);
|
|
95
|
+
|
|
96
|
+
if (networkType === "thread") {
|
|
97
|
+
return THREAD_ATTRIBUTE_PATHS;
|
|
98
|
+
}
|
|
99
|
+
if (networkType === "wifi") {
|
|
100
|
+
return WIFI_ATTRIBUTE_PATHS;
|
|
101
|
+
}
|
|
102
|
+
// Ethernet and unknown have no dynamic network data
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Get the list of node IDs to update based on current state.
|
|
108
|
+
*/
|
|
109
|
+
private _getNodeIdsToUpdate(): string[] {
|
|
110
|
+
if (this.selectedNodeType === "online") {
|
|
111
|
+
const nodeIds = [String(this.selectedNodeId)];
|
|
112
|
+
if (this._includeNeighbors) {
|
|
113
|
+
nodeIds.push(...this.onlineNeighborIds);
|
|
114
|
+
}
|
|
115
|
+
return nodeIds;
|
|
116
|
+
}
|
|
117
|
+
// offline and unknown: update neighbors only
|
|
118
|
+
return this.onlineNeighborIds;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
private async _executeUpdate(): Promise<void> {
|
|
122
|
+
if (this._isUpdating || this._updateCount === 0) return;
|
|
123
|
+
|
|
124
|
+
this._isUpdating = true;
|
|
125
|
+
|
|
126
|
+
// Set up 30s timeout to auto-close dialog
|
|
127
|
+
this._timeoutId = setTimeout(() => {
|
|
128
|
+
console.warn("Update connections timed out after 30s");
|
|
129
|
+
this._closeDialog();
|
|
130
|
+
}, 30000);
|
|
131
|
+
|
|
132
|
+
try {
|
|
133
|
+
const nodeIds = this._getNodeIdsToUpdate();
|
|
134
|
+
|
|
135
|
+
// Build promises for all node updates
|
|
136
|
+
const updatePromises = nodeIds.map(async nodeIdStr => {
|
|
137
|
+
const node = this.nodes[nodeIdStr];
|
|
138
|
+
if (!node) return;
|
|
139
|
+
|
|
140
|
+
const paths = this._getAttributePathsForNode(nodeIdStr);
|
|
141
|
+
if (paths.length === 0) return;
|
|
142
|
+
|
|
143
|
+
// Use the actual node_id from the node object (number | bigint)
|
|
144
|
+
await this.client.readAttribute(node.node_id, paths);
|
|
145
|
+
});
|
|
146
|
+
|
|
147
|
+
// Wait for all to complete (results come via events, we just need completion)
|
|
148
|
+
await Promise.all(updatePromises);
|
|
149
|
+
|
|
150
|
+
// Close dialog on success
|
|
151
|
+
this._closeDialog();
|
|
152
|
+
} catch (error) {
|
|
153
|
+
console.error("Failed to update connections:", error);
|
|
154
|
+
// Close dialog on error too - don't leave user stuck
|
|
155
|
+
this._closeDialog();
|
|
156
|
+
} finally {
|
|
157
|
+
// Clear timeout if we finished before 30s
|
|
158
|
+
if (this._timeoutId) {
|
|
159
|
+
clearTimeout(this._timeoutId);
|
|
160
|
+
this._timeoutId = null;
|
|
161
|
+
}
|
|
162
|
+
this._isUpdating = false;
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
private _closeDialog(): void {
|
|
167
|
+
// Prevent double-firing the close event
|
|
168
|
+
if (this._hasClosedEvent) return;
|
|
169
|
+
this._hasClosedEvent = true;
|
|
170
|
+
|
|
171
|
+
const dialog = this.shadowRoot?.querySelector("md-dialog") as HTMLElement & { close: () => void };
|
|
172
|
+
dialog?.close();
|
|
173
|
+
// Use 'dialog-closed' to avoid conflicting with network-details 'close' event
|
|
174
|
+
this.dispatchEvent(new CustomEvent("dialog-closed", { bubbles: true, composed: true }));
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/** Handle native dialog closed event (ESC key, backdrop click, etc.) */
|
|
178
|
+
private _handleDialogClosed(): void {
|
|
179
|
+
this._closeDialog();
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
private _handleCheckboxChange(e: Event): void {
|
|
183
|
+
const checkbox = e.target as HTMLInputElement;
|
|
184
|
+
this._includeNeighbors = checkbox.checked;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
private _renderOnlineContent(): unknown {
|
|
188
|
+
return html`
|
|
189
|
+
<p>Refresh network information for "<strong>${this.selectedNodeName}</strong>".</p>
|
|
190
|
+
${this.onlineNeighborIds.length > 0
|
|
191
|
+
? html`
|
|
192
|
+
<label class="checkbox-row">
|
|
193
|
+
<md-checkbox
|
|
194
|
+
?checked=${this._includeNeighbors}
|
|
195
|
+
@change=${this._handleCheckboxChange}
|
|
196
|
+
?disabled=${this._isUpdating}
|
|
197
|
+
></md-checkbox>
|
|
198
|
+
<span
|
|
199
|
+
>Include ${this.onlineNeighborIds.length} connected online
|
|
200
|
+
neighbor${this.onlineNeighborIds.length !== 1 ? "s" : ""}</span
|
|
201
|
+
>
|
|
202
|
+
</label>
|
|
203
|
+
`
|
|
204
|
+
: nothing}
|
|
205
|
+
`;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
private _renderOfflineContent(): unknown {
|
|
209
|
+
return html`
|
|
210
|
+
<p>"<strong>${this.selectedNodeName}</strong>" appears to be offline.</p>
|
|
211
|
+
${this.onlineNeighborIds.length > 0
|
|
212
|
+
? html`
|
|
213
|
+
<p>
|
|
214
|
+
Update network data from its ${this.onlineNeighborIds.length} online
|
|
215
|
+
neighbor${this.onlineNeighborIds.length !== 1 ? "s" : ""} to refresh connection info.
|
|
216
|
+
</p>
|
|
217
|
+
`
|
|
218
|
+
: html` <p>No online neighbors available to update.</p> `}
|
|
219
|
+
`;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
private _renderUnknownContent(): unknown {
|
|
223
|
+
return html`
|
|
224
|
+
<p>This device is not commissioned to this fabric and cannot be queried directly.</p>
|
|
225
|
+
${this.onlineNeighborIds.length > 0
|
|
226
|
+
? html`
|
|
227
|
+
<p>
|
|
228
|
+
Update network data from ${this.onlineNeighborIds.length}
|
|
229
|
+
node${this.onlineNeighborIds.length !== 1 ? "s" : ""} that
|
|
230
|
+
see${this.onlineNeighborIds.length === 1 ? "s" : ""} this device to refresh info.
|
|
231
|
+
</p>
|
|
232
|
+
`
|
|
233
|
+
: html` <p>No online nodes available that see this device.</p> `}
|
|
234
|
+
`;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
override render() {
|
|
238
|
+
const buttonText =
|
|
239
|
+
this._updateCount === 0
|
|
240
|
+
? "No nodes to update"
|
|
241
|
+
: `Update ${this._updateCount} node${this._updateCount !== 1 ? "s" : ""}`;
|
|
242
|
+
|
|
243
|
+
return html`
|
|
244
|
+
<md-dialog @closed=${this._handleDialogClosed}>
|
|
245
|
+
<div slot="headline">Update Connection Data</div>
|
|
246
|
+
<div slot="content">
|
|
247
|
+
${this.selectedNodeType === "online"
|
|
248
|
+
? this._renderOnlineContent()
|
|
249
|
+
: this.selectedNodeType === "offline"
|
|
250
|
+
? this._renderOfflineContent()
|
|
251
|
+
: this._renderUnknownContent()}
|
|
252
|
+
</div>
|
|
253
|
+
<div slot="actions">
|
|
254
|
+
<md-text-button @click=${this._closeDialog} ?disabled=${this._isUpdating}>Cancel</md-text-button>
|
|
255
|
+
<md-filled-button
|
|
256
|
+
@click=${this._executeUpdate}
|
|
257
|
+
?disabled=${this._isUpdating || this._updateCount === 0}
|
|
258
|
+
>
|
|
259
|
+
${this._isUpdating
|
|
260
|
+
? html`<span class="updating-content"
|
|
261
|
+
>${svg`<svg class="spinner" viewBox="0 0 24 24" width="18" height="18"><path fill="currentColor" d="${mdiLoading}"/></svg>`}Updating...</span
|
|
262
|
+
>`
|
|
263
|
+
: buttonText}
|
|
264
|
+
</md-filled-button>
|
|
265
|
+
</div>
|
|
266
|
+
</md-dialog>
|
|
267
|
+
`;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
static override styles = css`
|
|
271
|
+
md-dialog {
|
|
272
|
+
--md-dialog-container-color: var(--md-sys-color-surface, #fff);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
[slot="content"] {
|
|
276
|
+
padding: 0 24px;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
[slot="content"] p {
|
|
280
|
+
margin: 0 0 16px 0;
|
|
281
|
+
font-size: 0.875rem;
|
|
282
|
+
line-height: 1.5;
|
|
283
|
+
color: var(--md-sys-color-on-surface, #333);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
[slot="content"] p:last-child {
|
|
287
|
+
margin-bottom: 0;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
.checkbox-row {
|
|
291
|
+
display: flex;
|
|
292
|
+
align-items: center;
|
|
293
|
+
gap: 8px;
|
|
294
|
+
cursor: pointer;
|
|
295
|
+
font-size: 0.875rem;
|
|
296
|
+
color: var(--md-sys-color-on-surface, #333);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
.updating-content {
|
|
300
|
+
display: inline-flex;
|
|
301
|
+
align-items: center;
|
|
302
|
+
gap: 8px;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
.spinner {
|
|
306
|
+
animation: spin 1s linear infinite;
|
|
307
|
+
flex-shrink: 0;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
.updating-content svg {
|
|
311
|
+
color: inherit;
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
@keyframes spin {
|
|
315
|
+
from {
|
|
316
|
+
transform: rotate(0deg);
|
|
317
|
+
}
|
|
318
|
+
to {
|
|
319
|
+
transform: rotate(360deg);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
md-filled-button {
|
|
324
|
+
min-width: 140px;
|
|
325
|
+
}
|
|
326
|
+
`;
|
|
327
|
+
}
|
|
@@ -69,9 +69,9 @@ export class WiFiGraph extends BaseNetworkGraph {
|
|
|
69
69
|
// Clear stored edge colors since we're rebuilding edges
|
|
70
70
|
this._clearOriginalEdgeColors();
|
|
71
71
|
|
|
72
|
-
// Get WiFi
|
|
72
|
+
// Get WiFi devices only (Ethernet has no dynamic network data)
|
|
73
73
|
const categorized = categorizeDevices(this.nodes);
|
|
74
|
-
const wifiNodeIds =
|
|
74
|
+
const wifiNodeIds = categorized.wifi;
|
|
75
75
|
|
|
76
76
|
if (wifiNodeIds.length === 0) {
|
|
77
77
|
this._nodesDataSet.clear();
|
|
@@ -156,6 +156,7 @@ export class WiFiGraph extends BaseNetworkGraph {
|
|
|
156
156
|
},
|
|
157
157
|
width: 2,
|
|
158
158
|
title: wifiDiag.rssi !== null ? `RSSI: ${wifiDiag.rssi} dBm` : "RSSI: Unknown",
|
|
159
|
+
dashes: isOffline, // Dashed lines for offline devices
|
|
159
160
|
});
|
|
160
161
|
}
|
|
161
162
|
}
|
|
@@ -176,7 +177,7 @@ export class WiFiGraph extends BaseNetworkGraph {
|
|
|
176
177
|
|
|
177
178
|
override render() {
|
|
178
179
|
const categorized = categorizeDevices(this.nodes);
|
|
179
|
-
const wifiCount = categorized.wifi.length
|
|
180
|
+
const wifiCount = categorized.wifi.length;
|
|
180
181
|
|
|
181
182
|
if (wifiCount === 0) {
|
|
182
183
|
return html`
|
package/src/util/format_hex.ts
CHANGED
|
@@ -43,3 +43,42 @@ export function formatNodeAddress(fabricIndex: number | undefined, nodeId: numbe
|
|
|
43
43
|
const fabricPart = fabricIndex !== undefined ? fabricIndex : "?";
|
|
44
44
|
return `@${fabricPart}:${nodeId.toString(16)}`;
|
|
45
45
|
}
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* Get the effective fabric index for formatting a node address.
|
|
49
|
+
* Returns undefined for test nodes or when fabric index is not available,
|
|
50
|
+
* which will result in "?" being displayed.
|
|
51
|
+
*
|
|
52
|
+
* @param serverFabricIndex - The fabric_index from client.serverInfo
|
|
53
|
+
* @param isTestNode - Whether this is a test node (from isTestNodeId())
|
|
54
|
+
*/
|
|
55
|
+
export function getEffectiveFabricIndex(
|
|
56
|
+
serverFabricIndex: number | undefined,
|
|
57
|
+
isTestNode: boolean,
|
|
58
|
+
): number | undefined {
|
|
59
|
+
return isTestNode || serverFabricIndex === undefined ? undefined : serverFabricIndex;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Format a node address, handling string node IDs.
|
|
64
|
+
* Useful when node IDs come from various sources (string keys, numbers, bigints).
|
|
65
|
+
*
|
|
66
|
+
* @param fabricIndex - The fabric index (or undefined for "?")
|
|
67
|
+
* @param nodeId - The node ID as string, number, or bigint
|
|
68
|
+
* @returns Formatted address like "@1:7b" or empty string if nodeId is invalid
|
|
69
|
+
*/
|
|
70
|
+
export function formatNodeAddressFromAny(fabricIndex: number | undefined, nodeId: number | bigint | string): string {
|
|
71
|
+
let numericId: bigint;
|
|
72
|
+
if (typeof nodeId === "bigint") {
|
|
73
|
+
numericId = nodeId;
|
|
74
|
+
} else if (typeof nodeId === "number") {
|
|
75
|
+
numericId = BigInt(nodeId);
|
|
76
|
+
} else {
|
|
77
|
+
try {
|
|
78
|
+
numericId = BigInt(nodeId);
|
|
79
|
+
} catch {
|
|
80
|
+
return "";
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return formatNodeAddress(fabricIndex, numericId);
|
|
84
|
+
}
|