@matter-server/dashboard 0.2.7-alpha.0-20260118-993a1c7 → 0.2.7-alpha.0-20260119-49e7237

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 (80) 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 +3 -2
  3. package/dist/esm/components/dialogs/binding/node-binding-dialog.js.map +1 -1
  4. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.d.ts.map +1 -1
  5. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.js +2 -1
  6. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-existing.js.map +1 -1
  7. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.d.ts.map +1 -1
  8. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.js +3 -2
  9. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-thread.js.map +1 -1
  10. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.d.ts.map +1 -1
  11. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.js +5 -4
  12. package/dist/esm/components/dialogs/commission-node-dialog/commission-node-wifi.js.map +1 -1
  13. package/dist/esm/components/dialogs/settings/log-level-dialog.d.ts +33 -0
  14. package/dist/esm/components/dialogs/settings/log-level-dialog.d.ts.map +1 -0
  15. package/dist/esm/components/dialogs/settings/log-level-dialog.js +189 -0
  16. package/dist/esm/components/dialogs/settings/log-level-dialog.js.map +6 -0
  17. package/dist/esm/components/dialogs/settings/show-log-level-dialog.d.ts +8 -0
  18. package/dist/esm/components/dialogs/settings/show-log-level-dialog.d.ts.map +1 -0
  19. package/dist/esm/components/dialogs/settings/show-log-level-dialog.js +15 -0
  20. package/dist/esm/components/dialogs/settings/show-log-level-dialog.js.map +6 -0
  21. package/dist/esm/pages/cluster-commands/clusters/level-control-commands.d.ts.map +1 -1
  22. package/dist/esm/pages/cluster-commands/clusters/level-control-commands.js +5 -2
  23. package/dist/esm/pages/cluster-commands/clusters/level-control-commands.js.map +1 -1
  24. package/dist/esm/pages/cluster-commands/clusters/on-off-commands.d.ts.map +1 -1
  25. package/dist/esm/pages/cluster-commands/clusters/on-off-commands.js +6 -3
  26. package/dist/esm/pages/cluster-commands/clusters/on-off-commands.js.map +1 -1
  27. package/dist/esm/pages/components/header.d.ts +1 -0
  28. package/dist/esm/pages/components/header.d.ts.map +1 -1
  29. package/dist/esm/pages/components/header.js +13 -1
  30. package/dist/esm/pages/components/header.js.map +1 -1
  31. package/dist/esm/pages/components/node-details.d.ts.map +1 -1
  32. package/dist/esm/pages/components/node-details.js +6 -5
  33. package/dist/esm/pages/components/node-details.js.map +1 -1
  34. package/dist/esm/pages/components/server-details.d.ts.map +1 -1
  35. package/dist/esm/pages/components/server-details.js +3 -2
  36. package/dist/esm/pages/components/server-details.js.map +1 -1
  37. package/dist/esm/pages/matter-server-view.d.ts.map +1 -1
  38. package/dist/esm/pages/matter-server-view.js +14 -1
  39. package/dist/esm/pages/matter-server-view.js.map +1 -1
  40. package/dist/esm/util/async-handler.d.ts +50 -0
  41. package/dist/esm/util/async-handler.d.ts.map +1 -0
  42. package/dist/esm/util/async-handler.js +33 -0
  43. package/dist/esm/util/async-handler.js.map +6 -0
  44. package/dist/esm/util/fire_event.d.ts.map +1 -1
  45. package/dist/esm/util/fire_event.js +1 -2
  46. package/dist/esm/util/fire_event.js.map +1 -1
  47. package/dist/esm/util/format_hex.d.ts +13 -2
  48. package/dist/esm/util/format_hex.d.ts.map +1 -1
  49. package/dist/esm/util/format_hex.js +6 -1
  50. package/dist/esm/util/format_hex.js.map +1 -1
  51. package/dist/web/js/{commission-node-dialog-DGw5qDgH.js → commission-node-dialog-B4_wgFye.js} +5 -5
  52. package/dist/web/js/{commission-node-existing-CHyyeC8y.js → commission-node-existing-BktL7vHX.js} +6 -5
  53. package/dist/web/js/{commission-node-thread-iRDSlidy.js → commission-node-thread-ox6fB3dO.js} +7 -6
  54. package/dist/web/js/{commission-node-wifi-C4YNR3bG.js → commission-node-wifi-Dlayi41Z.js} +9 -8
  55. package/dist/web/js/{dialog-box-ag-xOaYh.js → dialog-box-DFs0hcPv.js} +2 -2
  56. package/dist/web/js/{fire_event-BeiEbHcE.js → fire_event-oVtV3_P5.js} +2 -3
  57. package/dist/web/js/log-level-dialog-BVxKJJ49.js +3235 -0
  58. package/dist/web/js/main.js +170 -69
  59. package/dist/web/js/{matter-dashboard-app-BxQ4W_uT.js → matter-dashboard-app-Dr-IYMsD.js} +138 -17
  60. package/dist/web/js/{node-binding-dialog-ClziphM0.js → node-binding-dialog-CqE3VuZN.js} +6 -5
  61. package/dist/web/js/outlined-text-field-BVcHUxiS.js +968 -0
  62. package/dist/web/js/{prevent_default-Bs2sUnny.js → prevent_default-tLhqZmsK.js} +1 -1
  63. package/dist/web/js/validator-muF28wYx.js +1122 -0
  64. package/package.json +4 -4
  65. package/src/components/dialogs/binding/node-binding-dialog.ts +3 -2
  66. package/src/components/dialogs/commission-node-dialog/commission-node-existing.ts +2 -1
  67. package/src/components/dialogs/commission-node-dialog/commission-node-thread.ts +3 -2
  68. package/src/components/dialogs/commission-node-dialog/commission-node-wifi.ts +5 -4
  69. package/src/components/dialogs/settings/log-level-dialog.ts +183 -0
  70. package/src/components/dialogs/settings/show-log-level-dialog.ts +14 -0
  71. package/src/pages/cluster-commands/clusters/level-control-commands.ts +5 -2
  72. package/src/pages/cluster-commands/clusters/on-off-commands.ts +6 -3
  73. package/src/pages/components/header.ts +16 -1
  74. package/src/pages/components/node-details.ts +6 -5
  75. package/src/pages/components/server-details.ts +3 -3
  76. package/src/pages/matter-server-view.ts +17 -2
  77. package/src/util/async-handler.ts +74 -0
  78. package/src/util/fire_event.ts +1 -3
  79. package/src/util/format_hex.ts +17 -2
  80. package/dist/web/js/outlined-text-field-B-CiqgEJ.js +0 -2086
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@matter-server/dashboard",
3
- "version": "0.2.7-alpha.0-20260118-993a1c7",
3
+ "version": "0.2.7-alpha.0-20260119-49e7237",
4
4
  "description": "Dashboard for OHF Matter Server",
5
5
  "bugs": {
6
6
  "url": "https://github.com/matter-js/matterjs-server/issues"
@@ -23,7 +23,7 @@
23
23
  },
24
24
  "devDependencies": {
25
25
  "@babel/preset-env": "^7.28.5",
26
- "@matter/main": "0.16.5",
26
+ "@matter/main": "0.16.6-alpha.0-20260118-ceec17415",
27
27
  "@rollup/plugin-babel": "^6.1.0",
28
28
  "@rollup/plugin-commonjs": "^29.0.0",
29
29
  "rollup-plugin-copy": "^3.5.0",
@@ -37,8 +37,8 @@
37
37
  "dependencies": {
38
38
  "@lit/context": "^1.1.6",
39
39
  "@material/web": "^2.4.1",
40
- "@matter-server/ws-client": "0.2.7-alpha.0-20260118-993a1c7",
41
- "@matter-server/custom-clusters": "0.2.7-alpha.0-20260118-993a1c7",
40
+ "@matter-server/ws-client": "0.2.7-alpha.0-20260119-49e7237",
41
+ "@matter-server/custom-clusters": "0.2.7-alpha.0-20260119-49e7237",
42
42
  "@mdi/js": "^7.4.47",
43
43
  "lit": "^3.3.1",
44
44
  "tslib": "^2.8.1"
@@ -16,6 +16,7 @@ import "../../../components/ha-svg-icon";
16
16
  import { AccessControlEntry, BindingTarget, MatterClient, MatterNode } from "@matter-server/ws-client";
17
17
  import { css, html, LitElement, nothing } from "lit";
18
18
  import { customElement, property, query } from "lit/decorators.js";
19
+ import { handleAsync } from "../../../util/async-handler.js";
19
20
  import { preventDefault } from "../../../util/prevent_default.js";
20
21
  import { BindingEntryDataTransformer, BindingEntryStruct, InputType } from "./model.js";
21
22
 
@@ -314,7 +315,7 @@ export class NodeBindingDialog extends LitElement {
314
315
  </div>
315
316
  <div slot="end">
316
317
  <md-text-button
317
- @click=${() => this.deleteBindingHandler(index)}
318
+ @click=${handleAsync(() => this.deleteBindingHandler(index))}
318
319
  >delete</md-text-button
319
320
  </div>
320
321
  </md-list-item>
@@ -368,7 +369,7 @@ export class NodeBindingDialog extends LitElement {
368
369
  </div>
369
370
  </div>
370
371
  <div slot="actions">
371
- <md-text-button @click=${this.addBindingHandler}>Add</md-text-button>
372
+ <md-text-button @click=${handleAsync(() => this.addBindingHandler())}>Add</md-text-button>
372
373
  <md-text-button @click=${this._close}>Cancel</md-text-button>
373
374
  </div>
374
375
  </md-dialog>
@@ -12,6 +12,7 @@ import { MatterClient } from "@matter-server/ws-client";
12
12
  import { LitElement, html, nothing } from "lit";
13
13
  import { customElement, property, query, state } from "lit/decorators.js";
14
14
  import { clientContext } from "../../../client/client-context.js";
15
+ import { handleAsync } from "../../../util/async-handler.js";
15
16
  import { fireEvent } from "../../../util/fire_event.js";
16
17
 
17
18
  @customElement("commission-node-existing")
@@ -30,7 +31,7 @@ export class CommissionNodeExisting extends LitElement {
30
31
  return html`<md-outlined-text-field label="Share code" .disabled="${this._loading}"> </md-outlined-text-field>
31
32
  <br />
32
33
  <br />
33
- <md-outlined-button @click=${this._commissionNode} .disabled="${this._loading}"
34
+ <md-outlined-button @click=${handleAsync(() => this._commissionNode())} .disabled="${this._loading}"
34
35
  >Commission</md-outlined-button
35
36
  >${this._loading ? html`<md-circular-progress indeterminate></md-circular-progress>` : nothing}`;
36
37
  }
@@ -12,6 +12,7 @@ import { MatterClient } from "@matter-server/ws-client";
12
12
  import { LitElement, html, nothing } from "lit";
13
13
  import { customElement, property, query, state } from "lit/decorators.js";
14
14
  import { clientContext } from "../../../client/client-context.js";
15
+ import { handleAsync } from "../../../util/async-handler.js";
15
16
  import { fireEvent } from "../../../util/fire_event.js";
16
17
 
17
18
  @customElement("commission-node-thread")
@@ -34,14 +35,14 @@ export class CommissionNodeThread extends LitElement {
34
35
  </md-outlined-text-field>
35
36
  <br />
36
37
  <br />
37
- <md-outlined-button @click=${this._setThreadDataset} .disabled="${this._loading}"
38
+ <md-outlined-button @click=${handleAsync(() => this._setThreadDataset())} .disabled="${this._loading}"
38
39
  >Set Thread Dataset</md-outlined-button
39
40
  >${this._loading ? html`<md-circular-progress indeterminate></md-circular-progress>` : nothing}`;
40
41
  }
41
42
  return html`<md-outlined-text-field label="Pairing code" .disabled="${this._loading}"> </md-outlined-text-field>
42
43
  <br />
43
44
  <br />
44
- <md-outlined-button @click=${this._commissionNode} .disabled="${this._loading}"
45
+ <md-outlined-button @click=${handleAsync(() => this._commissionNode())} .disabled="${this._loading}"
45
46
  >Commission</md-outlined-button
46
47
  >${this._loading ? html`<md-circular-progress indeterminate></md-circular-progress>` : nothing}`;
47
48
  }
@@ -12,6 +12,7 @@ import { MatterClient } from "@matter-server/ws-client";
12
12
  import { LitElement, html, nothing } from "lit";
13
13
  import { customElement, property, query, state } from "lit/decorators.js";
14
14
  import { clientContext } from "../../../client/client-context.js";
15
+ import { handleAsync } from "../../../util/async-handler.js";
15
16
  import { fireEvent } from "../../../util/fire_event.js";
16
17
 
17
18
  @customElement("commission-node-wifi")
@@ -37,7 +38,7 @@ export class CommissionNodeWifi extends LitElement {
37
38
  </md-outlined-text-field>
38
39
  <br />
39
40
  <br />
40
- <md-outlined-button @click=${this._setWifiCredentials} .disabled="${this._loading}"
41
+ <md-outlined-button @click=${handleAsync(() => this._setWifiCredentials())} .disabled="${this._loading}"
41
42
  >Set WiFi Credentials</md-outlined-button
42
43
  >${this._loading
43
44
  ? html`<md-circular-progress indeterminate .visible="${this._loading}"></md-circular-progress>`
@@ -46,12 +47,12 @@ export class CommissionNodeWifi extends LitElement {
46
47
  return html`<md-outlined-text-field label="Pairing code" .disabled="${this._loading}"> </md-outlined-text-field>
47
48
  <br />
48
49
  <br />
49
- <md-outlined-button @click=${this._commissionNode} .disabled="${this._loading}"
50
+ <md-outlined-button @click=${handleAsync(() => this._commissionNode())} .disabled="${this._loading}"
50
51
  >Commission</md-outlined-button
51
52
  >${this._loading ? html`<md-circular-progress indeterminate></md-circular-progress>` : nothing}`;
52
53
  }
53
54
 
54
- private _setWifiCredentials() {
55
+ private async _setWifiCredentials() {
55
56
  const ssid = this._ssidField.value;
56
57
  if (!ssid) {
57
58
  alert("SSID is required");
@@ -64,7 +65,7 @@ export class CommissionNodeWifi extends LitElement {
64
65
  }
65
66
  this._loading = true;
66
67
  try {
67
- this.client.setWifiCredentials(ssid, password);
68
+ await this.client.setWifiCredentials(ssid, password);
68
69
  } catch (err) {
69
70
  alert(`Error setting WiFi credentials: \n${(err as Error).message}`);
70
71
  } finally {
@@ -0,0 +1,183 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import "@material/web/button/text-button";
8
+ import "@material/web/dialog/dialog";
9
+ import type { MdDialog } from "@material/web/dialog/dialog.js";
10
+ import "@material/web/select/outlined-select";
11
+ import type { MdOutlinedSelect } from "@material/web/select/outlined-select.js";
12
+ import "@material/web/select/select-option";
13
+ import { LogLevelString, MatterClient } from "@matter-server/ws-client";
14
+ import { css, html, LitElement, nothing } from "lit";
15
+ import { customElement, property, query, state } from "lit/decorators.js";
16
+ import { fireAndForget, handleAsync } from "../../../util/async-handler.js";
17
+ import { preventDefault } from "../../../util/prevent_default.js";
18
+
19
+ const LOG_LEVELS: { value: LogLevelString; label: string }[] = [
20
+ { value: "critical", label: "Critical" },
21
+ { value: "error", label: "Error" },
22
+ { value: "warning", label: "Warning" },
23
+ { value: "info", label: "Info" },
24
+ { value: "debug", label: "Debug" },
25
+ ];
26
+
27
+ @customElement("log-level-dialog")
28
+ export class LogLevelDialog extends LitElement {
29
+ @property({ attribute: false })
30
+ public client!: MatterClient;
31
+
32
+ @state() private _consoleLevel: LogLevelString = "info";
33
+ @state() private _fileLevel: LogLevelString | null = null;
34
+ @state() private _loading = true;
35
+ @state() private _applying = false;
36
+
37
+ @query("md-outlined-select[name='console']")
38
+ private _consoleSelect!: MdOutlinedSelect;
39
+
40
+ @query("md-outlined-select[name='file']")
41
+ private _fileSelect?: MdOutlinedSelect;
42
+
43
+ override connectedCallback() {
44
+ super.connectedCallback();
45
+ fireAndForget(this._loadLogLevels());
46
+ }
47
+
48
+ private async _loadLogLevels() {
49
+ try {
50
+ const result = await this.client.getLogLevel();
51
+ this._consoleLevel = result.console_loglevel;
52
+ this._fileLevel = result.file_loglevel;
53
+ } catch (err) {
54
+ console.error("Failed to load log levels:", err);
55
+ } finally {
56
+ this._loading = false;
57
+ }
58
+ }
59
+
60
+ private async _apply() {
61
+ this._applying = true;
62
+ try {
63
+ const consoleLevel = this._consoleSelect.value as LogLevelString;
64
+ const fileLevel = this._fileSelect?.value as LogLevelString | undefined;
65
+
66
+ const result = await this.client.setLogLevel(
67
+ consoleLevel,
68
+ this._fileLevel !== null ? fileLevel : undefined,
69
+ );
70
+
71
+ this._consoleLevel = result.console_loglevel;
72
+ this._fileLevel = result.file_loglevel;
73
+ this._close();
74
+ } catch (err) {
75
+ console.error("Failed to apply log levels:", err);
76
+ alert("Failed to apply log levels");
77
+ } finally {
78
+ this._applying = false;
79
+ }
80
+ }
81
+
82
+ private _close() {
83
+ this.shadowRoot!.querySelector<MdDialog>("md-dialog")!.close();
84
+ }
85
+
86
+ private _handleClosed() {
87
+ this.parentNode!.removeChild(this);
88
+ }
89
+
90
+ protected override render() {
91
+ return html`
92
+ <md-dialog open @cancel=${preventDefault} @closed=${this._handleClosed}>
93
+ <div slot="headline">Server Log Settings</div>
94
+ <div slot="content">
95
+ ${this._loading
96
+ ? html`<p class="loading">Loading...</p>`
97
+ : html`
98
+ <p class="hint">Changes are temporary and will be reset on the next server restart.</p>
99
+ <div class="form-field">
100
+ <label>Console Log Level</label>
101
+ <md-outlined-select name="console" .value=${this._consoleLevel}>
102
+ ${LOG_LEVELS.map(
103
+ level => html`
104
+ <md-select-option
105
+ value=${level.value}
106
+ ?selected=${level.value === this._consoleLevel}
107
+ >
108
+ <div slot="headline">${level.label}</div>
109
+ </md-select-option>
110
+ `,
111
+ )}
112
+ </md-outlined-select>
113
+ </div>
114
+ ${this._fileLevel !== null
115
+ ? html`
116
+ <div class="form-field">
117
+ <label>File Log Level</label>
118
+ <md-outlined-select name="file" .value=${this._fileLevel}>
119
+ ${LOG_LEVELS.map(
120
+ level => html`
121
+ <md-select-option
122
+ value=${level.value}
123
+ ?selected=${level.value === this._fileLevel}
124
+ >
125
+ <div slot="headline">${level.label}</div>
126
+ </md-select-option>
127
+ `,
128
+ )}
129
+ </md-outlined-select>
130
+ </div>
131
+ `
132
+ : nothing}
133
+ `}
134
+ </div>
135
+ <div slot="actions">
136
+ <md-text-button @click=${this._close}>Cancel</md-text-button>
137
+ <md-text-button
138
+ @click=${handleAsync(() => this._apply())}
139
+ ?disabled=${this._loading || this._applying}
140
+ >
141
+ ${this._applying ? "Applying..." : "Apply"}
142
+ </md-text-button>
143
+ </div>
144
+ </md-dialog>
145
+ `;
146
+ }
147
+
148
+ static override styles = css`
149
+ .loading {
150
+ text-align: center;
151
+ padding: 24px;
152
+ color: var(--md-sys-color-on-surface-variant);
153
+ }
154
+
155
+ .hint {
156
+ font-size: 0.875rem;
157
+ color: var(--md-sys-color-on-surface-variant);
158
+ margin: 0 0 16px 0;
159
+ font-style: italic;
160
+ }
161
+
162
+ .form-field {
163
+ margin-bottom: 16px;
164
+ }
165
+
166
+ .form-field label {
167
+ display: block;
168
+ margin-bottom: 8px;
169
+ font-weight: 500;
170
+ color: var(--md-sys-color-on-surface);
171
+ }
172
+
173
+ md-outlined-select {
174
+ width: 100%;
175
+ }
176
+ `;
177
+ }
178
+
179
+ declare global {
180
+ interface HTMLElementTagNameMap {
181
+ "log-level-dialog": LogLevelDialog;
182
+ }
183
+ }
@@ -0,0 +1,14 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ import { MatterClient } from "@matter-server/ws-client";
8
+
9
+ export const showLogLevelDialog = async (client: MatterClient) => {
10
+ await import("./log-level-dialog.js");
11
+ const dialog = document.createElement("log-level-dialog");
12
+ dialog.client = client;
13
+ document.querySelector("matter-dashboard-app")?.renderRoot.appendChild(dialog);
14
+ };
@@ -7,6 +7,7 @@
7
7
  import "@material/web/button/filled-button";
8
8
  import { html } from "lit";
9
9
  import { customElement, state } from "lit/decorators.js";
10
+ import { handleAsync } from "../../../util/async-handler.js";
10
11
  import { BaseClusterCommands } from "../base-cluster-commands.js";
11
12
  import { registerClusterCommands } from "../registry.js";
12
13
 
@@ -60,8 +61,10 @@ class LevelControlClusterCommands extends BaseClusterCommands {
60
61
  />
61
62
  Execute if Off
62
63
  </label>
63
- <md-outlined-button @click=${this._handleMoveToLevel}>MoveToLevel</md-outlined-button>
64
- <md-outlined-button @click=${this._handleMoveToLevelWithOnOff}
64
+ <md-outlined-button @click=${handleAsync(() => this._handleMoveToLevel())}
65
+ >MoveToLevel</md-outlined-button
66
+ >
67
+ <md-outlined-button @click=${handleAsync(() => this._handleMoveToLevelWithOnOff())}
65
68
  >MoveToLevelWithOnOff</md-outlined-button
66
69
  >
67
70
  </div>
@@ -8,6 +8,7 @@ import "@material/web/button/filled-button";
8
8
  import "@material/web/button/outlined-button";
9
9
  import { html } from "lit";
10
10
  import { customElement } from "lit/decorators.js";
11
+ import { handleAsync } from "../../../util/async-handler.js";
11
12
  import { BaseClusterCommands } from "../base-cluster-commands.js";
12
13
  import { registerClusterCommands } from "../registry.js";
13
14
 
@@ -25,9 +26,11 @@ class OnOffClusterCommands extends BaseClusterCommands {
25
26
  <summary>OnOff Commands</summary>
26
27
  <div class="command-content">
27
28
  <div class="command-row">
28
- <md-outlined-button @click=${this._handleOn}>On</md-outlined-button>
29
- <md-outlined-button @click=${this._handleOff}>Off</md-outlined-button>
30
- <md-outlined-button @click=${this._handleToggle}>Toggle</md-outlined-button>
29
+ <md-outlined-button @click=${handleAsync(() => this._handleOn())}>On</md-outlined-button>
30
+ <md-outlined-button @click=${handleAsync(() => this._handleOff())}>Off</md-outlined-button>
31
+ <md-outlined-button @click=${handleAsync(() => this._handleToggle())}
32
+ >Toggle</md-outlined-button
33
+ >
31
34
  </div>
32
35
  </div>
33
36
  </details>
@@ -10,9 +10,10 @@ import "@material/web/iconbutton/icon-button";
10
10
  import "@material/web/list/list";
11
11
  import "@material/web/list/list-item";
12
12
  import { MatterClient } from "@matter-server/ws-client";
13
- import { mdiArrowLeft, mdiBrightnessAuto, mdiLogout, mdiWeatherNight, mdiWeatherSunny } from "@mdi/js";
13
+ import { mdiArrowLeft, mdiBrightnessAuto, mdiCog, mdiLogout, mdiWeatherNight, mdiWeatherSunny } from "@mdi/js";
14
14
  import { LitElement, css, html, nothing } from "lit";
15
15
  import { customElement, property, state } from "lit/decorators.js";
16
+ import { showLogLevelDialog } from "../../components/dialogs/settings/show-log-level-dialog.js";
16
17
  import "../../components/ha-svg-icon";
17
18
  import { EffectiveTheme, ThemePreference, ThemeService } from "../../util/theme-service.js";
18
19
 
@@ -51,6 +52,12 @@ export class DashboardHeader extends LitElement {
51
52
  ThemeService.cycleTheme();
52
53
  }
53
54
 
55
+ private _openSettings() {
56
+ if (this.client) {
57
+ showLogLevelDialog(this.client);
58
+ }
59
+ }
60
+
54
61
  private _getThemeIcon(): string {
55
62
  switch (this._themePreference) {
56
63
  case "light":
@@ -95,6 +102,14 @@ export class DashboardHeader extends LitElement {
95
102
  </md-icon-button>
96
103
  `;
97
104
  })}
105
+ <!-- settings button (only when connected) -->
106
+ ${this.client
107
+ ? html`
108
+ <md-icon-button @click=${this._openSettings} title="Server Settings">
109
+ <ha-svg-icon .path=${mdiCog}></ha-svg-icon>
110
+ </md-icon-button>
111
+ `
112
+ : nothing}
98
113
  <!-- theme toggle button -->
99
114
  <md-icon-button @click=${this._cycleTheme} .title=${this._getThemeTooltip()}>
100
115
  <ha-svg-icon .path=${this._getThemeIcon()}></ha-svg-icon>
@@ -21,6 +21,7 @@ import { DeviceType } from "../../client/models/descriptions.js";
21
21
  import { showAlertDialog, showPromptDialog } from "../../components/dialog-box/show-dialog-box.js";
22
22
  import { showNodeBindingDialog } from "../../components/dialogs/binding/show-node-binding-dialog.js";
23
23
  import "../../components/ha-svg-icon";
24
+ import { handleAsync } from "../../util/async-handler.js";
24
25
  import { getEndpointDeviceTypes } from "../matter-endpoint-view.js";
25
26
  import { bindingContext } from "./context.js";
26
27
 
@@ -104,7 +105,7 @@ export class NodeDetails extends LitElement {
104
105
  </div>`}
105
106
  </md-list-item>
106
107
  <md-list-item class="btn">
107
- <md-outlined-button @click=${this._reinterview}
108
+ <md-outlined-button @click=${handleAsync(() => this._reinterview())}
108
109
  >Interview<ha-svg-icon slot="icon" .path=${mdiChatProcessing}></ha-svg-icon
109
110
  ></md-outlined-button>
110
111
  ${this._updateInitiated
@@ -118,22 +119,22 @@ export class NodeDetails extends LitElement {
118
119
  this.node.updateStateProgress,
119
120
  )}<ha-svg-icon slot="icon" .path=${mdiUpdate}></ha-svg-icon
120
121
  ></md-outlined-button>`
121
- : html`<md-outlined-button @click=${this._searchUpdate}
122
+ : html`<md-outlined-button @click=${handleAsync(() => this._searchUpdate())}
122
123
  >Update<ha-svg-icon slot="icon" .path=${mdiUpdate}></ha-svg-icon
123
124
  ></md-outlined-button>`}
124
125
  ${bindings
125
126
  ? html`
126
- <md-outlined-button @click=${this._binding}>
127
+ <md-outlined-button @click=${handleAsync(() => this._binding())}>
127
128
  Binding
128
129
  <ha-svg-icon slot="icon" .path=${mdiLink}></ha-svg-icon>
129
130
  </md-outlined-button>
130
131
  `
131
132
  : nothing}
132
133
 
133
- <md-outlined-button @click=${this._openCommissioningWindow}
134
+ <md-outlined-button @click=${handleAsync(() => this._openCommissioningWindow())}
134
135
  >Share<ha-svg-icon slot="icon" .path=${mdiShareVariant}></ha-svg-icon
135
136
  ></md-outlined-button>
136
- <md-outlined-button @click=${this._remove}
137
+ <md-outlined-button @click=${handleAsync(() => this._remove())}
137
138
  >Remove<ha-svg-icon slot="icon" .path=${mdiTrashCan}></ha-svg-icon
138
139
  ></md-outlined-button>
139
140
  </md-list-item>
@@ -18,6 +18,7 @@ import { customElement } from "lit/decorators.js";
18
18
  import { showAlertDialog, showPromptDialog } from "../../components/dialog-box/show-dialog-box.js";
19
19
  import { showCommissionNodeDialog } from "../../components/dialogs/commission-node-dialog/show-commission-node-dialog.js";
20
20
  import "../../components/ha-svg-icon";
21
+ import { handleAsync } from "../../util/async-handler.js";
21
22
 
22
23
  @customElement("server-details")
23
24
  export class ServerDetails extends LitElement {
@@ -54,7 +55,7 @@ export class ServerDetails extends LitElement {
54
55
  <md-list-item class="btn">
55
56
  <span>
56
57
  <md-outlined-button @click=${this._commissionNode}>Commission node<ha-svg-icon slot="icon" .path=${mdiPlus}></ha-svg-icon></md-outlined-button>
57
- <md-outlined-button @click=${this._uploadDiagnosticsDumpFile}>Import node<ha-svg-icon slot="icon" .path=${mdiFile}></ha-svg-icon></md-outlined-button>
58
+ <md-outlined-button @click=${handleAsync(() => this._uploadDiagnosticsDumpFile())}>Import node<ha-svg-icon slot="icon" .path=${mdiFile}></ha-svg-icon></md-outlined-button>
58
59
  </md-list-item>
59
60
  </md-list>
60
61
  <!-- hidden file element for the upload diagnostics -->
@@ -83,8 +84,7 @@ export class ServerDetails extends LitElement {
83
84
  ) {
84
85
  return;
85
86
  }
86
- // @ts-expect-error why?
87
- const fileElem = this.renderRoot.getElementById("fileElem") as HTMLInputElement;
87
+ const fileElem = this.shadowRoot!.getElementById("fileElem") as HTMLInputElement;
88
88
  fileElem.click();
89
89
  }
90
90
 
@@ -8,11 +8,12 @@ import "@material/web/divider/divider";
8
8
  import "@material/web/iconbutton/icon-button";
9
9
  import "@material/web/list/list";
10
10
  import "@material/web/list/list-item";
11
- import { MatterClient, MatterNode } from "@matter-server/ws-client";
11
+ import { isTestNodeId, MatterClient, MatterNode } from "@matter-server/ws-client";
12
12
  import { mdiChevronRight } from "@mdi/js";
13
- import { LitElement, css, html, nothing } from "lit";
13
+ import { css, html, LitElement, nothing } from "lit";
14
14
  import { customElement, property } from "lit/decorators.js";
15
15
  import "../components/ha-svg-icon";
16
+ import { formatNodeAddress } from "../util/format_hex.js";
16
17
  import "./components/footer";
17
18
  import "./components/header";
18
19
  import "./components/server-details";
@@ -65,6 +66,15 @@ class MatterServerView extends LitElement {
65
66
  <md-list-item type="link" href=${`#node/${node.node_id}`}>
66
67
  <div slot="headline">
67
68
  Node ${node.node_id}
69
+ <span class="hex-id"
70
+ >(${formatNodeAddress(
71
+ isTestNodeId(node.node_id) ||
72
+ this.client.serverInfo.fabric_index === undefined
73
+ ? undefined
74
+ : this.client.serverInfo.fabric_index,
75
+ node.node_id,
76
+ )})</span
77
+ >
68
78
  ${node.available ? "" : html`<span class="status">OFFLINE</span>`}
69
79
  </div>
70
80
  <div slot="supporting-text">
@@ -113,5 +123,10 @@ class MatterServerView extends LitElement {
113
123
  font-weight: bold;
114
124
  font-size: 0.8em;
115
125
  }
126
+
127
+ .hex-id {
128
+ color: var(--text-color, rgba(0, 0, 0, 0.6));
129
+ font-size: 0.85em;
130
+ }
116
131
  `;
117
132
  }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * @license
3
+ * Copyright 2025-2026 Open Home Foundation
4
+ * SPDX-License-Identifier: Apache-2.0
5
+ */
6
+
7
+ /**
8
+ * Wraps an async function to be used as an event handler.
9
+ * Catches any errors and logs them, optionally calling an error callback.
10
+ *
11
+ * Usage in Lit templates:
12
+ * ```
13
+ * <button @click=${handleAsync(() => this._save())}>Save</button>
14
+ * ```
15
+ *
16
+ * With custom error handling:
17
+ * ```
18
+ * <button @click=${handleAsync(() => this._save(), (e) => this._showError(e))}>Save</button>
19
+ * ```
20
+ *
21
+ * @param fn - Async function to execute
22
+ * @param onError - Optional error callback
23
+ * @returns Event handler function
24
+ */
25
+ export function handleAsync<T>(fn: () => Promise<T>, onError?: (error: Error) => void): () => void {
26
+ return () => {
27
+ fn().catch((error: Error) => {
28
+ console.error("Async operation failed:", error);
29
+ onError?.(error);
30
+ });
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Wraps an async function that takes an event parameter.
36
+ *
37
+ * Usage:
38
+ * ```
39
+ * <input @change=${handleAsyncEvent((e) => this._handleChange(e))}>
40
+ * ```
41
+ */
42
+ export function handleAsyncEvent<E extends Event, T>(
43
+ fn: (event: E) => Promise<T>,
44
+ onError?: (error: Error) => void,
45
+ ): (event: E) => void {
46
+ return (event: E) => {
47
+ fn(event).catch((error: Error) => {
48
+ console.error("Async operation failed:", error);
49
+ onError?.(error);
50
+ });
51
+ };
52
+ }
53
+
54
+ /**
55
+ * Fire-and-forget helper for calling async methods from sync contexts.
56
+ * Use this when you need to call an async method but can't await it.
57
+ *
58
+ * Usage in lifecycle methods:
59
+ * ```
60
+ * connectedCallback() {
61
+ * super.connectedCallback();
62
+ * fireAndForget(this._loadData());
63
+ * }
64
+ * ```
65
+ *
66
+ * @param promise - Promise to execute
67
+ * @param onError - Optional error callback
68
+ */
69
+ export function fireAndForget<T>(promise: Promise<T>, onError?: (error: Error) => void): void {
70
+ promise.catch((error: Error) => {
71
+ console.error("Async operation failed:", error);
72
+ onError?.(error);
73
+ });
74
+ }
@@ -70,14 +70,12 @@ export const fireEvent = <HassEvent extends ValidHassDomEvent>(
70
70
  },
71
71
  ) => {
72
72
  options = options || {};
73
- // @ts-expect-error why?
74
- detail = detail === null || detail === undefined ? {} : detail;
75
73
  const event = new Event(type, {
76
74
  bubbles: options.bubbles === undefined ? true : options.bubbles,
77
75
  cancelable: Boolean(options.cancelable),
78
76
  composed: options.composed === undefined ? true : options.composed,
79
77
  });
80
- (event as any).detail = detail;
78
+ (event as any).detail = detail ?? {};
81
79
  node.dispatchEvent(event);
82
80
  return event;
83
81
  };