@matter-server/dashboard 1.2.4 → 1.2.5
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 +15 -2
- package/dist/esm/pages/components/node-details.d.ts.map +1 -1
- package/dist/esm/pages/components/node-details.js +14 -1
- package/dist/esm/pages/components/node-details.js.map +1 -1
- package/dist/esm/pages/matter-cluster-view.d.ts +1 -0
- package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-cluster-view.js +8 -0
- package/dist/esm/pages/matter-cluster-view.js.map +1 -1
- package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -1
- package/dist/esm/pages/matter-dashboard-app.js +1 -0
- package/dist/esm/pages/matter-dashboard-app.js.map +1 -1
- package/dist/esm/pages/network/network-details.d.ts.map +1 -1
- package/dist/esm/pages/network/network-details.js +20 -7
- package/dist/esm/pages/network/network-details.js.map +1 -1
- package/dist/esm/pages/network/network-types.d.ts +2 -0
- package/dist/esm/pages/network/network-types.d.ts.map +1 -1
- package/dist/esm/pages/network/network-utils.d.ts +16 -0
- package/dist/esm/pages/network/network-utils.d.ts.map +1 -1
- package/dist/esm/pages/network/network-utils.js +33 -0
- 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 +13 -0
- package/dist/esm/pages/network/thread-graph.js.map +1 -1
- package/dist/esm/util/pairing-code.d.ts +18 -0
- package/dist/esm/util/pairing-code.d.ts.map +1 -0
- package/dist/esm/util/pairing-code.js +24 -0
- package/dist/esm/util/pairing-code.js.map +6 -0
- package/dist/web/js/{attribute-write-dialog-fBF9_jCK.js → attribute-write-dialog-yHLoNOkW.js} +1 -1
- package/dist/web/js/{command-invoke-dialog-DWMBX1gN.js → command-invoke-dialog-ByzPtgEb.js} +1 -1
- package/dist/web/js/{commission-node-dialog-BptZBF2k.js → commission-node-dialog-BlT9WOki.js} +5 -5
- package/dist/web/js/{commission-node-existing-yUyavzOc.js → commission-node-existing-DMPiGpS5.js} +1 -1
- package/dist/web/js/{commission-node-thread-Cb5v-CtI.js → commission-node-thread-D3X-Not8.js} +1 -1
- package/dist/web/js/{commission-node-wifi-R0fGOeQ-.js → commission-node-wifi-3sLvZVvY.js} +1 -1
- package/dist/web/js/{dialog-box-DLCbBR3S.js → dialog-box-DXneB7f7.js} +1 -1
- package/dist/web/js/{fabric-label-dialog-tW-AnWBx.js → fabric-label-dialog-DoBL1t3o.js} +1 -1
- package/dist/web/js/main.js +1 -1
- package/dist/web/js/{matter-dashboard-app-BGWRh_7q.js → matter-dashboard-app-mMXFHv8v.js} +1787 -17
- package/dist/web/js/{node-acl-add-dialog-DMKY5qsl.js → node-acl-add-dialog-DJrFkAp9.js} +1 -1
- package/dist/web/js/{node-binding-dialog-bephut8j.js → node-binding-dialog-CQt-LbiG.js} +1 -1
- package/dist/web/js/{node-label-dialog-CHDD9Bk1.js → node-label-dialog-D1CyUaCS.js} +1 -1
- package/dist/web/js/{settings-dialog-BT_YDlXZ.js → settings-dialog-B9ctg2Ji.js} +1 -1
- package/package.json +8 -5
- package/src/pages/components/node-details.ts +14 -1
- package/src/pages/matter-cluster-view.ts +9 -0
- package/src/pages/matter-dashboard-app.ts +3 -0
- package/src/pages/network/network-details.ts +22 -7
- package/src/pages/network/network-types.ts +2 -0
- package/src/pages/network/network-utils.ts +45 -0
- package/src/pages/network/thread-graph.ts +17 -0
- package/src/util/pairing-code.ts +32 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i, c, a as clientContext, n, r, b as i$1, P as Privilege, v as nodeIdKey, x as aclCapacity, s as showAlertDialog, y as targetServerClusters, z as clusters, B as AuthMode, C as addAclEntry, E as PRIVILEGE_NAMES, d as b, F as mdiClose, G as getDeviceName, h as handleAsync, g as t } from './matter-dashboard-app-
|
|
1
|
+
import { i, c, a as clientContext, n, r, b as i$1, P as Privilege, v as nodeIdKey, x as aclCapacity, s as showAlertDialog, y as targetServerClusters, z as clusters, B as AuthMode, C as addAclEntry, E as PRIVILEGE_NAMES, d as b, F as mdiClose, G as getDeviceName, h as handleAsync, g as t } from './matter-dashboard-app-mMXFHv8v.js';
|
|
2
2
|
import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
|
|
3
3
|
import './main.js';
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i, c, a as clientContext, n, r, b as i$1, v as nodeIdKey, z as clusters, s as showAlertDialog, H as targetAclCapacityForBinding, I as addBinding, J as bindableClusters, A, d as b, G as getDeviceName, K as getEndpointDeviceTypes, h as handleAsync, g as t } from './matter-dashboard-app-
|
|
1
|
+
import { i, c, a as clientContext, n, r, b as i$1, v as nodeIdKey, z as clusters, s as showAlertDialog, H as targetAclCapacityForBinding, I as addBinding, J as bindableClusters, A, d as b, G as getDeviceName, K as getEndpointDeviceTypes, h as handleAsync, g as t } from './matter-dashboard-app-mMXFHv8v.js';
|
|
2
2
|
import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
|
|
3
3
|
import './main.js';
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { r, n, g as t, b as i, M as MAX_NODE_LABEL_LENGTH, d as b, w as writeNodeLabel, s as showAlertDialog } from './matter-dashboard-app-
|
|
1
|
+
import { r, n, g as t, b as i, M as MAX_NODE_LABEL_LENGTH, d as b, w as writeNodeLabel, s as showAlertDialog } from './matter-dashboard-app-mMXFHv8v.js';
|
|
2
2
|
import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
|
|
3
3
|
import './main.js';
|
|
4
4
|
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { i, c, a as clientContext, t as tickContext, r, e, b as i$1, f as fireAndForget, s as showAlertDialog, d as b, A, h as handleAsync, g as t, n, D as DevModeService, m as mdiWifi, j as mdiPlus, k as mdiPencil, l as mdiDelete, o as mdiEyeOff, p as mdiEye, q as mdiAccessPoint } from './matter-dashboard-app-
|
|
1
|
+
import { i, c, a as clientContext, t as tickContext, r, e, b as i$1, f as fireAndForget, s as showAlertDialog, d as b, A, h as handleAsync, g as t, n, D as DevModeService, m as mdiWifi, j as mdiPlus, k as mdiPencil, l as mdiDelete, o as mdiEyeOff, p as mdiEye, q as mdiAccessPoint } from './matter-dashboard-app-mMXFHv8v.js';
|
|
2
2
|
import { p as preventDefault } from './prevent_default-D-ohDGsN.js';
|
|
3
3
|
import './main.js';
|
|
4
4
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@matter-server/dashboard",
|
|
3
|
-
"version": "1.2.
|
|
3
|
+
"version": "1.2.5",
|
|
4
4
|
"description": "Dashboard for OHF Matter Server",
|
|
5
5
|
"homepage": "https://github.com/matter-js/matterjs-server",
|
|
6
6
|
"bugs": {
|
|
@@ -28,27 +28,30 @@
|
|
|
28
28
|
"generate": "tsx scripts/generate-descriptions.ts",
|
|
29
29
|
"build": "npm run generate && nacho-build && npm run bundle",
|
|
30
30
|
"build-clean": "npm run generate && nacho-build --clean && npm run bundle",
|
|
31
|
-
"bundle": "rollup -c"
|
|
31
|
+
"bundle": "rollup -c",
|
|
32
|
+
"test": "matter-test --force-exit"
|
|
32
33
|
},
|
|
33
34
|
"dependencies": {
|
|
34
35
|
"@lit/context": "^1.1.6",
|
|
35
36
|
"@material/web": "^2.4.1",
|
|
36
|
-
"@matter-server/custom-clusters": "1.2.
|
|
37
|
-
"@matter-server/ws-client": "1.2.
|
|
37
|
+
"@matter-server/custom-clusters": "1.2.5",
|
|
38
|
+
"@matter-server/ws-client": "1.2.5",
|
|
38
39
|
"@mdi/js": "^7.4.47",
|
|
39
40
|
"lit": "^3.3.3",
|
|
41
|
+
"qrcode-generator": "^2.0.4",
|
|
40
42
|
"tslib": "^2.8.1",
|
|
41
43
|
"vis-network": "^10.1.0"
|
|
42
44
|
},
|
|
43
45
|
"devDependencies": {
|
|
44
46
|
"@babel/preset-env": "^8.0.2",
|
|
45
|
-
"@matter/main": "0.17.5
|
|
47
|
+
"@matter/main": "0.17.5",
|
|
46
48
|
"@rollup/plugin-babel": "^7.1.0",
|
|
47
49
|
"@rollup/plugin-commonjs": "^29.0.3",
|
|
48
50
|
"@rollup/plugin-json": "^6.1.0",
|
|
49
51
|
"@rollup/plugin-node-resolve": "^16.0.3",
|
|
50
52
|
"@rollup/plugin-terser": "^1.0.0",
|
|
51
53
|
"@rollup/plugin-typescript": "^12.3.0",
|
|
54
|
+
"@types/qrcode-generator": "^0.0.16",
|
|
52
55
|
"rollup": "^4.62.2",
|
|
53
56
|
"rollup-plugin-copy": "^3.5.0",
|
|
54
57
|
"serve": "^14.2.6"
|
|
@@ -26,6 +26,7 @@ import "../camera-overlay.js";
|
|
|
26
26
|
import { getDeviceIcon } from "../../util/device-icons.js";
|
|
27
27
|
import { getEndpointDeviceTypes } from "../../util/endpoints.js";
|
|
28
28
|
import { ICD_CLUSTER_ID, icdBadge } from "../../util/icd.js";
|
|
29
|
+
import { formatManualPairingCode, renderPairingQrCodeDataUri } from "../../util/pairing-code.js";
|
|
29
30
|
import { bindingContext } from "./context.js";
|
|
30
31
|
|
|
31
32
|
/** Map updateState values to user-friendly labels */
|
|
@@ -320,9 +321,21 @@ export class NodeDetails extends LitElement {
|
|
|
320
321
|
}
|
|
321
322
|
try {
|
|
322
323
|
const shareCode = await this.client.openCommissioningWindow(this.node!.node_id);
|
|
324
|
+
const manualCode = formatManualPairingCode(shareCode.setup_manual_code);
|
|
325
|
+
const qrDataUri = renderPairingQrCodeDataUri(shareCode.setup_qr_code);
|
|
323
326
|
showAlertDialog({
|
|
324
327
|
title: "Share device",
|
|
325
|
-
text: `
|
|
328
|
+
text: html`
|
|
329
|
+
<div style="display:flex;flex-direction:column;align-items:center;gap:16px;">
|
|
330
|
+
<img src=${qrDataUri} alt="Commissioning QR code" style="width:200px;height:200px;" />
|
|
331
|
+
<div style="text-align:center;">
|
|
332
|
+
<div style="font-size:0.8rem;color:var(--md-sys-color-on-surface-variant);">Setup code</div>
|
|
333
|
+
<div style="font-size:1.4rem;font-family:monospace;letter-spacing:0.05em;">
|
|
334
|
+
${manualCode}
|
|
335
|
+
</div>
|
|
336
|
+
</div>
|
|
337
|
+
</div>
|
|
338
|
+
`,
|
|
326
339
|
});
|
|
327
340
|
} catch (err: any) {
|
|
328
341
|
showAlertDialog({
|
|
@@ -377,6 +377,7 @@ class MatterClusterView extends LitElement {
|
|
|
377
377
|
// Reset per-attribute refresh state when navigating to a different cluster/endpoint.
|
|
378
378
|
if (changedProperties.has("cluster") || changedProperties.has("endpoint")) {
|
|
379
379
|
this._refreshState = {};
|
|
380
|
+
this._scrollCommandPanelIntoView();
|
|
380
381
|
}
|
|
381
382
|
|
|
382
383
|
// After render, find and configure the cluster commands component
|
|
@@ -391,6 +392,14 @@ class MatterClusterView extends LitElement {
|
|
|
391
392
|
}
|
|
392
393
|
}
|
|
393
394
|
|
|
395
|
+
// Route change resets scroll to top; for clusters with a command panel land on the panel instead.
|
|
396
|
+
private _scrollCommandPanelIntoView() {
|
|
397
|
+
requestAnimationFrame(() => {
|
|
398
|
+
const container = this.shadowRoot?.getElementById("cluster-commands-container");
|
|
399
|
+
container?.scrollIntoView({ block: "start", behavior: "auto" });
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
394
403
|
private _goBack() {
|
|
395
404
|
history.back();
|
|
396
405
|
}
|
|
@@ -64,6 +64,9 @@ class MatterDashboardApp extends LitElement {
|
|
|
64
64
|
const hash = location.hash.substring(1);
|
|
65
65
|
const pathParts = hash.split("/");
|
|
66
66
|
|
|
67
|
+
// Single scroll container: without this a route change keeps the previous view's offset.
|
|
68
|
+
window.scrollTo(0, 0);
|
|
69
|
+
|
|
67
70
|
// Reset initial selected node
|
|
68
71
|
this._initialSelectedNodeId = null;
|
|
69
72
|
|
|
@@ -37,12 +37,16 @@ import {
|
|
|
37
37
|
formatThreadVersion,
|
|
38
38
|
getDeviceName,
|
|
39
39
|
getNetworkType,
|
|
40
|
+
DIAGNOSTIC_MESH_NODE_EXPLANATION,
|
|
41
|
+
EXTERNAL_ROUTER_CAPABLE_NOTE,
|
|
42
|
+
EXTERNAL_THREAD_DEVICE_CASES,
|
|
40
43
|
getNodeConnectionsFromPairs,
|
|
41
44
|
getRoutableDestinationsCount,
|
|
42
45
|
getSignalColorFromRssi,
|
|
43
46
|
getThreadChannel,
|
|
44
47
|
getThreadExtendedAddressHex,
|
|
45
48
|
getThreadRole,
|
|
49
|
+
getThreadRoleDescription,
|
|
46
50
|
getThreadRoleName,
|
|
47
51
|
getThreadVersion,
|
|
48
52
|
getWiFiDiagnostics,
|
|
@@ -237,6 +241,7 @@ export class NetworkDetails extends LitElement {
|
|
|
237
241
|
<span class="label">Role:</span>
|
|
238
242
|
<span class="value">${getThreadRoleName(threadRole)}</span>
|
|
239
243
|
</div>
|
|
244
|
+
<p class="hint-text inline-note">${getThreadRoleDescription(threadRole)}</p>
|
|
240
245
|
${threadVersion !== undefined
|
|
241
246
|
? html`
|
|
242
247
|
<div class="info-row">
|
|
@@ -482,10 +487,7 @@ export class NetworkDetails extends LitElement {
|
|
|
482
487
|
|
|
483
488
|
<md-divider></md-divider>
|
|
484
489
|
<div class="section">
|
|
485
|
-
<p class="hint-text">
|
|
486
|
-
This node was inferred from Border Router diagnostics (Route64 / child table) and is not
|
|
487
|
-
commissioned to this fabric, so no device details are available.
|
|
488
|
-
</p>
|
|
490
|
+
<p class="hint-text">${DIAGNOSTIC_MESH_NODE_EXPLANATION}</p>
|
|
489
491
|
</div>
|
|
490
492
|
`;
|
|
491
493
|
}
|
|
@@ -504,6 +506,9 @@ export class NetworkDetails extends LitElement {
|
|
|
504
506
|
<span class="label">Type:</span>
|
|
505
507
|
<span class="value">${unknown.isRouter ? "Router (external)" : "End Device (external)"}</span>
|
|
506
508
|
</div>
|
|
509
|
+
${unknown.isRouter
|
|
510
|
+
? html`<p class="hint-text inline-note">${EXTERNAL_ROUTER_CAPABLE_NOTE}</p>`
|
|
511
|
+
: nothing}
|
|
507
512
|
<div class="info-row">
|
|
508
513
|
<span class="label">Extended Address:</span>
|
|
509
514
|
<span class="value mono">${unknown.extAddressHex}</span>
|
|
@@ -539,10 +544,12 @@ export class NetworkDetails extends LitElement {
|
|
|
539
544
|
<md-divider></md-divider>
|
|
540
545
|
<div class="section">
|
|
541
546
|
<p class="hint-text">
|
|
542
|
-
This device appears in Thread neighbor
|
|
543
|
-
|
|
544
|
-
Apple and Aqara), or a device from another Matter ecosystem.
|
|
547
|
+
This device appears in a commissioned node's Thread neighbor table but is not part of this fabric.
|
|
548
|
+
It may be:
|
|
545
549
|
</p>
|
|
550
|
+
<ul class="hint-text">
|
|
551
|
+
${EXTERNAL_THREAD_DEVICE_CASES.map(c => html`<li>${c}</li>`)}
|
|
552
|
+
</ul>
|
|
546
553
|
</div>
|
|
547
554
|
`;
|
|
548
555
|
}
|
|
@@ -1828,6 +1835,14 @@ export class NetworkDetails extends LitElement {
|
|
|
1828
1835
|
margin: 0;
|
|
1829
1836
|
}
|
|
1830
1837
|
|
|
1838
|
+
.hint-text.inline-note {
|
|
1839
|
+
margin-top: 6px;
|
|
1840
|
+
margin-bottom: 4px;
|
|
1841
|
+
padding-left: 10px;
|
|
1842
|
+
border-left: 2px solid var(--md-sys-color-outline-variant, #ccc);
|
|
1843
|
+
font-size: 0.72rem;
|
|
1844
|
+
}
|
|
1845
|
+
|
|
1831
1846
|
.connected-nodes-list {
|
|
1832
1847
|
display: flex;
|
|
1833
1848
|
flex-direction: column;
|
|
@@ -35,6 +35,8 @@ export interface NetworkGraphNode {
|
|
|
35
35
|
shape: "image";
|
|
36
36
|
networkType: NetworkType;
|
|
37
37
|
threadRole?: ThreadRoutingRole;
|
|
38
|
+
/** Hover tooltip (vis.js): role or unknown/external explanation. */
|
|
39
|
+
title?: string;
|
|
38
40
|
/** Whether the node is offline */
|
|
39
41
|
offline?: boolean;
|
|
40
42
|
/** Whether this is an unknown/external device */
|
|
@@ -150,6 +150,51 @@ export function getThreadRoleName(role: number | undefined): string {
|
|
|
150
150
|
}
|
|
151
151
|
}
|
|
152
152
|
|
|
153
|
+
/** Tooltip-friendly long form of {@link getThreadRoleName}. */
|
|
154
|
+
export function getThreadRoleDescription(role: number | undefined): string {
|
|
155
|
+
switch (role) {
|
|
156
|
+
case 2:
|
|
157
|
+
return "Sleepy End Device: keeps its radio off while idle to save battery and reaches the mesh only through a parent router. The links shown for it can include stale router-table entries for Thread addresses it no longer uses.";
|
|
158
|
+
case 3:
|
|
159
|
+
return "End Device: a leaf node that reaches the mesh through a parent router and does not route for other nodes. The links shown for it can include stale router-table entries for old or unused Thread addresses.";
|
|
160
|
+
case 4:
|
|
161
|
+
return "REED (Router-Eligible End Device): currently acts as an end device but can be promoted to a full Router when the mesh needs more routing capacity.";
|
|
162
|
+
case 5:
|
|
163
|
+
return "Router: forwards traffic for other nodes in the Thread mesh.";
|
|
164
|
+
case 6:
|
|
165
|
+
return "Leader: the elected node that manages router assignments for the Thread network.";
|
|
166
|
+
case 0:
|
|
167
|
+
case 1:
|
|
168
|
+
return "Thread routing role is unassigned or unspecified.";
|
|
169
|
+
default:
|
|
170
|
+
return "Thread routing role could not be determined.";
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Cases a Thread node shown as unknown/external may actually be. These devices are
|
|
176
|
+
* inferred from a commissioned node's neighbor table; the code cannot tell the cases
|
|
177
|
+
* apart, so the copy enumerates the possibilities rather than asserting one.
|
|
178
|
+
*/
|
|
179
|
+
export const EXTERNAL_THREAD_DEVICE_CASES = [
|
|
180
|
+
"a device commissioned to another Matter fabric on the same Thread network",
|
|
181
|
+
"a native (non-Matter) Thread accessory",
|
|
182
|
+
"a Border Router whose Thread radio MAC differs from its MeshCoP border-agent ID (common with Apple and Aqara)",
|
|
183
|
+
"a stale neighbor entry for a device that has left",
|
|
184
|
+
] as const;
|
|
185
|
+
|
|
186
|
+
/** Single-sentence form of {@link EXTERNAL_THREAD_DEVICE_CASES} for plain-text tooltips. */
|
|
187
|
+
export const EXTERNAL_THREAD_DEVICE_EXPLANATION = `Seen in a commissioned node's Thread neighbor table but not part of this fabric. It may be ${EXTERNAL_THREAD_DEVICE_CASES.slice(0, -1).join(", ")}, or ${EXTERNAL_THREAD_DEVICE_CASES[EXTERNAL_THREAD_DEVICE_CASES.length - 1]}.`;
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* `isRouter` for an external neighbor is derived from rx-on-when-idle, so it means
|
|
191
|
+
* router-capable (mains-powered), not a confirmed routing role.
|
|
192
|
+
*/
|
|
193
|
+
export const EXTERNAL_ROUTER_CAPABLE_NOTE = `"Router" here means the neighbor advertised rx-on-when-idle (mains-powered / router-capable), not a confirmed routing role.`;
|
|
194
|
+
|
|
195
|
+
export const DIAGNOSTIC_MESH_NODE_EXPLANATION =
|
|
196
|
+
"Inferred from Border Router diagnostics (Route64 / child table) and not commissioned to this fabric, so no device details are available.";
|
|
197
|
+
|
|
153
198
|
/**
|
|
154
199
|
* Gets WiFi security type name.
|
|
155
200
|
*/
|
|
@@ -27,6 +27,9 @@ import {
|
|
|
27
27
|
buildRloc16Map,
|
|
28
28
|
buildThreadEdgePairs,
|
|
29
29
|
decodeMeshcopStateBitmap,
|
|
30
|
+
DIAGNOSTIC_MESH_NODE_EXPLANATION,
|
|
31
|
+
EXTERNAL_ROUTER_CAPABLE_NOTE,
|
|
32
|
+
EXTERNAL_THREAD_DEVICE_EXPLANATION,
|
|
30
33
|
findDiagnosticMeshNodes,
|
|
31
34
|
findUnknownDevices,
|
|
32
35
|
makeDiagnosticRloc16Resolver,
|
|
@@ -36,6 +39,7 @@ import {
|
|
|
36
39
|
getNetworkType,
|
|
37
40
|
getThreadExtendedAddressHex,
|
|
38
41
|
getThreadRole,
|
|
42
|
+
getThreadRoleDescription,
|
|
39
43
|
mergeDiagnosticEdges,
|
|
40
44
|
signalLevelToColor,
|
|
41
45
|
stripMdnsHostname,
|
|
@@ -340,6 +344,7 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
340
344
|
return {
|
|
341
345
|
id: nodeId,
|
|
342
346
|
label: getDeviceName(node),
|
|
347
|
+
title: getThreadRoleDescription(threadRole),
|
|
343
348
|
image: createNodeIconDataUrl(node, threadRole, false, isOffline),
|
|
344
349
|
shape: "image" as const,
|
|
345
350
|
networkType: "thread" as const,
|
|
@@ -401,9 +406,16 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
401
406
|
const decodedState = decodeMeshcopStateBitmap(device.stateBitmapHex);
|
|
402
407
|
const isLeader = decodedState?.threadRoleValue === 3;
|
|
403
408
|
const isPrimaryBbr = decodedState?.bbr === true && decodedState.bbrFunction === "primary";
|
|
409
|
+
const brRole = new Array<string>();
|
|
410
|
+
if (isLeader) brRole.push("currently the Thread Leader");
|
|
411
|
+
if (isPrimaryBbr) brRole.push("Primary Backbone Border Router (BBR)");
|
|
412
|
+
const brTitle =
|
|
413
|
+
"Thread Border Router bridging the Thread mesh to the IP network" +
|
|
414
|
+
(brRole.length > 0 ? ` — ${brRole.join("; ")}.` : ".");
|
|
404
415
|
graphNodes.push({
|
|
405
416
|
id: device.id,
|
|
406
417
|
label,
|
|
418
|
+
title: brTitle,
|
|
407
419
|
image: createBorderRouterIconDataUrl(isSelected, isLeader, isPrimaryBbr),
|
|
408
420
|
shape: "image" as const,
|
|
409
421
|
networkType: "thread" as const,
|
|
@@ -415,9 +427,13 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
415
427
|
const typeLabel =
|
|
416
428
|
diagNode?.vendorName !== undefined ? `${baseType} (${diagNode.vendorName})` : baseType;
|
|
417
429
|
const suffix = device.networkName !== undefined ? `\n${device.networkName}` : "";
|
|
430
|
+
const title = device.isRouter
|
|
431
|
+
? `${EXTERNAL_THREAD_DEVICE_EXPLANATION} ${EXTERNAL_ROUTER_CAPABLE_NOTE}`
|
|
432
|
+
: EXTERNAL_THREAD_DEVICE_EXPLANATION;
|
|
418
433
|
graphNodes.push({
|
|
419
434
|
id: device.id,
|
|
420
435
|
label: `${typeLabel} [${device.extAddressHex.slice(-8)}]${suffix}`,
|
|
436
|
+
title,
|
|
421
437
|
image: createUnknownDeviceIconDataUrl(device.isRouter, isSelected),
|
|
422
438
|
shape: "image" as const,
|
|
423
439
|
networkType: "thread" as const,
|
|
@@ -485,6 +501,7 @@ export class ThreadGraph extends BaseNetworkGraph {
|
|
|
485
501
|
graphNodes.push({
|
|
486
502
|
id: meshNode.id,
|
|
487
503
|
label,
|
|
504
|
+
title: DIAGNOSTIC_MESH_NODE_EXPLANATION,
|
|
488
505
|
image: createUnknownDeviceIconDataUrl(meshNode.isRouter, meshNode.id === this._selectedNodeId),
|
|
489
506
|
shape: "image" as const,
|
|
490
507
|
networkType: "thread" as const,
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import qrcode from "qrcode-generator";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Format a Matter manual pairing code for display. The 11-digit short code is
|
|
11
|
+
* grouped as `XXXX-XXX-XXXX` per the Matter spec; codes of any other length are
|
|
12
|
+
* returned stripped to their digits, or unchanged when they contain no digits.
|
|
13
|
+
*/
|
|
14
|
+
export function formatManualPairingCode(code: string): string {
|
|
15
|
+
const digits = code.replace(/\D/g, "");
|
|
16
|
+
if (digits.length === 11) {
|
|
17
|
+
return `${digits.slice(0, 4)}-${digits.slice(4, 7)}-${digits.slice(7, 11)}`;
|
|
18
|
+
}
|
|
19
|
+
return digits || code;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Render a Matter QR pairing payload (the `MT:...` string) as an SVG data URI.
|
|
24
|
+
* The generated SVG carries an opaque white background, so the code stays
|
|
25
|
+
* scannable regardless of the surrounding theme.
|
|
26
|
+
*/
|
|
27
|
+
export function renderPairingQrCodeDataUri(qrPayload: string, cellSize = 5, margin = 4): string {
|
|
28
|
+
const qr = qrcode(0, "M");
|
|
29
|
+
qr.addData(qrPayload);
|
|
30
|
+
qr.make();
|
|
31
|
+
return `data:image/svg+xml,${encodeURIComponent(qr.createSvgTag(cellSize, margin))}`;
|
|
32
|
+
}
|