@matter-server/dashboard 1.1.8-alpha.0-20260707-c552b9d → 1.2.0

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 (133) hide show
  1. package/README.md +30 -1
  2. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.d.ts +6 -0
  3. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.d.ts.map +1 -1
  4. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.js +55 -2
  5. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.js.map +1 -1
  6. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.d.ts +6 -0
  7. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.d.ts.map +1 -1
  8. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.js +53 -3
  9. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.js.map +1 -1
  10. package/dist/esm/components/dialogs/fabric-label-dialog/fabric-label-dialog.d.ts +29 -0
  11. package/dist/esm/components/dialogs/fabric-label-dialog/fabric-label-dialog.d.ts.map +1 -0
  12. package/dist/esm/components/dialogs/fabric-label-dialog/fabric-label-dialog.js +112 -0
  13. package/dist/esm/components/dialogs/fabric-label-dialog/fabric-label-dialog.js.map +6 -0
  14. package/dist/esm/components/dialogs/fabric-label-dialog/show-fabric-label-dialog.d.ts +8 -0
  15. package/dist/esm/components/dialogs/fabric-label-dialog/show-fabric-label-dialog.d.ts.map +1 -0
  16. package/dist/esm/components/dialogs/fabric-label-dialog/show-fabric-label-dialog.js +17 -0
  17. package/dist/esm/components/dialogs/fabric-label-dialog/show-fabric-label-dialog.js.map +6 -0
  18. package/dist/esm/components/dialogs/settings/settings-dialog.d.ts +28 -0
  19. package/dist/esm/components/dialogs/settings/settings-dialog.d.ts.map +1 -1
  20. package/dist/esm/components/dialogs/settings/settings-dialog.js +496 -105
  21. package/dist/esm/components/dialogs/settings/settings-dialog.js.map +1 -1
  22. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts +6 -0
  23. package/dist/esm/pages/cluster-commands/base-cluster-commands.d.ts.map +1 -1
  24. package/dist/esm/pages/cluster-commands/base-cluster-commands.js +8 -0
  25. package/dist/esm/pages/cluster-commands/base-cluster-commands.js.map +1 -1
  26. package/dist/esm/pages/cluster-commands/clusters/access-control-commands.d.ts.map +1 -1
  27. package/dist/esm/pages/cluster-commands/clusters/access-control-commands.js +21 -7
  28. package/dist/esm/pages/cluster-commands/clusters/access-control-commands.js.map +1 -1
  29. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.d.ts.map +1 -1
  30. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.js +34 -13
  31. package/dist/esm/pages/cluster-commands/clusters/avsum-commands.js.map +1 -1
  32. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.d.ts.map +1 -1
  33. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js +17 -11
  34. package/dist/esm/pages/cluster-commands/clusters/basic-information-commands.js.map +1 -1
  35. package/dist/esm/pages/cluster-commands/clusters/binding-commands.d.ts.map +1 -1
  36. package/dist/esm/pages/cluster-commands/clusters/binding-commands.js +24 -9
  37. package/dist/esm/pages/cluster-commands/clusters/binding-commands.js.map +1 -1
  38. package/dist/esm/pages/cluster-commands/clusters/icd-management-commands.d.ts +54 -0
  39. package/dist/esm/pages/cluster-commands/clusters/icd-management-commands.d.ts.map +1 -0
  40. package/dist/esm/pages/cluster-commands/clusters/icd-management-commands.js +514 -0
  41. package/dist/esm/pages/cluster-commands/clusters/icd-management-commands.js.map +6 -0
  42. package/dist/esm/pages/cluster-commands/index.d.ts +1 -0
  43. package/dist/esm/pages/cluster-commands/index.d.ts.map +1 -1
  44. package/dist/esm/pages/cluster-commands/index.js +1 -0
  45. package/dist/esm/pages/cluster-commands/index.js.map +1 -1
  46. package/dist/esm/pages/components/node-details.d.ts +2 -0
  47. package/dist/esm/pages/components/node-details.d.ts.map +1 -1
  48. package/dist/esm/pages/components/node-details.js +43 -2
  49. package/dist/esm/pages/components/node-details.js.map +1 -1
  50. package/dist/esm/pages/components/server-details.d.ts +4 -0
  51. package/dist/esm/pages/components/server-details.d.ts.map +1 -1
  52. package/dist/esm/pages/components/server-details.js +50 -2
  53. package/dist/esm/pages/components/server-details.js.map +1 -1
  54. package/dist/esm/pages/matter-cluster-view.d.ts.map +1 -1
  55. package/dist/esm/pages/matter-cluster-view.js +1 -1
  56. package/dist/esm/pages/matter-cluster-view.js.map +1 -1
  57. package/dist/esm/pages/matter-network-view.d.ts +2 -0
  58. package/dist/esm/pages/matter-network-view.d.ts.map +1 -1
  59. package/dist/esm/pages/matter-network-view.js +23 -0
  60. package/dist/esm/pages/matter-network-view.js.map +1 -1
  61. package/dist/esm/pages/matter-server-view.d.ts.map +1 -1
  62. package/dist/esm/pages/matter-server-view.js +35 -1
  63. package/dist/esm/pages/matter-server-view.js.map +1 -1
  64. package/dist/esm/pages/network/border-router-store.d.ts +11 -4
  65. package/dist/esm/pages/network/border-router-store.d.ts.map +1 -1
  66. package/dist/esm/pages/network/border-router-store.js +39 -0
  67. package/dist/esm/pages/network/border-router-store.js.map +1 -1
  68. package/dist/esm/pages/network/network-details.d.ts +11 -1
  69. package/dist/esm/pages/network/network-details.d.ts.map +1 -1
  70. package/dist/esm/pages/network/network-details.js +305 -3
  71. package/dist/esm/pages/network/network-details.js.map +1 -1
  72. package/dist/esm/pages/network/network-types.d.ts +3 -183
  73. package/dist/esm/pages/network/network-types.d.ts.map +1 -1
  74. package/dist/esm/pages/network/network-types.js +1 -10
  75. package/dist/esm/pages/network/network-types.js.map +2 -2
  76. package/dist/esm/pages/network/network-utils.d.ts +19 -171
  77. package/dist/esm/pages/network/network-utils.d.ts.map +1 -1
  78. package/dist/esm/pages/network/network-utils.js +118 -510
  79. package/dist/esm/pages/network/network-utils.js.map +2 -2
  80. package/dist/esm/pages/network/thread-graph.d.ts +23 -1
  81. package/dist/esm/pages/network/thread-graph.d.ts.map +1 -1
  82. package/dist/esm/pages/network/thread-graph.js +187 -10
  83. package/dist/esm/pages/network/thread-graph.js.map +1 -1
  84. package/dist/esm/util/duration.d.ts +8 -0
  85. package/dist/esm/util/duration.d.ts.map +1 -0
  86. package/dist/esm/util/duration.js +25 -0
  87. package/dist/esm/util/duration.js.map +6 -0
  88. package/dist/esm/util/icd.d.ts +54 -0
  89. package/dist/esm/util/icd.d.ts.map +1 -0
  90. package/dist/esm/util/icd.js +139 -0
  91. package/dist/esm/util/icd.js.map +6 -0
  92. package/dist/web/js/{attribute-write-dialog-Bk3AWMoZ.js → attribute-write-dialog-Bu7d1xMh.js} +1 -1
  93. package/dist/web/js/{command-invoke-dialog-H8A7dCFd.js → command-invoke-dialog-BK7MYWq-.js} +1 -1
  94. package/dist/web/js/{commission-node-dialog-GIvqBYGw.js → commission-node-dialog-BeRg7rEp.js} +5 -5
  95. package/dist/web/js/{commission-node-existing-DglV5NXP.js → commission-node-existing-ugSNvMcK.js} +2 -2
  96. package/dist/web/js/{commission-node-thread-DzrHWt8R.js → commission-node-thread-CT_Qbe1U.js} +53 -4
  97. package/dist/web/js/{commission-node-wifi-DWQ9mxqk.js → commission-node-wifi-Bx1nhWl4.js} +48 -5
  98. package/dist/web/js/{dialog-box-CsqgNzj2.js → dialog-box-DolVvXoK.js} +1 -1
  99. package/dist/web/js/fabric-label-dialog-D4NlfZS9.js +94 -0
  100. package/dist/web/js/fire_event-CJVsUQ4T.js +18 -0
  101. package/dist/web/js/main.js +101 -22
  102. package/dist/web/js/{matter-dashboard-app-BrbVPiqG.js → matter-dashboard-app-DMIDxj8f.js} +3118 -944
  103. package/dist/web/js/{node-acl-add-dialog-COL4myAw.js → node-acl-add-dialog-8qtV5Lx-.js} +1 -1
  104. package/dist/web/js/{node-binding-dialog-_Odw2AGk.js → node-binding-dialog-B75AcGPb.js} +1 -1
  105. package/dist/web/js/{node-label-dialog-D6hA1gVX.js → node-label-dialog-QxhxDvAP.js} +1 -1
  106. package/dist/web/js/settings-dialog-CeBkVje3.js +980 -0
  107. package/package.json +4 -4
  108. package/src/components/dialogs/commission-node-dialog/commission-node-thread.ts +64 -3
  109. package/src/components/dialogs/commission-node-dialog/commission-node-wifi.ts +62 -4
  110. package/src/components/dialogs/fabric-label-dialog/fabric-label-dialog.ts +109 -0
  111. package/src/components/dialogs/fabric-label-dialog/show-fabric-label-dialog.ts +16 -0
  112. package/src/components/dialogs/settings/settings-dialog.ts +526 -118
  113. package/src/pages/cluster-commands/base-cluster-commands.ts +11 -0
  114. package/src/pages/cluster-commands/clusters/access-control-commands.ts +21 -7
  115. package/src/pages/cluster-commands/clusters/avsum-commands.ts +34 -13
  116. package/src/pages/cluster-commands/clusters/basic-information-commands.ts +17 -12
  117. package/src/pages/cluster-commands/clusters/binding-commands.ts +26 -10
  118. package/src/pages/cluster-commands/clusters/icd-management-commands.ts +551 -0
  119. package/src/pages/cluster-commands/index.ts +1 -0
  120. package/src/pages/components/node-details.ts +46 -2
  121. package/src/pages/components/server-details.ts +55 -2
  122. package/src/pages/matter-cluster-view.ts +4 -3
  123. package/src/pages/matter-network-view.ts +25 -0
  124. package/src/pages/matter-server-view.ts +37 -1
  125. package/src/pages/network/border-router-store.ts +51 -4
  126. package/src/pages/network/network-details.ts +379 -3
  127. package/src/pages/network/network-types.ts +19 -193
  128. package/src/pages/network/network-utils.ts +125 -803
  129. package/src/pages/network/thread-graph.ts +241 -13
  130. package/src/util/duration.ts +24 -0
  131. package/src/util/icd.ts +197 -0
  132. package/dist/web/js/fire_event-BEdDUG4R.js +0 -168
  133. package/dist/web/js/settings-dialog-DUmZpfSq.js +0 -606
@@ -21,7 +21,7 @@ import "@material/web/iconbutton/icon-button";
21
21
  import "@material/web/switch/switch";
22
22
  import { consume } from "@lit/context";
23
23
  import "@material/web/textfield/outlined-text-field";
24
- import { mdiAccessPoint, mdiEye, mdiEyeOff, mdiWifi } from "@mdi/js";
24
+ import { mdiAccessPoint, mdiDelete, mdiEye, mdiEyeOff, mdiPencil, mdiPlus, mdiWifi } from "@mdi/js";
25
25
  import { css, html, LitElement, nothing } from "lit";
26
26
  import { customElement, property, query, state } from "lit/decorators.js";
27
27
  import { clientContext, tickContext } from "../../../client/client-context.js";
@@ -39,6 +39,14 @@ let SettingsDialog = class extends LitElement {
39
39
  this._expandedRow = null;
40
40
  this._credLoading = false;
41
41
  this._showPassword = false;
42
+ this._wifiEditId = null;
43
+ this._wifiAdding = false;
44
+ this._threadEditId = null;
45
+ this._threadAdding = false;
46
+ this._credError = null;
47
+ }
48
+ get _supportsCredentialLists() {
49
+ return (this.client.serverInfo?.schema_version ?? 0) >= 12;
42
50
  }
43
51
  connectedCallback() {
44
52
  super.connectedCallback();
@@ -60,6 +68,18 @@ let SettingsDialog = class extends LitElement {
60
68
  });
61
69
  });
62
70
  }
71
+ void this._loadCredentials();
72
+ }
73
+ async _loadCredentials() {
74
+ if (!this._supportsCredentialLists) {
75
+ this._creds = void 0;
76
+ return;
77
+ }
78
+ try {
79
+ this._creds = await this.client.getAllCredentials();
80
+ } catch {
81
+ this._creds = void 0;
82
+ }
63
83
  }
64
84
  _close() {
65
85
  this.shadowRoot.querySelector("md-dialog").close();
@@ -79,84 +99,455 @@ let SettingsDialog = class extends LitElement {
79
99
  }
80
100
  _toggleExpand(row) {
81
101
  this._expandedRow = this._expandedRow === row ? null : row;
102
+ this._resetCredForms();
103
+ }
104
+ _resetCredForms() {
82
105
  this._showPassword = false;
106
+ this._wifiEditId = null;
107
+ this._wifiAdding = false;
108
+ this._threadEditId = null;
109
+ this._threadAdding = false;
110
+ this._credError = null;
83
111
  }
84
112
  _cancelCred() {
85
113
  this._expandedRow = null;
86
- this._showPassword = false;
114
+ this._resetCredForms();
87
115
  }
88
116
  _togglePassword() {
89
117
  this._showPassword = !this._showPassword;
90
118
  }
91
- async _saveWifi() {
119
+ _editWifi(id) {
120
+ this._wifiAdding = false;
121
+ this._wifiEditId = id;
122
+ this._showPassword = false;
123
+ this._credError = null;
124
+ }
125
+ _addWifi() {
126
+ this._wifiEditId = null;
127
+ this._wifiAdding = true;
128
+ this._showPassword = false;
129
+ this._credError = null;
130
+ }
131
+ _cancelWifiForm() {
132
+ this._wifiEditId = null;
133
+ this._wifiAdding = false;
134
+ this._showPassword = false;
135
+ this._credError = null;
136
+ }
137
+ _editThread(id) {
138
+ this._threadAdding = false;
139
+ this._threadEditId = id;
140
+ this._credError = null;
141
+ }
142
+ _addThread() {
143
+ this._threadEditId = null;
144
+ this._threadAdding = true;
145
+ this._credError = null;
146
+ }
147
+ _cancelThreadForm() {
148
+ this._threadEditId = null;
149
+ this._threadAdding = false;
150
+ this._credError = null;
151
+ }
152
+ /** Mirror the server's id validation. Returns the trimmed id, or null after setting an inline error. */
153
+ _validateNewCredId(raw, existing) {
154
+ const trimmed = raw.trim();
155
+ if (trimmed === "") {
156
+ this._credError = "Identifier is required";
157
+ return null;
158
+ }
159
+ const lower = trimmed.toLowerCase();
160
+ if (lower === "default" || lower === "delete") {
161
+ this._credError = `'${trimmed}' is a reserved identifier`;
162
+ return null;
163
+ }
164
+ if (existing.some((e) => e.id.toLowerCase() === lower)) {
165
+ this._credError = `An entry with identifier '${trimmed}' already exists`;
166
+ return null;
167
+ }
168
+ return trimmed;
169
+ }
170
+ async _saveWifi(id) {
92
171
  const ssid = this._wifiSsidField.value.trim();
93
172
  if (!ssid) {
94
- showAlertDialog({ title: "Validation error", text: "SSID is required" });
173
+ this._credError = "SSID is required";
95
174
  return;
96
175
  }
176
+ let targetId = id;
177
+ if (targetId === void 0 && this._supportsCredentialLists) {
178
+ const validated = this._validateNewCredId(this._wifiIdField?.value ?? "", this._creds?.wifi ?? []);
179
+ if (validated === null) return;
180
+ targetId = validated;
181
+ }
97
182
  const password = this._wifiPasswordField.value;
98
- if (!password) {
99
- showAlertDialog({ title: "Validation error", text: "Password is required" });
183
+ const hasStoredSecret = id !== void 0 && (id === "default" ? this.client.serverInfo.wifi_credentials_set ?? false : true);
184
+ if (!password && !hasStoredSecret) {
185
+ this._credError = "Password is required";
100
186
  return;
101
187
  }
102
188
  this._credLoading = true;
189
+ this._credError = null;
103
190
  try {
104
- await this.client.setWifiCredentials(ssid, password);
105
- this._expandedRow = null;
106
- this._showPassword = false;
191
+ await this.client.setWifiCredentials(ssid, password, { id: targetId });
192
+ this._cancelWifiForm();
193
+ await this._loadCredentials();
107
194
  } catch (err) {
108
195
  showAlertDialog({ title: "Error saving WiFi credentials", text: err.message });
109
196
  } finally {
110
197
  this._credLoading = false;
111
198
  }
112
199
  }
113
- async _removeWifi() {
200
+ async _removeWifi(id) {
114
201
  this._credLoading = true;
115
202
  try {
116
- await this.client.removeWifiCredentials();
117
- this._expandedRow = null;
118
- this._showPassword = false;
203
+ await this.client.removeWifiCredentials({ id });
204
+ this._cancelWifiForm();
205
+ await this._loadCredentials();
119
206
  } catch (err) {
120
207
  showAlertDialog({ title: "Error removing WiFi credentials", text: err.message });
121
208
  } finally {
122
209
  this._credLoading = false;
123
210
  }
124
211
  }
125
- async _saveThread() {
212
+ async _saveThread(id) {
126
213
  const dataset = this._threadDatasetField.value.trim();
127
214
  if (!dataset) {
128
- showAlertDialog({ title: "Validation error", text: "Thread dataset is required" });
215
+ this._credError = "Thread dataset is required";
129
216
  return;
130
217
  }
131
218
  if (!/^[0-9a-fA-F]*$/.test(dataset) || dataset.length % 2 !== 0) {
132
- showAlertDialog({
133
- title: "Invalid Thread dataset",
134
- text: "Must be a hex string with even length (each byte is two hex characters)"
135
- });
219
+ this._credError = "Dataset must be a hex string with even length (each byte is two hex characters)";
136
220
  return;
137
221
  }
222
+ let targetId = id;
223
+ if (targetId === void 0 && this._supportsCredentialLists) {
224
+ const validated = this._validateNewCredId(this._threadIdField?.value ?? "", this._creds?.thread ?? []);
225
+ if (validated === null) return;
226
+ targetId = validated;
227
+ }
138
228
  this._credLoading = true;
229
+ this._credError = null;
139
230
  try {
140
- await this.client.setThreadOperationalDataset(dataset);
141
- this._expandedRow = null;
231
+ await this.client.setThreadOperationalDataset(dataset, { id: targetId });
232
+ this._cancelThreadForm();
233
+ await this._loadCredentials();
142
234
  } catch (err) {
143
235
  showAlertDialog({ title: "Error saving Thread dataset", text: err.message });
144
236
  } finally {
145
237
  this._credLoading = false;
146
238
  }
147
239
  }
148
- async _removeThread() {
240
+ async _removeThread(id) {
149
241
  this._credLoading = true;
150
242
  try {
151
- await this.client.removeThreadDataset();
152
- this._expandedRow = null;
153
- this._showPassword = false;
243
+ await this.client.removeThreadDataset({ id });
244
+ this._cancelThreadForm();
245
+ await this._loadCredentials();
154
246
  } catch (err) {
155
247
  showAlertDialog({ title: "Error removing Thread dataset", text: err.message });
156
248
  } finally {
157
249
  this._credLoading = false;
158
250
  }
159
251
  }
252
+ _renderCredError() {
253
+ return this._credError ? html`<p class="cred-error">${this._credError}</p>` : nothing;
254
+ }
255
+ _renderWifiList() {
256
+ const entries = this._creds?.wifi ?? [];
257
+ return html`
258
+ <div class="cred-group">
259
+ <div class="cred-group-header">
260
+ <div class="cred-info">
261
+ <ha-svg-icon .path=${mdiWifi}></ha-svg-icon>
262
+ <span class="cred-label">WiFi</span>
263
+ </div>
264
+ <md-text-button
265
+ @click=${this._addWifi}
266
+ .disabled=${this._credLoading || this._wifiAdding || this._wifiEditId !== null}
267
+ >
268
+ <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>Add
269
+ </md-text-button>
270
+ </div>
271
+ ${entries.length === 0 && !this._wifiAdding ? html`<p class="cred-empty">No WiFi credentials configured</p>` : nothing}
272
+ ${entries.map((entry) => this._renderWifiEntry(entry))}
273
+ ${this._wifiAdding ? this._renderWifiForm(void 0, "") : nothing}
274
+ </div>
275
+ `;
276
+ }
277
+ _renderWifiEntry(entry) {
278
+ const isDefault = entry.id === "default";
279
+ if (this._wifiEditId === entry.id) {
280
+ return this._renderWifiForm(entry.id, entry.ssid);
281
+ }
282
+ if (isDefault && !(this.client.serverInfo?.wifi_credentials_set ?? false)) {
283
+ return html`
284
+ <div class="cred-row">
285
+ <div class="cred-info cred-entry">
286
+ <span class="cred-unset">Not configured</span>
287
+ <span class="cred-badge">Default</span>
288
+ </div>
289
+ <div class="cred-row-actions">
290
+ <md-text-button @click=${() => this._editWifi("default")} .disabled=${this._credLoading}>
291
+ <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>Set
292
+ </md-text-button>
293
+ </div>
294
+ </div>
295
+ `;
296
+ }
297
+ return html`
298
+ <div class="cred-row">
299
+ <div class="cred-info cred-entry">
300
+ <span class="cred-value">${entry.ssid || "(no SSID)"}</span>
301
+ ${isDefault ? html`<span class="cred-badge">Default</span>` : nothing}
302
+ </div>
303
+ <div class="cred-row-actions">
304
+ <md-icon-button
305
+ title="Edit"
306
+ @click=${() => this._editWifi(entry.id)}
307
+ .disabled=${this._credLoading}
308
+ >
309
+ <ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
310
+ </md-icon-button>
311
+ ${isDefault ? html`<md-text-button
312
+ @click=${handleAsync(() => this._removeWifi("default"))}
313
+ .disabled=${this._credLoading}
314
+ >Clear</md-text-button
315
+ >` : html`<md-icon-button
316
+ title="Delete"
317
+ @click=${handleAsync(() => this._removeWifi(entry.id))}
318
+ .disabled=${this._credLoading}
319
+ >
320
+ <ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
321
+ </md-icon-button>`}
322
+ </div>
323
+ </div>
324
+ `;
325
+ }
326
+ _renderWifiForm(id, ssid) {
327
+ const isAdd = id === void 0;
328
+ return html`
329
+ <div class="cred-form">
330
+ ${isAdd ? html`<md-outlined-text-field
331
+ id="cred-wifi-id"
332
+ label="Identifier"
333
+ supporting-text="Unique name (not 'default' or 'delete')"
334
+ .disabled=${this._credLoading}
335
+ ></md-outlined-text-field>` : nothing}
336
+ <md-outlined-text-field
337
+ id="cred-wifi-ssid"
338
+ label="SSID"
339
+ .value=${ssid}
340
+ .disabled=${this._credLoading}
341
+ ></md-outlined-text-field>
342
+ <div class="password-row">
343
+ <md-outlined-text-field
344
+ id="cred-wifi-password"
345
+ label="Password"
346
+ .type=${this._showPassword ? "text" : "password"}
347
+ .placeholder=${isAdd ? "" : "leave blank to keep"}
348
+ .disabled=${this._credLoading}
349
+ ></md-outlined-text-field>
350
+ <md-icon-button @click=${this._togglePassword}>
351
+ <ha-svg-icon .path=${this._showPassword ? mdiEyeOff : mdiEye}></ha-svg-icon>
352
+ </md-icon-button>
353
+ </div>
354
+ ${this._renderCredError()}
355
+ <div class="form-actions">
356
+ <md-text-button @click=${this._cancelWifiForm} .disabled=${this._credLoading}
357
+ >Cancel</md-text-button
358
+ >
359
+ <md-filled-button @click=${handleAsync(() => this._saveWifi(id))} .disabled=${this._credLoading}
360
+ >Save</md-filled-button
361
+ >
362
+ </div>
363
+ </div>
364
+ `;
365
+ }
366
+ _renderThreadList() {
367
+ const entries = this._creds?.thread ?? [];
368
+ return html`
369
+ <div class="cred-group cred-group-thread">
370
+ <div class="cred-group-header">
371
+ <div class="cred-info">
372
+ <ha-svg-icon .path=${mdiAccessPoint}></ha-svg-icon>
373
+ <span class="cred-label">Thread</span>
374
+ </div>
375
+ <md-text-button
376
+ @click=${this._addThread}
377
+ .disabled=${this._credLoading || this._threadAdding || this._threadEditId !== null}
378
+ >
379
+ <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>Add
380
+ </md-text-button>
381
+ </div>
382
+ ${entries.length === 0 && !this._threadAdding ? html`<p class="cred-empty">No Thread datasets configured</p>` : nothing}
383
+ ${entries.map((entry) => this._renderThreadEntry(entry))}
384
+ ${this._threadAdding ? this._renderThreadForm(void 0) : nothing}
385
+ </div>
386
+ `;
387
+ }
388
+ _renderThreadEntry(entry) {
389
+ const isDefault = entry.id === "default";
390
+ if (this._threadEditId === entry.id) {
391
+ return this._renderThreadForm(entry.id);
392
+ }
393
+ if (isDefault && !(this.client.serverInfo?.thread_credentials_set ?? false)) {
394
+ return html`
395
+ <div class="cred-row">
396
+ <div class="cred-info cred-entry">
397
+ <span class="cred-unset">Not configured</span>
398
+ <span class="cred-badge">Default</span>
399
+ </div>
400
+ <div class="cred-row-actions">
401
+ <md-text-button @click=${() => this._editThread("default")} .disabled=${this._credLoading}>
402
+ <ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon>Set
403
+ </md-text-button>
404
+ </div>
405
+ </div>
406
+ `;
407
+ }
408
+ const label = entry.networkName ?? entry.extPanId ?? "Thread network set";
409
+ return html`
410
+ <div class="cred-row">
411
+ <div class="cred-info cred-entry">
412
+ <span class="cred-value">${label}</span>
413
+ ${isDefault ? html`<span class="cred-badge">Default</span>` : nothing}
414
+ </div>
415
+ <div class="cred-row-actions">
416
+ <md-icon-button
417
+ title="Edit"
418
+ @click=${() => this._editThread(entry.id)}
419
+ .disabled=${this._credLoading}
420
+ >
421
+ <ha-svg-icon .path=${mdiPencil}></ha-svg-icon>
422
+ </md-icon-button>
423
+ ${isDefault ? html`<md-text-button
424
+ @click=${handleAsync(() => this._removeThread("default"))}
425
+ .disabled=${this._credLoading}
426
+ >Clear</md-text-button
427
+ >` : html`<md-icon-button
428
+ title="Delete"
429
+ @click=${handleAsync(() => this._removeThread(entry.id))}
430
+ .disabled=${this._credLoading}
431
+ >
432
+ <ha-svg-icon .path=${mdiDelete}></ha-svg-icon>
433
+ </md-icon-button>`}
434
+ </div>
435
+ </div>
436
+ `;
437
+ }
438
+ _renderThreadForm(id) {
439
+ const isAdd = id === void 0;
440
+ return html`
441
+ <div class="cred-form">
442
+ ${isAdd ? html`<md-outlined-text-field
443
+ id="cred-thread-id"
444
+ label="Identifier"
445
+ supporting-text="Unique name (not 'default' or 'delete')"
446
+ .disabled=${this._credLoading}
447
+ ></md-outlined-text-field>` : nothing}
448
+ <md-outlined-text-field
449
+ id="cred-thread-dataset"
450
+ label="Thread dataset"
451
+ supporting-text="Hex string (e.g. 0E080000...)"
452
+ .disabled=${this._credLoading}
453
+ ></md-outlined-text-field>
454
+ ${this._renderCredError()}
455
+ <div class="form-actions">
456
+ <md-text-button @click=${this._cancelThreadForm} .disabled=${this._credLoading}
457
+ >Cancel</md-text-button
458
+ >
459
+ <md-filled-button @click=${handleAsync(() => this._saveThread(id))} .disabled=${this._credLoading}
460
+ >Save</md-filled-button
461
+ >
462
+ </div>
463
+ </div>
464
+ `;
465
+ }
466
+ _renderLegacyWifi() {
467
+ return html`
468
+ <div class="cred-row">
469
+ <div class="cred-info">
470
+ <ha-svg-icon .path=${mdiWifi}></ha-svg-icon>
471
+ <span class="cred-label">WiFi</span>
472
+ ${this.client.serverInfo.wifi_credentials_set ? html`<span class="cred-value">${this.client.serverInfo.wifi_ssid}</span>` : html`<span class="cred-unset">Not configured</span>`}
473
+ </div>
474
+ <md-text-button @click=${() => this._toggleExpand("wifi")} .disabled=${this._credLoading}
475
+ >Edit</md-text-button
476
+ >
477
+ </div>
478
+ ${this._expandedRow === "wifi" ? html`<div class="cred-form">
479
+ <md-outlined-text-field
480
+ id="cred-wifi-ssid"
481
+ label="SSID"
482
+ .value=${this.client.serverInfo.wifi_ssid ?? ""}
483
+ .disabled=${this._credLoading}
484
+ ></md-outlined-text-field>
485
+ <div class="password-row">
486
+ <md-outlined-text-field
487
+ id="cred-wifi-password"
488
+ label="Password"
489
+ .type=${this._showPassword ? "text" : "password"}
490
+ .disabled=${this._credLoading}
491
+ ></md-outlined-text-field>
492
+ <md-icon-button @click=${this._togglePassword}>
493
+ <ha-svg-icon .path=${this._showPassword ? mdiEyeOff : mdiEye}></ha-svg-icon>
494
+ </md-icon-button>
495
+ </div>
496
+ ${this._renderCredError()}
497
+ <div class="form-actions">
498
+ <md-text-button @click=${this._cancelCred} .disabled=${this._credLoading}
499
+ >Cancel</md-text-button
500
+ >
501
+ ${this.client.serverInfo.wifi_credentials_set ? html`<md-text-button
502
+ @click=${handleAsync(() => this._removeWifi())}
503
+ .disabled=${this._credLoading}
504
+ >Remove</md-text-button
505
+ >` : nothing}
506
+ <md-filled-button @click=${handleAsync(() => this._saveWifi())} .disabled=${this._credLoading}
507
+ >Save</md-filled-button
508
+ >
509
+ </div>
510
+ </div>` : nothing}
511
+ `;
512
+ }
513
+ _renderLegacyThread() {
514
+ return html`
515
+ <div class="cred-row cred-row-thread">
516
+ <div class="cred-info">
517
+ <ha-svg-icon .path=${mdiAccessPoint}></ha-svg-icon>
518
+ <span class="cred-label">Thread</span>
519
+ ${this.client.serverInfo.thread_credentials_set ? html`<span class="cred-value">Thread network set</span>` : html`<span class="cred-unset">Not configured</span>`}
520
+ </div>
521
+ <md-text-button @click=${() => this._toggleExpand("thread")} .disabled=${this._credLoading}
522
+ >Edit</md-text-button
523
+ >
524
+ </div>
525
+ ${this._expandedRow === "thread" ? html`<div class="cred-form">
526
+ <md-outlined-text-field
527
+ id="cred-thread-dataset"
528
+ label="Thread dataset"
529
+ supporting-text="Hex string (e.g. 0E080000...)"
530
+ .disabled=${this._credLoading}
531
+ ></md-outlined-text-field>
532
+ ${this._renderCredError()}
533
+ <div class="form-actions">
534
+ <md-text-button @click=${this._cancelCred} .disabled=${this._credLoading}
535
+ >Cancel</md-text-button
536
+ >
537
+ ${this.client.serverInfo.thread_credentials_set ? html`<md-text-button
538
+ @click=${handleAsync(() => this._removeThread())}
539
+ .disabled=${this._credLoading}
540
+ >Remove</md-text-button
541
+ >` : nothing}
542
+ <md-filled-button
543
+ @click=${handleAsync(() => this._saveThread())}
544
+ .disabled=${this._credLoading}
545
+ >Save</md-filled-button
546
+ >
547
+ </div>
548
+ </div>` : nothing}
549
+ `;
550
+ }
160
551
  render() {
161
552
  return html`
162
553
  <md-dialog open @cancel=${preventDefault} @closed=${this._handleClosed}>
@@ -195,86 +586,8 @@ let SettingsDialog = class extends LitElement {
195
586
  <section id="network-credentials" class="section">
196
587
  <h3 class="section-title">Network credentials</h3>
197
588
 
198
- <div class="cred-row">
199
- <div class="cred-info">
200
- <ha-svg-icon .path=${mdiWifi}></ha-svg-icon>
201
- <span class="cred-label">WiFi</span>
202
- ${this.client.serverInfo.wifi_credentials_set ? html`<span class="cred-value">${this.client.serverInfo.wifi_ssid}</span>` : html`<span class="cred-unset">Not configured</span>`}
203
- </div>
204
- <md-text-button @click=${() => this._toggleExpand("wifi")} .disabled=${this._credLoading}
205
- >Edit</md-text-button
206
- >
207
- </div>
208
-
209
- ${this._expandedRow === "wifi" ? html` <div class="cred-form">
210
- <md-outlined-text-field
211
- id="cred-wifi-ssid"
212
- label="SSID"
213
- .value=${this.client.serverInfo.wifi_ssid ?? ""}
214
- .disabled=${this._credLoading}
215
- ></md-outlined-text-field>
216
- <div class="password-row">
217
- <md-outlined-text-field
218
- id="cred-wifi-password"
219
- label="Password"
220
- .type=${this._showPassword ? "text" : "password"}
221
- .disabled=${this._credLoading}
222
- ></md-outlined-text-field>
223
- <md-icon-button @click=${this._togglePassword}>
224
- <ha-svg-icon .path=${this._showPassword ? mdiEyeOff : mdiEye}></ha-svg-icon>
225
- </md-icon-button>
226
- </div>
227
- <div class="form-actions">
228
- <md-text-button @click=${this._cancelCred} .disabled=${this._credLoading}
229
- >Cancel</md-text-button
230
- >
231
- ${this.client.serverInfo.wifi_credentials_set ? html`<md-text-button
232
- @click=${handleAsync(() => this._removeWifi())}
233
- .disabled=${this._credLoading}
234
- >Remove</md-text-button
235
- >` : nothing}
236
- <md-filled-button
237
- @click=${handleAsync(() => this._saveWifi())}
238
- .disabled=${this._credLoading}
239
- >Save</md-filled-button
240
- >
241
- </div>
242
- </div>` : nothing}
243
-
244
- <div class="cred-row cred-row-thread">
245
- <div class="cred-info">
246
- <ha-svg-icon .path=${mdiAccessPoint}></ha-svg-icon>
247
- <span class="cred-label">Thread</span>
248
- ${this.client.serverInfo.thread_credentials_set ? html`<span class="cred-value">Thread network set</span>` : html`<span class="cred-unset">Not configured</span>`}
249
- </div>
250
- <md-text-button @click=${() => this._toggleExpand("thread")} .disabled=${this._credLoading}
251
- >Edit</md-text-button
252
- >
253
- </div>
254
-
255
- ${this._expandedRow === "thread" ? html` <div class="cred-form">
256
- <md-outlined-text-field
257
- id="cred-thread-dataset"
258
- label="Thread dataset"
259
- supporting-text="Hex string (e.g. 0E080000...)"
260
- .disabled=${this._credLoading}
261
- ></md-outlined-text-field>
262
- <div class="form-actions">
263
- <md-text-button @click=${this._cancelCred} .disabled=${this._credLoading}
264
- >Cancel</md-text-button
265
- >
266
- ${this.client.serverInfo.thread_credentials_set ? html`<md-text-button
267
- @click=${handleAsync(() => this._removeThread())}
268
- .disabled=${this._credLoading}
269
- >Remove</md-text-button
270
- >` : nothing}
271
- <md-filled-button
272
- @click=${handleAsync(() => this._saveThread())}
273
- .disabled=${this._credLoading}
274
- >Save</md-filled-button
275
- >
276
- </div>
277
- </div>` : nothing}
589
+ ${this._supportsCredentialLists ? this._renderWifiList() : this._renderLegacyWifi()}
590
+ ${this._supportsCredentialLists ? this._renderThreadList() : this._renderLegacyThread()}
278
591
 
279
592
  <p class="cred-hint">Used when commissioning new devices. Existing devices are not affected.</p>
280
593
  </section>
@@ -354,6 +667,60 @@ SettingsDialog.styles = css`
354
667
  margin-top: 8px;
355
668
  }
356
669
 
670
+ .cred-group {
671
+ display: flex;
672
+ flex-direction: column;
673
+ gap: 2px;
674
+ }
675
+
676
+ .cred-group-thread {
677
+ margin-top: 16px;
678
+ }
679
+
680
+ .cred-group-header {
681
+ display: flex;
682
+ align-items: center;
683
+ justify-content: space-between;
684
+ }
685
+
686
+ .cred-entry {
687
+ gap: 8px;
688
+ }
689
+
690
+ .cred-row-actions {
691
+ display: flex;
692
+ align-items: center;
693
+ gap: 2px;
694
+ }
695
+
696
+ .cred-row-actions md-icon-button {
697
+ --md-icon-button-icon-size: 18px;
698
+ }
699
+
700
+ .cred-badge {
701
+ font-size: 0.7rem;
702
+ font-weight: 500;
703
+ text-transform: uppercase;
704
+ letter-spacing: 0.05em;
705
+ padding: 1px 8px;
706
+ border-radius: 10px;
707
+ background: var(--md-sys-color-secondary-container);
708
+ color: var(--md-sys-color-on-secondary-container);
709
+ }
710
+
711
+ .cred-empty {
712
+ margin: 4px 0 0 28px;
713
+ font-size: 0.85rem;
714
+ font-style: italic;
715
+ color: var(--md-sys-color-on-surface-variant);
716
+ }
717
+
718
+ .cred-error {
719
+ margin: 0;
720
+ font-size: 0.8rem;
721
+ color: var(--md-sys-color-error);
722
+ }
723
+
357
724
  .cred-info {
358
725
  display: flex;
359
726
  align-items: center;
@@ -438,15 +805,39 @@ __decorateClass([
438
805
  __decorateClass([
439
806
  state()
440
807
  ], SettingsDialog.prototype, "_showPassword", 2);
808
+ __decorateClass([
809
+ state()
810
+ ], SettingsDialog.prototype, "_creds", 2);
811
+ __decorateClass([
812
+ state()
813
+ ], SettingsDialog.prototype, "_wifiEditId", 2);
814
+ __decorateClass([
815
+ state()
816
+ ], SettingsDialog.prototype, "_wifiAdding", 2);
817
+ __decorateClass([
818
+ state()
819
+ ], SettingsDialog.prototype, "_threadEditId", 2);
820
+ __decorateClass([
821
+ state()
822
+ ], SettingsDialog.prototype, "_threadAdding", 2);
823
+ __decorateClass([
824
+ state()
825
+ ], SettingsDialog.prototype, "_credError", 2);
441
826
  __decorateClass([
442
827
  query("#cred-wifi-ssid")
443
828
  ], SettingsDialog.prototype, "_wifiSsidField", 2);
444
829
  __decorateClass([
445
830
  query("#cred-wifi-password")
446
831
  ], SettingsDialog.prototype, "_wifiPasswordField", 2);
832
+ __decorateClass([
833
+ query("#cred-wifi-id")
834
+ ], SettingsDialog.prototype, "_wifiIdField", 2);
447
835
  __decorateClass([
448
836
  query("#cred-thread-dataset")
449
837
  ], SettingsDialog.prototype, "_threadDatasetField", 2);
838
+ __decorateClass([
839
+ query("#cred-thread-id")
840
+ ], SettingsDialog.prototype, "_threadIdField", 2);
450
841
  SettingsDialog = __decorateClass([
451
842
  customElement("settings-dialog")
452
843
  ], SettingsDialog);