@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.
Files changed (33) hide show
  1. package/dist/esm/components/dialogs/binding/node-binding-dialog.d.ts.map +1 -1
  2. package/dist/esm/components/dialogs/binding/node-binding-dialog.js +44 -17
  3. package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +1 -1
  4. package/dist/esm/entrypoint/main.js +1 -1
  5. package/dist/esm/entrypoint/main.js.map +1 -1
  6. package/dist/esm/pages/matter-network-view.d.ts +5 -0
  7. package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
  8. package/dist/esm/pages/matter-network-view.js +96 -13
  9. package/dist/esm/pages/matter-network-view.js.map +1 -1
  10. package/dist/esm/pages/network/base-network-graph.d.ts +34 -0
  11. package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -1
  12. package/dist/esm/pages/network/base-network-graph.js +106 -23
  13. package/dist/esm/pages/network/base-network-graph.js.map +1 -1
  14. package/dist/esm/util/matter-status.d.ts +49 -0
  15. package/dist/esm/util/matter-status.d.ts.map +1 -0
  16. package/dist/esm/util/matter-status.js +110 -0
  17. package/dist/esm/util/matter-status.js.map +6 -0
  18. package/dist/web/js/{commission-node-dialog-CcMuttYO.js → commission-node-dialog-DBugVQOl.js} +4 -4
  19. package/dist/web/js/{commission-node-existing-CqTRDMAr.js → commission-node-existing-ts2MN2bQ.js} +2 -2
  20. package/dist/web/js/{commission-node-thread-DgwtTVwK.js → commission-node-thread-CixfNuM3.js} +2 -2
  21. package/dist/web/js/{commission-node-wifi-XaN2SEnE.js → commission-node-wifi-CWC30EYn.js} +2 -2
  22. package/dist/web/js/{dialog-box-COpDD8i7.js → dialog-box-SeMuFiWx.js} +1 -1
  23. package/dist/web/js/{fire_event-mDYWi2sw.js → fire_event-B63oGTcK.js} +1 -1
  24. package/dist/web/js/{log-level-dialog-Bc32kZVw.js → log-level-dialog-J0gFiLLM.js} +1 -1
  25. package/dist/web/js/main.js +2 -2
  26. package/dist/web/js/{matter-dashboard-app-CrBHT4fT.js → matter-dashboard-app-D7YWrgXj.js} +222 -43
  27. package/dist/web/js/{node-binding-dialog-C8fqOJiB.js → node-binding-dialog-DSBwIJcQ.js} +146 -18
  28. package/package.json +6 -6
  29. package/src/components/dialogs/binding/node-binding-dialog.ts +45 -21
  30. package/src/entrypoint/main.ts +2 -0
  31. package/src/pages/matter-network-view.ts +102 -13
  32. package/src/pages/network/base-network-graph.ts +130 -30
  33. 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
- // Fit with padding to keep nodes away from edges
249
- this._network?.fit({
250
- animation: {
251
- duration: 500,
252
- easingFunction: "easeInOutQuad",
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
- // Also fit when physics fully stops (catches any drift after initial stabilization)
258
- this._network.on("stabilized", () => {
259
- this._network?.fit({
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
- this._originalEdgeColors.set(edgeId, {
333
- color: colorObj?.color ?? "#999999",
334
- highlight: colorObj?.highlight ?? "#999999",
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: originalColor?.color, highlight: originalColor?.highlight }
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
+ }