@matter-server/dashboard 0.3.2 → 0.3.3

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 (104) hide show
  1. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts +2 -2
  2. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts.map +1 -1
  3. package/dist/esm/pages/cluster-commands/base-cluster-commands.js.map +1 -1
  4. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts +36 -0
  5. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts.map +1 -0
  6. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js +159 -0
  7. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js.map +6 -0
  8. package/dist/esm/pages/cluster-commands/index.d.ts +1 -0
  9. package/dist/esm/pages/cluster-commands/index.d.ts.map +1 -1
  10. package/dist/esm/pages/cluster-commands/index.js +1 -0
  11. package/dist/esm/pages/cluster-commands/index.js.map +1 -1
  12. package/dist/esm/pages/components/footer.d.ts.map +1 -1
  13. package/dist/esm/pages/components/footer.js +4 -7
  14. package/dist/esm/pages/components/footer.js.map +1 -1
  15. package/dist/esm/pages/components/header.d.ts +5 -0
  16. package/dist/esm/pages/components/header.d.ts.map +1 -1
  17. package/dist/esm/pages/components/header.js +75 -0
  18. package/dist/esm/pages/components/header.js.map +1 -1
  19. package/dist/esm/pages/components/node-details.js +1 -1
  20. package/dist/esm/pages/components/node-details.js.map +1 -1
  21. package/dist/esm/pages/components/server-details.d.ts.map +1 -1
  22. package/dist/esm/pages/components/server-details.js +0 -1
  23. package/dist/esm/pages/components/server-details.js.map +1 -1
  24. package/dist/esm/pages/matter-dashboard-app.d.ts +12 -0
  25. package/dist/esm/pages/matter-dashboard-app.d.ts.map +1 -1
  26. package/dist/esm/pages/matter-dashboard-app.js +84 -4
  27. package/dist/esm/pages/matter-dashboard-app.js.map +1 -1
  28. package/dist/esm/pages/matter-network-view.d.ts +52 -0
  29. package/dist/esm/pages/matter-network-view.d.ts.map +1 -0
  30. package/dist/esm/pages/matter-network-view.js +309 -0
  31. package/dist/esm/pages/matter-network-view.js.map +6 -0
  32. package/dist/esm/pages/matter-node-view.d.ts.map +1 -1
  33. package/dist/esm/pages/matter-node-view.js +70 -1
  34. package/dist/esm/pages/matter-node-view.js.map +1 -1
  35. package/dist/esm/pages/matter-server-view.d.ts +4 -0
  36. package/dist/esm/pages/matter-server-view.d.ts.map +1 -1
  37. package/dist/esm/pages/matter-server-view.js +16 -1
  38. package/dist/esm/pages/matter-server-view.js.map +1 -1
  39. package/dist/esm/pages/network/base-network-graph.d.ts +74 -0
  40. package/dist/esm/pages/network/base-network-graph.d.ts.map +1 -0
  41. package/dist/esm/pages/network/base-network-graph.js +403 -0
  42. package/dist/esm/pages/network/base-network-graph.js.map +6 -0
  43. package/dist/esm/pages/network/device-icons.d.ts +52 -0
  44. package/dist/esm/pages/network/device-icons.d.ts.map +1 -0
  45. package/dist/esm/pages/network/device-icons.js +197 -0
  46. package/dist/esm/pages/network/device-icons.js.map +6 -0
  47. package/dist/esm/pages/network/device-panel.d.ts +31 -0
  48. package/dist/esm/pages/network/device-panel.d.ts.map +1 -0
  49. package/dist/esm/pages/network/device-panel.js +183 -0
  50. package/dist/esm/pages/network/device-panel.js.map +6 -0
  51. package/dist/esm/pages/network/network-details.d.ts +47 -0
  52. package/dist/esm/pages/network/network-details.d.ts.map +1 -0
  53. package/dist/esm/pages/network/network-details.js +686 -0
  54. package/dist/esm/pages/network/network-details.js.map +6 -0
  55. package/dist/esm/pages/network/network-types.d.ts +153 -0
  56. package/dist/esm/pages/network/network-types.d.ts.map +1 -0
  57. package/dist/esm/pages/network/network-types.js +19 -0
  58. package/dist/esm/pages/network/network-types.js.map +6 -0
  59. package/dist/esm/pages/network/network-utils.d.ts +170 -0
  60. package/dist/esm/pages/network/network-utils.d.ts.map +1 -0
  61. package/dist/esm/pages/network/network-utils.js +472 -0
  62. package/dist/esm/pages/network/network-utils.js.map +6 -0
  63. package/dist/esm/pages/network/thread-graph.d.ts +27 -0
  64. package/dist/esm/pages/network/thread-graph.d.ts.map +1 -0
  65. package/dist/esm/pages/network/thread-graph.js +134 -0
  66. package/dist/esm/pages/network/thread-graph.js.map +6 -0
  67. package/dist/esm/pages/network/wifi-graph.d.ts +27 -0
  68. package/dist/esm/pages/network/wifi-graph.d.ts.map +1 -0
  69. package/dist/esm/pages/network/wifi-graph.js +167 -0
  70. package/dist/esm/pages/network/wifi-graph.js.map +6 -0
  71. package/dist/web/js/{commission-node-dialog-CBSDiqRW.js → commission-node-dialog-B1_khzZb.js} +5 -5
  72. package/dist/web/js/{commission-node-existing-TP6s8Tez.js → commission-node-existing-RpdajrwF.js} +2 -5
  73. package/dist/web/js/{commission-node-thread-DOB8pu6x.js → commission-node-thread-5f2itkTG.js} +2 -5
  74. package/dist/web/js/{commission-node-wifi-tzavmk1j.js → commission-node-wifi-DZ_pWqsa.js} +2 -5
  75. package/dist/web/js/{dialog-box-Dknil_Be.js → dialog-box-DEUxM4B1.js} +2 -2
  76. package/dist/web/js/{fire_event-DRpOSjJR.js → fire_event-BczBMT8E.js} +1 -1
  77. package/dist/web/js/{log-level-dialog-TXkma-7Z.js → log-level-dialog-Cr3PfX1X.js} +2 -3
  78. package/dist/web/js/main.js +1 -1
  79. package/dist/web/js/matter-dashboard-app-BuCe_Jxf.js +29990 -0
  80. package/dist/web/js/{node-binding-dialog-D52FCBFP.js → node-binding-dialog-DMiHNDLA.js} +2 -4
  81. package/dist/web/js/{prevent_default-BPgSQsuY.js → prevent_default-D4FX_PIh.js} +2 -42
  82. package/package.json +5 -4
  83. package/src/pages/cluster-commands/base-cluster-commands.ts +2 -2
  84. package/src/pages/cluster-commands/clusters/basic-information-commands.ts +171 -0
  85. package/src/pages/cluster-commands/index.ts +1 -0
  86. package/src/pages/components/footer.ts +4 -7
  87. package/src/pages/components/header.ts +81 -0
  88. package/src/pages/components/node-details.ts +2 -2
  89. package/src/pages/components/server-details.ts +0 -1
  90. package/src/pages/matter-dashboard-app.ts +105 -5
  91. package/src/pages/matter-network-view.ts +325 -0
  92. package/src/pages/matter-node-view.ts +75 -1
  93. package/src/pages/matter-server-view.ts +17 -1
  94. package/src/pages/network/base-network-graph.ts +463 -0
  95. package/src/pages/network/device-icons.ts +283 -0
  96. package/src/pages/network/device-panel.ts +180 -0
  97. package/src/pages/network/network-details.ts +750 -0
  98. package/src/pages/network/network-types.ts +161 -0
  99. package/src/pages/network/network-utils.ts +752 -0
  100. package/src/pages/network/thread-graph.ts +164 -0
  101. package/src/pages/network/wifi-graph.ts +192 -0
  102. package/dist/web/js/matter-dashboard-app-B7GUghkC.js +0 -17254
  103. package/dist/web/js/outlined-text-field-D1DyKQY-.js +0 -968
  104. package/dist/web/js/validator-C735j770.js +0 -1122
@@ -1,8 +1,6 @@
1
- import { j as i, s as c, v as clientContext, n, a as e, i as i$1, A, h as b, q as handleAsync, t } from './matter-dashboard-app-B7GUghkC.js';
2
- import { p as preventDefault } from './prevent_default-BPgSQsuY.js';
3
- import './outlined-text-field-D1DyKQY-.js';
1
+ import { k as i, G as c, H as clientContext, n, a as e, i as i$1, A, j as b, F as handleAsync, t } from './matter-dashboard-app-BuCe_Jxf.js';
2
+ import { p as preventDefault } from './prevent_default-D4FX_PIh.js';
4
3
  import './main.js';
5
- import './validator-C735j770.js';
6
4
 
7
5
  var _staticBlock$1;
8
6
  /**
@@ -1,44 +1,4 @@
1
- import { E as EASING, m as mixinDelegatesAria, i, _ as __decorate, n, a as e, r, h as b, A, f as e$1, j as i$1, t } from './matter-dashboard-app-B7GUghkC.js';
2
-
3
- /**
4
- * @license
5
- * Copyright 2021 Google LLC
6
- * SPDX-License-Identifier: Apache-2.0
7
- */
8
- /**
9
- * Re-dispatches an event from the provided element.
10
- *
11
- * This function is useful for forwarding non-composed events, such as `change`
12
- * events.
13
- *
14
- * @example
15
- * class MyInput extends LitElement {
16
- * render() {
17
- * return html`<input @change=${this.redispatchEvent}>`;
18
- * }
19
- *
20
- * protected redispatchEvent(event: Event) {
21
- * redispatchEvent(this, event);
22
- * }
23
- * }
24
- *
25
- * @param element The element to dispatch the event from.
26
- * @param event The event to re-dispatch.
27
- * @return Whether or not the event was dispatched (if cancelable).
28
- */
29
- function redispatchEvent(element, event) {
30
- // For bubbling events in SSR light DOM (or composed), stop their propagation
31
- // and dispatch the copy.
32
- if (event.bubbles && (!element.shadowRoot || event.composed)) {
33
- event.stopPropagation();
34
- }
35
- const copy = Reflect.construct(event.constructor, [event.type, event]);
36
- const dispatched = element.dispatchEvent(copy);
37
- if (!dispatched) {
38
- event.preventDefault();
39
- }
40
- return dispatched;
41
- }
1
+ import { E as EASING, m as mixinDelegatesAria, i, _ as __decorate, n, a as e, r, j as b, A, f as e$1, w as redispatchEvent, k as i$1, t } from './matter-dashboard-app-BuCe_Jxf.js';
42
2
 
43
3
  /**
44
4
  * @license
@@ -811,4 +771,4 @@ MdDialog = __decorate([t('md-dialog')], MdDialog);
811
771
  */
812
772
  const preventDefault = ev => ev.preventDefault();
813
773
 
814
- export { preventDefault as p, redispatchEvent as r };
774
+ export { preventDefault as p };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matter-server/dashboard",
3
- "version": "0.3.2",
3
+ "version": "0.3.3",
4
4
  "description": "Dashboard for OHF Matter Server",
5
5
  "bugs": {
6
6
  "url": "https://github.com/matter-js/matterjs-server/issues"
@@ -37,11 +37,12 @@
37
37
  "dependencies": {
38
38
  "@lit/context": "^1.1.6",
39
39
  "@material/web": "^2.4.1",
40
- "@matter-server/ws-client": "0.3.2",
41
- "@matter-server/custom-clusters": "0.3.2",
40
+ "@matter-server/ws-client": "0.3.3",
41
+ "@matter-server/custom-clusters": "0.3.3",
42
42
  "@mdi/js": "^7.4.47",
43
43
  "lit": "^3.3.2",
44
- "tslib": "^2.8.1"
44
+ "tslib": "^2.8.1",
45
+ "vis-network": "^9.1.9"
45
46
  },
46
47
  "files": [
47
48
  "dist/**/*",
@@ -5,7 +5,7 @@
5
5
  */
6
6
 
7
7
  import { MatterClient, MatterNode } from "@matter-server/ws-client";
8
- import { LitElement, css } from "lit";
8
+ import { LitElement, css, type CSSResultGroup } from "lit";
9
9
  import { property } from "lit/decorators.js";
10
10
 
11
11
  /**
@@ -39,7 +39,7 @@ export abstract class BaseClusterCommands extends LitElement {
39
39
  }
40
40
  }
41
41
 
42
- static override styles = css`
42
+ static override styles: CSSResultGroup = css`
43
43
  :host {
44
44
  display: block;
45
45
  }
@@ -0,0 +1,171 @@
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/textfield/outlined-text-field";
9
+ import { css, html, nothing, type CSSResultGroup } from "lit";
10
+ import { customElement, state } from "lit/decorators.js";
11
+ import { showAlertDialog } from "../../../components/dialog-box/show-dialog-box.js";
12
+ import { handleAsync } from "../../../util/async-handler.js";
13
+ import { BaseClusterCommands } from "../base-cluster-commands.js";
14
+ import { registerClusterCommands } from "../registry.js";
15
+
16
+ const CLUSTER_ID = 0x28; // BasicInformation cluster (40 decimal)
17
+ const NODE_LABEL_ATTRIBUTE_ID = 5;
18
+ const MAX_NODE_LABEL_LENGTH = 32;
19
+
20
+ /**
21
+ * Command panel for BasicInformation cluster (ID: 0x28 / 40).
22
+ * Provides ability to edit the NodeLabel attribute.
23
+ */
24
+ @customElement("basic-information-cluster-commands")
25
+ export class BasicInformationClusterCommands extends BaseClusterCommands {
26
+ @state()
27
+ private _nodeLabel: string = "";
28
+
29
+ @state()
30
+ private _saving: boolean = false;
31
+
32
+ override updated(changedProperties: Map<string, unknown>) {
33
+ super.updated(changedProperties);
34
+ // Load node label when node property is first set
35
+ if (changedProperties.has("node") && this.node) {
36
+ this._loadCurrentNodeLabel();
37
+ }
38
+ }
39
+
40
+ /**
41
+ * Load the current NodeLabel from the node's cached attributes.
42
+ */
43
+ private _loadCurrentNodeLabel() {
44
+ if (!this.node) {
45
+ return;
46
+ }
47
+
48
+ // First check the direct nodeLabel property on the node
49
+ if (this.node.nodeLabel) {
50
+ this._nodeLabel = this.node.nodeLabel;
51
+ return;
52
+ }
53
+
54
+ // Fallback to attribute path: endpoint/cluster/attribute = 0/40/5
55
+ // BasicInformation cluster is always on endpoint 0 per Matter specification
56
+ const attributePath = `0/${CLUSTER_ID}/${NODE_LABEL_ATTRIBUTE_ID}`;
57
+ const currentValue = this.node.attributes[attributePath];
58
+ if (typeof currentValue === "string") {
59
+ this._nodeLabel = currentValue;
60
+ } else {
61
+ this._nodeLabel = "";
62
+ }
63
+ }
64
+
65
+ /**
66
+ * Check if the node is available (not offline).
67
+ */
68
+ private get _isNodeAvailable(): boolean {
69
+ return this.node.available;
70
+ }
71
+
72
+ override render() {
73
+ return html`
74
+ <details class="command-panel">
75
+ <summary>Node Label</summary>
76
+ <div class="command-content">
77
+ ${!this._isNodeAvailable
78
+ ? html`<div class="offline-warning">Node is offline - cannot edit label</div>`
79
+ : nothing}
80
+ <div class="command-row">
81
+ <md-outlined-text-field
82
+ label="Node Label"
83
+ .value=${this._nodeLabel}
84
+ @input=${this._handleInput}
85
+ maxlength=${MAX_NODE_LABEL_LENGTH}
86
+ ?disabled=${!this._isNodeAvailable || this._saving}
87
+ supporting-text="Max ${MAX_NODE_LABEL_LENGTH} characters"
88
+ ></md-outlined-text-field>
89
+ <md-filled-button
90
+ @click=${handleAsync(() => this._handleSave())}
91
+ ?disabled=${!this._isNodeAvailable || this._saving}
92
+ >
93
+ ${this._saving ? "Saving..." : "Save"}
94
+ </md-filled-button>
95
+ </div>
96
+ </div>
97
+ </details>
98
+ `;
99
+ }
100
+
101
+ private _handleInput(e: Event) {
102
+ const input = e.target as HTMLInputElement;
103
+ this._nodeLabel = input.value;
104
+ }
105
+
106
+ private async _handleSave() {
107
+ if (!this._isNodeAvailable) {
108
+ showAlertDialog({
109
+ title: "Cannot Save",
110
+ text: "Node is offline. Please wait until the node is available.",
111
+ });
112
+ return;
113
+ }
114
+
115
+ this._saving = true;
116
+
117
+ try {
118
+ // BasicInformation cluster is always on endpoint 0 per Matter specification
119
+ const attributePath = `0/${CLUSTER_ID}/${NODE_LABEL_ATTRIBUTE_ID}`;
120
+ await this.client.writeAttribute(this.node.node_id, attributePath, this._nodeLabel);
121
+
122
+ showAlertDialog({
123
+ title: "Success",
124
+ text: `Node label set to "${this._nodeLabel}"`,
125
+ });
126
+ } catch (error) {
127
+ const errorMessage = error instanceof Error ? error.message : String(error);
128
+ showAlertDialog({
129
+ title: "Failed to set node label",
130
+ text: errorMessage,
131
+ });
132
+ } finally {
133
+ this._saving = false;
134
+ }
135
+ }
136
+
137
+ static override styles: CSSResultGroup = [
138
+ BaseClusterCommands.styles,
139
+ css`
140
+ .command-row {
141
+ align-items: flex-start;
142
+ }
143
+
144
+ md-outlined-text-field {
145
+ width: 36ch; /* 32 chars + some padding for field chrome */
146
+ }
147
+
148
+ md-filled-button {
149
+ margin-top: 8px; /* Align with text field input area, accounting for label */
150
+ }
151
+
152
+ .offline-warning {
153
+ color: var(--danger-color, #d32f2f);
154
+ font-size: 14px;
155
+ margin-bottom: 12px;
156
+ padding: 8px 12px;
157
+ background-color: var(--md-sys-color-error-container, #fdecea);
158
+ border-radius: 4px;
159
+ }
160
+ `,
161
+ ];
162
+ }
163
+
164
+ // Register this component for cluster ID 0x28 (40 decimal)
165
+ registerClusterCommands(CLUSTER_ID, "basic-information-cluster-commands");
166
+
167
+ declare global {
168
+ interface HTMLElementTagNameMap {
169
+ "basic-information-cluster-commands": BasicInformationClusterCommands;
170
+ }
171
+ }
@@ -16,5 +16,6 @@ export { getClusterCommandsTag, hasClusterCommands, registerClusterCommands } fr
16
16
  export { BaseClusterCommands } from "./base-cluster-commands.js";
17
17
 
18
18
  // Cluster command components (auto-register on import)
19
+ import "./clusters/basic-information-commands.js";
19
20
  import "./clusters/level-control-commands.js";
20
21
  import "./clusters/on-off-commands.js";
@@ -22,14 +22,11 @@ export class DashboardFooter extends LitElement {
22
22
 
23
23
  static override styles = css`
24
24
  .footer {
25
- padding: 16px;
25
+ padding: 4px 16px;
26
26
  text-align: center;
27
- font-size: 0.8em;
28
- color: var(--md-sys-color-on-surface);
29
- display: flex;
30
- flex-direction: column;
31
- position: relative;
32
- clear: both;
27
+ font-size: 0.75em;
28
+ color: var(--md-sys-color-on-surface-variant);
29
+ flex-shrink: 0;
33
30
  }
34
31
 
35
32
  .footer a {
@@ -23,10 +23,15 @@ interface HeaderAction {
23
23
  action: void;
24
24
  }
25
25
 
26
+ export type ActiveView = "nodes" | "thread" | "wifi";
27
+
26
28
  @customElement("dashboard-header")
27
29
  export class DashboardHeader extends LitElement {
28
30
  @property() public backButton?: string;
29
31
  @property() public actions?: HeaderAction[];
32
+ @property() public activeView?: ActiveView;
33
+ @property({ type: Boolean }) public hasThreadDevices?: boolean;
34
+ @property({ type: Boolean }) public hasWifiDevices?: boolean;
30
35
 
31
36
  public client?: MatterClient;
32
37
 
@@ -80,6 +85,48 @@ export class DashboardHeader extends LitElement {
80
85
  }
81
86
  }
82
87
 
88
+ private _renderNavTabs() {
89
+ if (this.activeView === undefined) {
90
+ return nothing;
91
+ }
92
+
93
+ // Only show tabs if at least one network type has devices
94
+ const showThreadTab = this.hasThreadDevices === true;
95
+ const showWifiTab = this.hasWifiDevices === true;
96
+
97
+ // Don't show nav tabs if no network devices exist
98
+ if (!showThreadTab && !showWifiTab) {
99
+ return nothing;
100
+ }
101
+
102
+ return html`
103
+ <nav class="nav-tabs" aria-label="View navigation">
104
+ <a
105
+ href="#nodes"
106
+ class="nav-tab ${this.activeView === "nodes" ? "active" : ""}"
107
+ aria-current=${this.activeView === "nodes" ? "page" : nothing}
108
+ >Nodes</a
109
+ >
110
+ ${showThreadTab
111
+ ? html`<a
112
+ href="#thread"
113
+ class="nav-tab ${this.activeView === "thread" ? "active" : ""}"
114
+ aria-current=${this.activeView === "thread" ? "page" : nothing}
115
+ >Thread</a
116
+ >`
117
+ : nothing}
118
+ ${showWifiTab
119
+ ? html`<a
120
+ href="#wifi"
121
+ class="nav-tab ${this.activeView === "wifi" ? "active" : ""}"
122
+ aria-current=${this.activeView === "wifi" ? "page" : nothing}
123
+ >WiFi</a
124
+ >`
125
+ : nothing}
126
+ </nav>
127
+ `;
128
+ }
129
+
83
130
  protected override render() {
84
131
  return html`
85
132
  <div class="header">
@@ -93,6 +140,7 @@ export class DashboardHeader extends LitElement {
93
140
  : ""}
94
141
 
95
142
  <div class="title">${this.title ?? ""}</div>
143
+ ${this._renderNavTabs()}
96
144
  <div class="actions">
97
145
  ${this.actions?.map(action => {
98
146
  return html`
@@ -156,5 +204,38 @@ export class DashboardHeader extends LitElement {
156
204
  max-width: 100%;
157
205
  align-items: center;
158
206
  }
207
+
208
+ .nav-tabs {
209
+ display: flex;
210
+ margin-left: 24px;
211
+ gap: 4px;
212
+ }
213
+
214
+ .nav-tab {
215
+ padding: 8px 16px;
216
+ color: var(--md-sys-color-on-primary);
217
+ text-decoration: none;
218
+ font-size: 0.875rem;
219
+ font-weight: 500;
220
+ border-radius: 4px 4px 0 0;
221
+ opacity: 0.7;
222
+ transition: opacity 0.2s;
223
+ }
224
+
225
+ .nav-tab:hover {
226
+ opacity: 0.9;
227
+ }
228
+
229
+ .nav-tab.active {
230
+ opacity: 1;
231
+ background-color: rgba(255, 255, 255, 0.15);
232
+ border-bottom: 2px solid var(--md-sys-color-on-primary);
233
+ }
234
+
235
+ @media (max-width: 768px) {
236
+ .nav-tabs {
237
+ display: none;
238
+ }
239
+ }
159
240
  `;
160
241
  }
@@ -199,8 +199,8 @@ export class NodeDetails extends LitElement {
199
199
  private async _binding() {
200
200
  try {
201
201
  showNodeBindingDialog(this.client, this.node!, this.endpoint);
202
- } catch (err: any) {
203
- console.log(err);
202
+ } catch (err: unknown) {
203
+ console.error("Binding error:", err);
204
204
  }
205
205
  }
206
206
 
@@ -71,7 +71,6 @@ export class ServerDetails extends LitElement {
71
71
  }
72
72
 
73
73
  private _commissionNode() {
74
- console.log(this.client);
75
74
  showCommissionNodeDialog(this.client!);
76
75
  }
77
76
 
@@ -14,10 +14,13 @@ import "../components/ha-svg-icon";
14
14
  import { clone } from "../util/clone_class.js";
15
15
  import type { Route } from "../util/routing.js";
16
16
  import "./components/header";
17
+ import type { ActiveView } from "./components/header.js";
17
18
  import "./matter-cluster-view";
18
19
  import "./matter-endpoint-view";
20
+ import "./matter-network-view";
19
21
  import "./matter-node-view";
20
22
  import "./matter-server-view";
23
+ import { categorizeDevices } from "./network/network-utils.js";
21
24
 
22
25
  declare global {
23
26
  interface HTMLElementTagNameMap {
@@ -32,27 +35,73 @@ class MatterDashboardApp extends LitElement {
32
35
  path: [],
33
36
  };
34
37
 
38
+ @state() private _activeView: ActiveView = "nodes";
39
+
40
+ /** Initial selected node ID from URL (string to avoid BigInt precision loss) */
41
+ @state() private _initialSelectedNodeId: string | null = null;
42
+
35
43
  public client!: MatterClient;
36
44
 
37
45
  @state()
38
46
  private _state: "connecting" | "connected" | "error" | "disconnected" = "connecting";
39
47
 
48
+ /** Track whether nodes have been loaded at least once (to avoid redirecting before data arrives) */
49
+ private _nodesLoaded = false;
50
+
40
51
  private provider = new ContextProvider(this, { context: clientContext, initialValue: this.client });
41
52
 
53
+ /** Reference to updateRoute function so it can be called from event listeners */
54
+ private _updateRoute?: () => void;
55
+
42
56
  protected override firstUpdated(_changedProperties: PropertyValueMap<any> | Map<PropertyKey, unknown>): void {
43
57
  super.firstUpdated(_changedProperties);
44
58
  this._connect();
45
59
 
46
60
  // Handle history changes
47
- const updateRoute = () => {
48
- const pathParts = location.hash.substring(1).split("/");
61
+ this._updateRoute = () => {
62
+ const hash = location.hash.substring(1);
63
+ const pathParts = hash.split("/");
64
+
65
+ // Reset initial selected node
66
+ this._initialSelectedNodeId = null;
67
+
68
+ // Get device counts for conditional navigation
69
+ const { hasThreadDevices, hasWifiDevices } = this._getDeviceCounts();
70
+
71
+ // Determine active view from hash
72
+ if (pathParts[0] === "thread") {
73
+ // Only redirect if nodes have been loaded (avoid redirecting on initial load before data arrives)
74
+ if (this._nodesLoaded && !hasThreadDevices) {
75
+ location.hash = "#nodes";
76
+ return;
77
+ }
78
+ this._activeView = "thread";
79
+ // Check for node ID: #thread/123 - keep as string to avoid BigInt precision loss
80
+ if (pathParts.length > 1 && pathParts[1]) {
81
+ this._initialSelectedNodeId = pathParts[1];
82
+ }
83
+ } else if (pathParts[0] === "wifi") {
84
+ // Only redirect if nodes have been loaded (avoid redirecting on initial load before data arrives)
85
+ if (this._nodesLoaded && !hasWifiDevices) {
86
+ location.hash = "#nodes";
87
+ return;
88
+ }
89
+ this._activeView = "wifi";
90
+ // Check for node ID: #wifi/123 - keep as string to avoid BigInt precision loss
91
+ if (pathParts.length > 1 && pathParts[1]) {
92
+ this._initialSelectedNodeId = pathParts[1];
93
+ }
94
+ } else if (hash === "nodes" || hash === "" || pathParts[0] === "node") {
95
+ this._activeView = "nodes";
96
+ }
97
+
49
98
  this._route = {
50
99
  prefix: pathParts.length == 1 ? "" : pathParts[0],
51
100
  path: pathParts.length == 1 ? pathParts : pathParts.slice(1),
52
101
  };
53
102
  };
54
- window.addEventListener("hashchange", updateRoute);
55
- updateRoute();
103
+ window.addEventListener("hashchange", this._updateRoute);
104
+ this._updateRoute();
56
105
  }
57
106
 
58
107
  private _connect() {
@@ -69,6 +118,13 @@ class MatterDashboardApp extends LitElement {
69
118
 
70
119
  private _setupEventListeners() {
71
120
  this.client.addEventListener("nodes_changed", () => {
121
+ // Mark nodes as loaded and re-evaluate route (for redirect logic)
122
+ const wasFirstLoad = !this._nodesLoaded;
123
+ this._nodesLoaded = true;
124
+ if (wasFirstLoad && this._updateRoute) {
125
+ // Re-run route check now that nodes are available
126
+ this._updateRoute();
127
+ }
72
128
  this.requestUpdate();
73
129
  this.provider.setValue(clone(this.client));
74
130
  });
@@ -85,6 +141,20 @@ class MatterDashboardApp extends LitElement {
85
141
  this._connect();
86
142
  };
87
143
 
144
+ /**
145
+ * Get device counts for Thread and WiFi networks.
146
+ */
147
+ private _getDeviceCounts(): { hasThreadDevices: boolean; hasWifiDevices: boolean } {
148
+ if (!this.client?.nodes) {
149
+ return { hasThreadDevices: false, hasWifiDevices: false };
150
+ }
151
+ const categorized = categorizeDevices(this.client.nodes);
152
+ return {
153
+ hasThreadDevices: categorized.thread.length > 0,
154
+ hasWifiDevices: categorized.wifi.length > 0 || categorized.ethernet.length > 0,
155
+ };
156
+ }
157
+
88
158
  override render() {
89
159
  if (this._state === "connecting") {
90
160
  return html`
@@ -154,11 +224,41 @@ class MatterDashboardApp extends LitElement {
154
224
  ></matter-node-view>
155
225
  `;
156
226
  }
157
- // root level: server overview
227
+ // Get device counts for conditional navigation
228
+ const { hasThreadDevices, hasWifiDevices } = this._getDeviceCounts();
229
+
230
+ // Check for Thread view (#thread or #thread/123)
231
+ if (this._route.prefix === "thread" || this._route.path[0] === "thread") {
232
+ return html`<matter-network-view
233
+ .client=${this.client}
234
+ .nodes=${this.client.nodes}
235
+ .activeView=${this._activeView}
236
+ .initialSelectedNodeId=${this._initialSelectedNodeId}
237
+ .hasThreadDevices=${hasThreadDevices}
238
+ .hasWifiDevices=${hasWifiDevices}
239
+ networkType="thread"
240
+ ></matter-network-view>`;
241
+ }
242
+ // Check for WiFi view (#wifi or #wifi/123)
243
+ if (this._route.prefix === "wifi" || this._route.path[0] === "wifi") {
244
+ return html`<matter-network-view
245
+ .client=${this.client}
246
+ .nodes=${this.client.nodes}
247
+ .activeView=${this._activeView}
248
+ .initialSelectedNodeId=${this._initialSelectedNodeId}
249
+ .hasThreadDevices=${hasThreadDevices}
250
+ .hasWifiDevices=${hasWifiDevices}
251
+ networkType="wifi"
252
+ ></matter-network-view>`;
253
+ }
254
+ // root level: server overview (nodes view)
158
255
  return html`<matter-server-view
159
256
  .client=${this.client}
160
257
  .nodes=${this.client.nodes}
161
258
  .route=${this._route}
259
+ .activeView=${this._activeView}
260
+ .hasThreadDevices=${hasThreadDevices}
261
+ .hasWifiDevices=${hasWifiDevices}
162
262
  ></matter-server-view>`;
163
263
  }
164
264