@matter-server/dashboard 0.3.4 → 0.3.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/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts.map +1 -1
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js +44 -17
- package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +1 -1
- package/dist/esm/entrypoint/main.js +1 -1
- package/dist/esm/entrypoint/main.js.map +1 -1
- package/dist/esm/pages/matter-network-view.d.ts +5 -0
- package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
- package/dist/esm/pages/matter-network-view.js +96 -13
- package/dist/esm/pages/matter-network-view.js.map +1 -1
- package/dist/esm/pages/network/base-network-graph.d.ts +34 -0
- package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -1
- package/dist/esm/pages/network/base-network-graph.js +106 -23
- package/dist/esm/pages/network/base-network-graph.js.map +1 -1
- package/dist/esm/util/matter-status.d.ts +49 -0
- package/dist/esm/util/matter-status.d.ts.map +1 -0
- package/dist/esm/util/matter-status.js +110 -0
- package/dist/esm/util/matter-status.js.map +6 -0
- package/dist/web/js/{commission-node-dialog-CcMuttYO.js → commission-node-dialog-DBugVQOl.js} +4 -4
- package/dist/web/js/{commission-node-existing-CqTRDMAr.js → commission-node-existing-ts2MN2bQ.js} +2 -2
- package/dist/web/js/{commission-node-thread-DgwtTVwK.js → commission-node-thread-CixfNuM3.js} +2 -2
- package/dist/web/js/{commission-node-wifi-XaN2SEnE.js → commission-node-wifi-CWC30EYn.js} +2 -2
- package/dist/web/js/{dialog-box-COpDD8i7.js → dialog-box-SeMuFiWx.js} +1 -1
- package/dist/web/js/{fire_event-mDYWi2sw.js → fire_event-B63oGTcK.js} +1 -1
- package/dist/web/js/{log-level-dialog-Bc32kZVw.js → log-level-dialog-J0gFiLLM.js} +1 -1
- package/dist/web/js/main.js +2 -2
- package/dist/web/js/{matter-dashboard-app-CrBHT4fT.js → matter-dashboard-app-D7YWrgXj.js} +222 -43
- package/dist/web/js/{node-binding-dialog-C8fqOJiB.js → node-binding-dialog-DSBwIJcQ.js} +146 -18
- package/package.json +6 -6
- package/src/components/dialogs/binding/node-binding-dialog.ts +45 -21
- package/src/entrypoint/main.ts +2 -0
- package/src/pages/matter-network-view.ts +102 -13
- package/src/pages/network/base-network-graph.ts +130 -30
- package/src/util/matter-status.ts +165 -0
|
@@ -23,6 +23,9 @@ export abstract class BaseNetworkGraph extends LitElement {
|
|
|
23
23
|
@state()
|
|
24
24
|
protected _selectedNodeId: number | string | null = null;
|
|
25
25
|
|
|
26
|
+
@state()
|
|
27
|
+
protected _physicsEnabled = true;
|
|
28
|
+
|
|
26
29
|
protected _network?: Network;
|
|
27
30
|
protected _nodesDataSet?: DataSet<NetworkGraphNode>;
|
|
28
31
|
protected _edgesDataSet?: DataSet<NetworkGraphEdge>;
|
|
@@ -30,6 +33,11 @@ export abstract class BaseNetworkGraph extends LitElement {
|
|
|
30
33
|
protected _resizeObserver?: ResizeObserver;
|
|
31
34
|
protected _themeUnsubscribe?: () => void;
|
|
32
35
|
protected _updateDebounceTimer?: ReturnType<typeof setTimeout>;
|
|
36
|
+
protected _autoFreezeTimer?: ReturnType<typeof setTimeout>;
|
|
37
|
+
/** Whether auto-freeze has already been applied (to avoid re-freezing after user unfreezes) */
|
|
38
|
+
private _autoFreezeApplied = false;
|
|
39
|
+
/** Whether initial fit has been done (to preserve user's zoom/pan after first load) */
|
|
40
|
+
private _initialFitDone = false;
|
|
33
41
|
|
|
34
42
|
/** Store original edge colors for restoration after highlighting */
|
|
35
43
|
private _originalEdgeColors: Map<string, { color: string; highlight: string }> = new Map();
|
|
@@ -166,6 +174,7 @@ export abstract class BaseNetworkGraph extends LitElement {
|
|
|
166
174
|
super.disconnectedCallback();
|
|
167
175
|
this._resizeObserver?.disconnect();
|
|
168
176
|
this._themeUnsubscribe?.();
|
|
177
|
+
this._cancelAutoFreezeTimer();
|
|
169
178
|
if (this._updateDebounceTimer) {
|
|
170
179
|
clearTimeout(this._updateDebounceTimer);
|
|
171
180
|
}
|
|
@@ -243,25 +252,21 @@ export abstract class BaseNetworkGraph extends LitElement {
|
|
|
243
252
|
this._dispatchNodeSelected(null);
|
|
244
253
|
});
|
|
245
254
|
|
|
246
|
-
// Auto-fit after stabilization completes
|
|
255
|
+
// Auto-fit after initial stabilization completes (only once)
|
|
247
256
|
this._network.on("stabilizationIterationsDone", () => {
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
257
|
+
if (!this._initialFitDone) {
|
|
258
|
+
// Fit with padding to keep nodes away from edges
|
|
259
|
+
this._network?.fit({
|
|
260
|
+
animation: {
|
|
261
|
+
duration: 500,
|
|
262
|
+
easingFunction: "easeInOutQuad",
|
|
263
|
+
},
|
|
264
|
+
});
|
|
265
|
+
this._initialFitDone = true;
|
|
256
266
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
animation: {
|
|
261
|
-
duration: 300,
|
|
262
|
-
easingFunction: "easeInOutQuad",
|
|
263
|
-
},
|
|
264
|
-
});
|
|
267
|
+
// Start auto-freeze timer (5 seconds after initial stabilization)
|
|
268
|
+
this._startAutoFreezeTimer();
|
|
269
|
+
}
|
|
265
270
|
});
|
|
266
271
|
|
|
267
272
|
this._updateGraph();
|
|
@@ -286,6 +291,99 @@ export abstract class BaseNetworkGraph extends LitElement {
|
|
|
286
291
|
});
|
|
287
292
|
}
|
|
288
293
|
|
|
294
|
+
/**
|
|
295
|
+
* Zooms in by 20%.
|
|
296
|
+
*/
|
|
297
|
+
public zoomIn(): void {
|
|
298
|
+
if (!this._network) return;
|
|
299
|
+
const scale = this._network.getScale();
|
|
300
|
+
this._network.moveTo({
|
|
301
|
+
scale: scale * 1.2,
|
|
302
|
+
animation: {
|
|
303
|
+
duration: 200,
|
|
304
|
+
easingFunction: "easeInOutQuad",
|
|
305
|
+
},
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
/**
|
|
310
|
+
* Zooms out by 20%.
|
|
311
|
+
*/
|
|
312
|
+
public zoomOut(): void {
|
|
313
|
+
if (!this._network) return;
|
|
314
|
+
const scale = this._network.getScale();
|
|
315
|
+
this._network.moveTo({
|
|
316
|
+
scale: scale / 1.2,
|
|
317
|
+
animation: {
|
|
318
|
+
duration: 200,
|
|
319
|
+
easingFunction: "easeInOutQuad",
|
|
320
|
+
},
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Returns whether physics simulation is currently enabled.
|
|
326
|
+
*/
|
|
327
|
+
public get physicsEnabled(): boolean {
|
|
328
|
+
return this._physicsEnabled;
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
/**
|
|
332
|
+
* Enables or disables physics simulation (node movement/settling).
|
|
333
|
+
* When disabled, nodes freeze in place; when enabled, they resume settling.
|
|
334
|
+
* @param enabled Whether to enable physics
|
|
335
|
+
* @param isManual If true, cancels any pending auto-freeze (user manually toggled)
|
|
336
|
+
*/
|
|
337
|
+
public setPhysicsEnabled(enabled: boolean, isManual = true): void {
|
|
338
|
+
// If user manually toggles, cancel auto-freeze to respect their choice
|
|
339
|
+
if (isManual) {
|
|
340
|
+
this._cancelAutoFreezeTimer();
|
|
341
|
+
this._autoFreezeApplied = true; // Prevent future auto-freeze
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
this._physicsEnabled = enabled;
|
|
345
|
+
this._network?.setOptions({
|
|
346
|
+
physics: { enabled },
|
|
347
|
+
});
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
/**
|
|
351
|
+
* Starts the auto-freeze timer. Physics will be disabled after 5 seconds
|
|
352
|
+
* unless the user manually toggles physics or the timer is cancelled.
|
|
353
|
+
*/
|
|
354
|
+
private _startAutoFreezeTimer(): void {
|
|
355
|
+
// Don't auto-freeze if already applied or user has manually controlled physics
|
|
356
|
+
if (this._autoFreezeApplied) {
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
this._cancelAutoFreezeTimer();
|
|
361
|
+
this._autoFreezeTimer = setTimeout(() => {
|
|
362
|
+
if (!this._autoFreezeApplied && this._physicsEnabled) {
|
|
363
|
+
this.setPhysicsEnabled(false, false); // false = not manual, don't mark as applied
|
|
364
|
+
this._autoFreezeApplied = true;
|
|
365
|
+
// Dispatch event so parent can update UI state
|
|
366
|
+
this.dispatchEvent(
|
|
367
|
+
new CustomEvent("physics-changed", {
|
|
368
|
+
detail: { enabled: false },
|
|
369
|
+
bubbles: true,
|
|
370
|
+
composed: true,
|
|
371
|
+
}),
|
|
372
|
+
);
|
|
373
|
+
}
|
|
374
|
+
}, 5000);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* Cancels any pending auto-freeze timer.
|
|
379
|
+
*/
|
|
380
|
+
private _cancelAutoFreezeTimer(): void {
|
|
381
|
+
if (this._autoFreezeTimer) {
|
|
382
|
+
clearTimeout(this._autoFreezeTimer);
|
|
383
|
+
this._autoFreezeTimer = undefined;
|
|
384
|
+
}
|
|
385
|
+
}
|
|
386
|
+
|
|
289
387
|
/**
|
|
290
388
|
* Selects a node by ID and focuses on it.
|
|
291
389
|
*/
|
|
@@ -323,16 +421,20 @@ export abstract class BaseNetworkGraph extends LitElement {
|
|
|
323
421
|
protected _highlightConnections(nodeId: number | string): void {
|
|
324
422
|
if (!this._edgesDataSet || !this._nodesDataSet) return;
|
|
325
423
|
|
|
326
|
-
// Store original colors before highlighting
|
|
327
424
|
const allEdges = this._edgesDataSet.get();
|
|
425
|
+
|
|
426
|
+
// First pass: Store original colors ONLY if not already stored.
|
|
427
|
+
// We must do this BEFORE any highlighting modifies edge colors.
|
|
428
|
+
// If edges were already highlighted (switching between nodes), the colors
|
|
429
|
+
// in the DataSet might be dimmed, so we rely on previously stored values.
|
|
328
430
|
for (const edge of allEdges) {
|
|
329
431
|
const edgeId = String(edge.id);
|
|
330
432
|
if (!this._originalEdgeColors.has(edgeId)) {
|
|
331
|
-
const colorObj = edge.color as { color: string; highlight: string };
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
});
|
|
433
|
+
const colorObj = edge.color as { color: string; highlight: string } | undefined;
|
|
434
|
+
// Extract colors, with fallbacks
|
|
435
|
+
const color = colorObj?.color ?? "#999999";
|
|
436
|
+
const highlight = colorObj?.highlight ?? color;
|
|
437
|
+
this._originalEdgeColors.set(edgeId, { color, highlight });
|
|
336
438
|
}
|
|
337
439
|
}
|
|
338
440
|
|
|
@@ -351,21 +453,19 @@ export abstract class BaseNetworkGraph extends LitElement {
|
|
|
351
453
|
}
|
|
352
454
|
}
|
|
353
455
|
|
|
354
|
-
// Update edges - make connected ones thicker
|
|
456
|
+
// Update edges - make connected ones thicker, dim non-connected ones
|
|
355
457
|
const dimmedColor = this._getDimmedEdgeColor();
|
|
356
458
|
const edgeUpdates = allEdges.map((edge: NetworkGraphEdge) => {
|
|
357
459
|
const isConnected = edge.from === nodeId || edge.to === nodeId;
|
|
358
460
|
const originalColor = this._originalEdgeColors.get(String(edge.id));
|
|
461
|
+
// Use stored original color for connected edges, fallback to a default if somehow missing
|
|
462
|
+
const connectedColor = originalColor ?? { color: "#999999", highlight: "#999999" };
|
|
359
463
|
return {
|
|
360
464
|
id: edge.id,
|
|
361
465
|
width: isConnected ? 3 : 1,
|
|
362
|
-
// Dim non-connected edges
|
|
363
466
|
color: isConnected
|
|
364
|
-
? { color:
|
|
365
|
-
: {
|
|
366
|
-
color: dimmedColor,
|
|
367
|
-
highlight: dimmedColor,
|
|
368
|
-
},
|
|
467
|
+
? { color: connectedColor.color, highlight: connectedColor.highlight }
|
|
468
|
+
: { color: dimmedColor, highlight: dimmedColor },
|
|
369
469
|
};
|
|
370
470
|
});
|
|
371
471
|
this._edgesDataSet.update(edgeUpdates);
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @license
|
|
3
|
+
* Copyright 2025-2026 Open Home Foundation
|
|
4
|
+
* SPDX-License-Identifier: Apache-2.0
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Matter.js Status codes mapped to readable names.
|
|
9
|
+
* @see MatterSpecification.v142.Core § 8.10.1
|
|
10
|
+
*/
|
|
11
|
+
export const MatterStatusNames: Record<number, string> = {
|
|
12
|
+
0: "Success",
|
|
13
|
+
1: "Failure",
|
|
14
|
+
125: "InvalidSubscription",
|
|
15
|
+
126: "UnsupportedAccess",
|
|
16
|
+
127: "UnsupportedEndpoint",
|
|
17
|
+
128: "InvalidAction",
|
|
18
|
+
129: "UnsupportedCommand",
|
|
19
|
+
133: "InvalidCommand",
|
|
20
|
+
134: "UnsupportedAttribute",
|
|
21
|
+
135: "ConstraintError",
|
|
22
|
+
136: "UnsupportedWrite",
|
|
23
|
+
137: "ResourceExhausted",
|
|
24
|
+
139: "NotFound",
|
|
25
|
+
140: "UnreportableAttribute",
|
|
26
|
+
141: "InvalidDataType",
|
|
27
|
+
143: "UnsupportedRead",
|
|
28
|
+
146: "DataVersionMismatch",
|
|
29
|
+
148: "Timeout",
|
|
30
|
+
155: "UnsupportedNode",
|
|
31
|
+
156: "Busy",
|
|
32
|
+
157: "AccessRestricted",
|
|
33
|
+
195: "UnsupportedCluster",
|
|
34
|
+
197: "NoUpstreamSubscription",
|
|
35
|
+
198: "NeedsTimedInteraction",
|
|
36
|
+
199: "UnsupportedEvent",
|
|
37
|
+
200: "PathsExhausted",
|
|
38
|
+
201: "TimedRequestMismatch",
|
|
39
|
+
202: "FailsafeRequired",
|
|
40
|
+
203: "InvalidInState",
|
|
41
|
+
204: "NoCommandResponse",
|
|
42
|
+
205: "TermsAndConditionsChanged",
|
|
43
|
+
206: "MaintenanceRequired",
|
|
44
|
+
207: "DynamicConstraintError",
|
|
45
|
+
208: "AlreadyExists",
|
|
46
|
+
209: "InvalidTransportType",
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Get readable name for a Matter status code */
|
|
50
|
+
export function getMatterStatusName(status: number): string {
|
|
51
|
+
return MatterStatusNames[status] ?? `Unknown(${status})`;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** Result type for Matter operations with status details */
|
|
55
|
+
export interface MatterOperationResult {
|
|
56
|
+
success: boolean;
|
|
57
|
+
status: number;
|
|
58
|
+
statusName: string;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** Outcome type for batch operations */
|
|
62
|
+
export type BatchOutcome = "all_success" | "all_failed" | "partial";
|
|
63
|
+
|
|
64
|
+
/** Result type for batch Matter operations (multiple entries) */
|
|
65
|
+
export interface MatterBatchResult {
|
|
66
|
+
/** Overall outcome: all succeeded, all failed, or partial */
|
|
67
|
+
outcome: BatchOutcome;
|
|
68
|
+
/** Number of successful operations (status === 0) */
|
|
69
|
+
successCount: number;
|
|
70
|
+
/** Number of failed operations (status !== 0) */
|
|
71
|
+
failureCount: number;
|
|
72
|
+
/** Count of failures per status code, e.g. { 126: 2, 135: 1 } */
|
|
73
|
+
errorCounts: Record<number, number>;
|
|
74
|
+
/** Human-readable summary message for display */
|
|
75
|
+
message: string;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** Create a successful operation result */
|
|
79
|
+
export function successResult(): MatterOperationResult {
|
|
80
|
+
return { success: true, status: 0, statusName: "Success" };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/** Create a failed operation result from a status code */
|
|
84
|
+
export function failureResult(status: number): MatterOperationResult {
|
|
85
|
+
return { success: false, status, statusName: getMatterStatusName(status) };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Create an operation result from a status code (success if 0) */
|
|
89
|
+
export function resultFromStatus(status: number): MatterOperationResult {
|
|
90
|
+
const statusName = getMatterStatusName(status);
|
|
91
|
+
return { success: status === 0, status, statusName };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Analyze multiple operation results and produce a batch result summary.
|
|
96
|
+
* @param results Array of objects with a `status` property (0 = success).
|
|
97
|
+
* If null, undefined, or empty, this is treated as a failed/unknown batch.
|
|
98
|
+
* @returns MatterBatchResult with outcome, counts, and human-readable message
|
|
99
|
+
*/
|
|
100
|
+
export function analyzeBatchResults(results: Array<{ status: number }> | null | undefined): MatterBatchResult {
|
|
101
|
+
if (!results || results.length === 0) {
|
|
102
|
+
return {
|
|
103
|
+
outcome: "all_failed",
|
|
104
|
+
successCount: 0,
|
|
105
|
+
failureCount: 0,
|
|
106
|
+
errorCounts: {},
|
|
107
|
+
message: "No response/results returned",
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
let successCount = 0;
|
|
112
|
+
let failureCount = 0;
|
|
113
|
+
const errorCounts: Record<number, number> = {};
|
|
114
|
+
|
|
115
|
+
for (const result of results) {
|
|
116
|
+
if (result.status === 0) {
|
|
117
|
+
successCount++;
|
|
118
|
+
} else {
|
|
119
|
+
failureCount++;
|
|
120
|
+
errorCounts[result.status] = (errorCounts[result.status] ?? 0) + 1;
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
let outcome: BatchOutcome;
|
|
125
|
+
if (failureCount === 0) {
|
|
126
|
+
outcome = "all_success";
|
|
127
|
+
} else if (successCount === 0) {
|
|
128
|
+
outcome = "all_failed";
|
|
129
|
+
} else {
|
|
130
|
+
outcome = "partial";
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const message = formatBatchMessage(outcome, successCount, failureCount, errorCounts);
|
|
134
|
+
|
|
135
|
+
return { outcome, successCount, failureCount, errorCounts, message };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/** Format a human-readable message for batch results */
|
|
139
|
+
function formatBatchMessage(
|
|
140
|
+
outcome: BatchOutcome,
|
|
141
|
+
successCount: number,
|
|
142
|
+
failureCount: number,
|
|
143
|
+
errorCounts: Record<number, number>,
|
|
144
|
+
): string {
|
|
145
|
+
const entryWord = (count: number) => (count === 1 ? "entry" : "entries");
|
|
146
|
+
|
|
147
|
+
if (outcome === "all_success") {
|
|
148
|
+
return "Write successful";
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// Format error breakdown: "2x UnsupportedAccess (126), 1x ConstraintError (135)"
|
|
152
|
+
const errorParts = Object.entries(errorCounts)
|
|
153
|
+
.map(([code, count]) => {
|
|
154
|
+
const statusCode = parseInt(code, 10);
|
|
155
|
+
return `${count}x ${getMatterStatusName(statusCode)} (${code})`;
|
|
156
|
+
})
|
|
157
|
+
.join(", ");
|
|
158
|
+
|
|
159
|
+
if (outcome === "all_failed") {
|
|
160
|
+
return `All ${failureCount} ${entryWord(failureCount)} failed: ${errorParts}`;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// partial
|
|
164
|
+
return `Partial failure: ${successCount} ${entryWord(successCount)} succeeded, ${failureCount} failed (${errorParts}). Please verify.`;
|
|
165
|
+
}
|