@attraccess/plugin-shelly 0.1.0-nightly.925.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +949 -0
- package/dist/migrations.js +57 -0
- package/frontend/__federation_expose_Plugin-Dd1Vs4iR.js +1627 -0
- package/frontend/__federation_fn_import-DAGm_xmC.js +2 -0
- package/frontend/_virtual___federation_fn_import-BXODRNto.js +261 -0
- package/frontend/index.html +13 -0
- package/frontend/jsx-runtime-B9Qmn8k7.js +42 -0
- package/frontend/preload-helper-DxWiKyOd.js +68 -0
- package/frontend/react-D5kF5LxZ.js +383 -0
- package/frontend/remoteEntry.js +74 -0
- package/frontend/rolldown-runtime-EAsCxrDo.js +14 -0
- package/frontend/style.css +442 -0
- package/package.json +44 -0
|
@@ -0,0 +1,1627 @@
|
|
|
1
|
+
import { r as importShared } from "./_virtual___federation_fn_import-BXODRNto.js";
|
|
2
|
+
import { t as require_jsx_runtime } from "./jsx-runtime-B9Qmn8k7.js";
|
|
3
|
+
//#region ../../../libs/plugins-frontend-sdk/src/lib/frontend.api-client.ts
|
|
4
|
+
var BASE_URL_GLOBAL = "__ATTRACCESS_API_BASE_URL__";
|
|
5
|
+
/** The API origin the host is talking to. Falls back to the current origin. */
|
|
6
|
+
function getApiBaseUrl() {
|
|
7
|
+
return window[BASE_URL_GLOBAL] ?? window.location.origin;
|
|
8
|
+
}
|
|
9
|
+
/** Thrown for any non-2xx response, carrying the status for callers that branch on it. */
|
|
10
|
+
var PluginApiError = class extends Error {
|
|
11
|
+
constructor(message, status) {
|
|
12
|
+
super(message);
|
|
13
|
+
this.status = status;
|
|
14
|
+
this.name = "PluginApiError";
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
async function toError(res) {
|
|
18
|
+
try {
|
|
19
|
+
const body = await res.json();
|
|
20
|
+
const message = Array.isArray(body.message) ? body.message.join(", ") : body.message;
|
|
21
|
+
if (message) return new PluginApiError(message, res.status);
|
|
22
|
+
} catch {}
|
|
23
|
+
return new PluginApiError(`Request failed (HTTP ${res.status}).`, res.status);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Creates a client bound to `basePath`, which is resolved against the host's
|
|
27
|
+
* API origin. Plugin routes are mounted under `/api/<plugin-name>`, so that is
|
|
28
|
+
* the usual argument; pass nothing to address the host API directly.
|
|
29
|
+
*/
|
|
30
|
+
function createPluginApiClient(basePath = "") {
|
|
31
|
+
const url = (path, query) => {
|
|
32
|
+
const target = new URL(`${basePath}${path}`, getApiBaseUrl());
|
|
33
|
+
Object.entries(query ?? {}).forEach(([key, value]) => {
|
|
34
|
+
if (value !== void 0 && value !== null) target.searchParams.set(key, String(value));
|
|
35
|
+
});
|
|
36
|
+
return target.toString();
|
|
37
|
+
};
|
|
38
|
+
const doFetch = (path, init) => fetch(url(path), {
|
|
39
|
+
credentials: "include",
|
|
40
|
+
...init
|
|
41
|
+
});
|
|
42
|
+
return {
|
|
43
|
+
url,
|
|
44
|
+
fetch: doFetch,
|
|
45
|
+
async request(path, options = {}) {
|
|
46
|
+
const { body, query, headers, ...init } = options;
|
|
47
|
+
const res = await fetch(url(path, query), {
|
|
48
|
+
credentials: "include",
|
|
49
|
+
...init,
|
|
50
|
+
headers: body === void 0 ? headers : {
|
|
51
|
+
"Content-Type": "application/json",
|
|
52
|
+
...headers
|
|
53
|
+
},
|
|
54
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
55
|
+
});
|
|
56
|
+
if (!res.ok) throw await toError(res);
|
|
57
|
+
const text = await res.text();
|
|
58
|
+
return text.length > 0 ? JSON.parse(text) : null;
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
//#endregion
|
|
63
|
+
//#region frontend/src/api.ts
|
|
64
|
+
var api = createPluginApiClient("/api/shelly");
|
|
65
|
+
function listDevices() {
|
|
66
|
+
return api.request("/devices");
|
|
67
|
+
}
|
|
68
|
+
function addDevice(input) {
|
|
69
|
+
return api.request("/devices", {
|
|
70
|
+
method: "POST",
|
|
71
|
+
body: input
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
function reprobeDevice(id) {
|
|
75
|
+
return api.request(`/devices/${id}/probe`, { method: "POST" });
|
|
76
|
+
}
|
|
77
|
+
function deleteDevice(id) {
|
|
78
|
+
return api.request(`/devices/${id}`, { method: "DELETE" });
|
|
79
|
+
}
|
|
80
|
+
function getDeviceInfo(id, input) {
|
|
81
|
+
return api.request(`/devices/${id}/info`, {
|
|
82
|
+
method: "POST",
|
|
83
|
+
body: input ?? {}
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Firmware state of every registered device, checked in one round trip. */
|
|
87
|
+
function listFirmware() {
|
|
88
|
+
return api.request("/devices/firmware");
|
|
89
|
+
}
|
|
90
|
+
function getFirmware(id, input) {
|
|
91
|
+
const params = new URLSearchParams();
|
|
92
|
+
if (input?.username) params.set("username", input.username);
|
|
93
|
+
if (input?.currentPassword) params.set("currentPassword", input.currentPassword);
|
|
94
|
+
const query = params.size ? `?${params.toString()}` : "";
|
|
95
|
+
return api.request(`/devices/${id}/firmware${query}`);
|
|
96
|
+
}
|
|
97
|
+
function startFirmwareUpdate(id, input) {
|
|
98
|
+
return api.request(`/devices/${id}/firmware/update`, {
|
|
99
|
+
method: "POST",
|
|
100
|
+
body: input
|
|
101
|
+
});
|
|
102
|
+
}
|
|
103
|
+
function setAdminPassword(id, input) {
|
|
104
|
+
return api.request(`/devices/${id}/auth`, {
|
|
105
|
+
method: "POST",
|
|
106
|
+
body: input
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
/**
|
|
110
|
+
* Runs discovery on the server (mDNS + subnet scan) and returns what it found.
|
|
111
|
+
* Slow by nature — a /24 scan takes a few seconds — so callers must show a
|
|
112
|
+
* pending state.
|
|
113
|
+
*/
|
|
114
|
+
function discoverDevices(input = {}) {
|
|
115
|
+
return api.request("/discovery", {
|
|
116
|
+
method: "POST",
|
|
117
|
+
body: input
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
//#endregion
|
|
121
|
+
//#region frontend/src/drawer.tsx
|
|
122
|
+
var import_jsx_runtime = require_jsx_runtime();
|
|
123
|
+
var { Button: Button$6, Description, DrawerBackdrop, DrawerContent, DrawerDialog, Input, InputGroup, Label, TextField, Tooltip: Tooltip$2 } = await importShared("@heroui/react");
|
|
124
|
+
var { EyeIcon: EyeIcon$1, EyeOffIcon: EyeOffIcon$1 } = await importShared("lucide-react");
|
|
125
|
+
var { useState: useState$6 } = await importShared("react");
|
|
126
|
+
var DRAWER_DIALOG_CLASSNAME = "sh:md:max-w-2xl sh:md:mx-auto sh:bg-surface-secondary";
|
|
127
|
+
var FIELD_CONTRAST_STYLE = {
|
|
128
|
+
["--field-border"]: "var(--border-secondary)",
|
|
129
|
+
["--border-width-field"]: "1px"
|
|
130
|
+
};
|
|
131
|
+
function StandardDrawer({ isOpen, onOpenChange, children }) {
|
|
132
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerBackdrop, {
|
|
133
|
+
isOpen,
|
|
134
|
+
onOpenChange,
|
|
135
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerContent, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerDialog, {
|
|
136
|
+
className: DRAWER_DIALOG_CLASSNAME,
|
|
137
|
+
style: FIELD_CONTRAST_STYLE,
|
|
138
|
+
children
|
|
139
|
+
}) })
|
|
140
|
+
});
|
|
141
|
+
}
|
|
142
|
+
function TextFieldRow({ label, value, onChange, placeholder, required, description, dataCy }) {
|
|
143
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField, {
|
|
144
|
+
value,
|
|
145
|
+
onChange,
|
|
146
|
+
isRequired: required,
|
|
147
|
+
children: [
|
|
148
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { children: label }),
|
|
149
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Input, {
|
|
150
|
+
placeholder,
|
|
151
|
+
autoComplete: "off",
|
|
152
|
+
"data-cy": dataCy
|
|
153
|
+
}),
|
|
154
|
+
description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
155
|
+
className: "sh:mt-1 sh:text-xs sh:text-muted",
|
|
156
|
+
children: description
|
|
157
|
+
})
|
|
158
|
+
]
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
function PasswordFieldRow({ label, value, onChange, description, required, dataCy, autoComplete }) {
|
|
162
|
+
const [visible, setVisible] = useState$6(false);
|
|
163
|
+
const toggleLabel = visible ? "Hide password" : "Show password";
|
|
164
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TextField, {
|
|
165
|
+
value,
|
|
166
|
+
onChange,
|
|
167
|
+
isRequired: required,
|
|
168
|
+
children: [
|
|
169
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Label, { children: label }),
|
|
170
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(InputGroup, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroup.Input, {
|
|
171
|
+
type: visible ? "text" : "password",
|
|
172
|
+
autoComplete,
|
|
173
|
+
"data-cy": dataCy
|
|
174
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InputGroup.Suffix, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip$2, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip$2.Trigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$6, {
|
|
175
|
+
isIconOnly: true,
|
|
176
|
+
variant: "ghost",
|
|
177
|
+
"aria-label": toggleLabel,
|
|
178
|
+
onPress: () => setVisible((v) => !v),
|
|
179
|
+
children: visible ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EyeOffIcon$1, { className: "sh:h-4 sh:w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EyeIcon$1, { className: "sh:h-4 sh:w-4" })
|
|
180
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip$2.Content, { children: toggleLabel })] }) })] }),
|
|
181
|
+
description && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Description, { children: description })
|
|
182
|
+
]
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
//#endregion
|
|
186
|
+
//#region frontend/src/StatusAlert.tsx
|
|
187
|
+
var { Alert, AlertContent, AlertDescription, AlertIndicator, AlertTitle } = await importShared("@heroui/react");
|
|
188
|
+
var { AlertCircleIcon, AlertTriangleIcon, CheckCircle2Icon, InfoIcon: InfoIcon$2 } = await importShared("lucide-react");
|
|
189
|
+
var iconByStatus = {
|
|
190
|
+
default: InfoIcon$2,
|
|
191
|
+
accent: InfoIcon$2,
|
|
192
|
+
success: CheckCircle2Icon,
|
|
193
|
+
warning: AlertTriangleIcon,
|
|
194
|
+
danger: AlertCircleIcon
|
|
195
|
+
};
|
|
196
|
+
function StatusAlert({ status, title, children, dataCy }) {
|
|
197
|
+
const Icon = iconByStatus[status];
|
|
198
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Alert, {
|
|
199
|
+
status,
|
|
200
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertIndicator, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Icon, {}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(AlertContent, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertTitle, { children: title }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AlertDescription, {
|
|
201
|
+
"data-cy": dataCy,
|
|
202
|
+
children
|
|
203
|
+
})] })]
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
//#endregion
|
|
207
|
+
//#region frontend/src/AddDeviceDrawer.tsx
|
|
208
|
+
var { Button: Button$5, DrawerBody: DrawerBody$4, DrawerHeader: DrawerHeader$4, Form: Form$3 } = await importShared("@heroui/react");
|
|
209
|
+
var { PlusIcon: PlusIcon$1, XIcon: XIcon$4 } = await importShared("lucide-react");
|
|
210
|
+
var { useCallback: useCallback$5, useRef: useRef$2, useState: useState$5 } = await importShared("react");
|
|
211
|
+
function AddDeviceDrawer({ isOpen, onOpenChange, onAdded }) {
|
|
212
|
+
const [ipAddress, setIpAddress] = useState$5("");
|
|
213
|
+
const [name, setName] = useState$5("");
|
|
214
|
+
const [submitting, setSubmitting] = useState$5(false);
|
|
215
|
+
const [error, setError] = useState$5(null);
|
|
216
|
+
const close = useCallback$5(() => onOpenChange(false), [onOpenChange]);
|
|
217
|
+
const inFlight = useRef$2(false);
|
|
218
|
+
const submit = useCallback$5(async () => {
|
|
219
|
+
if (inFlight.current) return;
|
|
220
|
+
const ip = ipAddress.trim();
|
|
221
|
+
if (!ip) {
|
|
222
|
+
setError("IP address is required.");
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
inFlight.current = true;
|
|
226
|
+
setSubmitting(true);
|
|
227
|
+
setError(null);
|
|
228
|
+
try {
|
|
229
|
+
await addDevice({
|
|
230
|
+
ipAddress: ip,
|
|
231
|
+
name: name.trim() || void 0
|
|
232
|
+
});
|
|
233
|
+
setIpAddress("");
|
|
234
|
+
setName("");
|
|
235
|
+
onAdded();
|
|
236
|
+
close();
|
|
237
|
+
} catch (err) {
|
|
238
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
239
|
+
} finally {
|
|
240
|
+
inFlight.current = false;
|
|
241
|
+
setSubmitting(false);
|
|
242
|
+
}
|
|
243
|
+
}, [
|
|
244
|
+
ipAddress,
|
|
245
|
+
name,
|
|
246
|
+
onAdded,
|
|
247
|
+
close
|
|
248
|
+
]);
|
|
249
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(StandardDrawer, {
|
|
250
|
+
isOpen,
|
|
251
|
+
onOpenChange,
|
|
252
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerHeader$4, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
253
|
+
className: "sh:flex sh:w-full sh:items-start sh:justify-between sh:gap-3",
|
|
254
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
255
|
+
className: "sh:flex sh:flex-col sh:gap-1",
|
|
256
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
|
|
257
|
+
className: "sh:text-lg sh:font-semibold",
|
|
258
|
+
children: "Add a device"
|
|
259
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
260
|
+
className: "sh:text-sm sh:text-muted",
|
|
261
|
+
children: [
|
|
262
|
+
"Enter the device's IP address. We probe ",
|
|
263
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "GET /shelly" }),
|
|
264
|
+
" to detect its generation and model."
|
|
265
|
+
]
|
|
266
|
+
})]
|
|
267
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$5, {
|
|
268
|
+
isIconOnly: true,
|
|
269
|
+
variant: "ghost",
|
|
270
|
+
"aria-label": "Close",
|
|
271
|
+
onPress: close,
|
|
272
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon$4, { size: 16 })
|
|
273
|
+
})]
|
|
274
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerBody$4, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Form$3, {
|
|
275
|
+
onSubmit: submit,
|
|
276
|
+
className: "sh:flex sh:flex-col sh:gap-4",
|
|
277
|
+
children: [
|
|
278
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextFieldRow, {
|
|
279
|
+
label: "IP address",
|
|
280
|
+
value: ipAddress,
|
|
281
|
+
onChange: setIpAddress,
|
|
282
|
+
placeholder: "192.168.1.42",
|
|
283
|
+
required: true,
|
|
284
|
+
dataCy: "shelly-add-ip"
|
|
285
|
+
}),
|
|
286
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextFieldRow, {
|
|
287
|
+
label: "Name (optional)",
|
|
288
|
+
value: name,
|
|
289
|
+
onChange: setName,
|
|
290
|
+
placeholder: "Workshop light",
|
|
291
|
+
dataCy: "shelly-add-name"
|
|
292
|
+
}),
|
|
293
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
|
|
294
|
+
status: "danger",
|
|
295
|
+
title: "Could not add the device",
|
|
296
|
+
dataCy: "shelly-add-error",
|
|
297
|
+
children: error
|
|
298
|
+
}),
|
|
299
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
300
|
+
className: "sh:flex sh:justify-end sh:gap-2 sh:pt-2",
|
|
301
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$5, {
|
|
302
|
+
variant: "secondary",
|
|
303
|
+
onPress: close,
|
|
304
|
+
children: "Cancel"
|
|
305
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$5, {
|
|
306
|
+
variant: "primary",
|
|
307
|
+
type: "submit",
|
|
308
|
+
isPending: submitting,
|
|
309
|
+
onPress: submit,
|
|
310
|
+
"data-cy": "shelly-add-submit",
|
|
311
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlusIcon$1, { className: "sh:h-4 sh:w-4" }), " Add device"]
|
|
312
|
+
})]
|
|
313
|
+
}),
|
|
314
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
315
|
+
type: "submit",
|
|
316
|
+
hidden: true
|
|
317
|
+
})
|
|
318
|
+
]
|
|
319
|
+
}) })]
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
//#endregion
|
|
323
|
+
//#region frontend/src/AdminPasswordDrawer.tsx
|
|
324
|
+
var { Button: Button$4, DrawerBody: DrawerBody$3, DrawerFooter: DrawerFooter$2, DrawerHeader: DrawerHeader$3, Form: Form$2 } = await importShared("@heroui/react");
|
|
325
|
+
var { KeyRoundIcon: KeyRoundIcon$1, XIcon: XIcon$3 } = await importShared("lucide-react");
|
|
326
|
+
var { useCallback: useCallback$4, useEffect: useEffect$3, useState: useState$4 } = await importShared("react");
|
|
327
|
+
function AdminPasswordDrawer({ device, onOpenChange, onSaved }) {
|
|
328
|
+
const [currentPassword, setCurrentPassword] = useState$4("");
|
|
329
|
+
const [password, setPassword] = useState$4("");
|
|
330
|
+
const [submitting, setSubmitting] = useState$4(false);
|
|
331
|
+
const [error, setError] = useState$4(null);
|
|
332
|
+
useEffect$3(() => {
|
|
333
|
+
if (device) {
|
|
334
|
+
setCurrentPassword("");
|
|
335
|
+
setPassword("");
|
|
336
|
+
setError(null);
|
|
337
|
+
}
|
|
338
|
+
}, [device]);
|
|
339
|
+
const close = useCallback$4(() => onOpenChange(false), [onOpenChange]);
|
|
340
|
+
const submit = useCallback$4(async () => {
|
|
341
|
+
if (!device) return;
|
|
342
|
+
const nextPassword = password.trim();
|
|
343
|
+
if (!nextPassword) {
|
|
344
|
+
setError("New password is required.");
|
|
345
|
+
return;
|
|
346
|
+
}
|
|
347
|
+
setSubmitting(true);
|
|
348
|
+
setError(null);
|
|
349
|
+
try {
|
|
350
|
+
await setAdminPassword(device.id, {
|
|
351
|
+
currentPassword: currentPassword || void 0,
|
|
352
|
+
password: nextPassword
|
|
353
|
+
});
|
|
354
|
+
onSaved();
|
|
355
|
+
close();
|
|
356
|
+
} catch (err) {
|
|
357
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
358
|
+
} finally {
|
|
359
|
+
setSubmitting(false);
|
|
360
|
+
}
|
|
361
|
+
}, [
|
|
362
|
+
close,
|
|
363
|
+
currentPassword,
|
|
364
|
+
device,
|
|
365
|
+
onSaved,
|
|
366
|
+
password
|
|
367
|
+
]);
|
|
368
|
+
const onSubmit = useCallback$4((event) => {
|
|
369
|
+
event.preventDefault();
|
|
370
|
+
submit();
|
|
371
|
+
}, [submit]);
|
|
372
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(StandardDrawer, {
|
|
373
|
+
isOpen: !!device,
|
|
374
|
+
onOpenChange,
|
|
375
|
+
children: [
|
|
376
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerHeader$3, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
377
|
+
className: "sh:flex sh:w-full sh:items-start sh:justify-between sh:gap-3",
|
|
378
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
379
|
+
className: "sh:flex sh:min-w-0 sh:flex-col sh:gap-1",
|
|
380
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
381
|
+
className: "sh:flex sh:items-center sh:gap-2",
|
|
382
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyRoundIcon$1, { className: "sh:h-5 sh:w-5 sh:shrink-0 sh:text-accent-soft-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
|
|
383
|
+
className: "sh:text-lg sh:font-semibold",
|
|
384
|
+
children: "Admin password"
|
|
385
|
+
})]
|
|
386
|
+
}), device && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
387
|
+
className: "sh:text-sm sh:text-muted",
|
|
388
|
+
children: [
|
|
389
|
+
"Set or change the admin password of ",
|
|
390
|
+
device.name,
|
|
391
|
+
" (",
|
|
392
|
+
device.ipAddress,
|
|
393
|
+
")."
|
|
394
|
+
]
|
|
395
|
+
})]
|
|
396
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$4, {
|
|
397
|
+
isIconOnly: true,
|
|
398
|
+
variant: "ghost",
|
|
399
|
+
"aria-label": "Close",
|
|
400
|
+
onPress: close,
|
|
401
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon$3, { size: 16 })
|
|
402
|
+
})]
|
|
403
|
+
}) }),
|
|
404
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerBody$3, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Form$2, {
|
|
405
|
+
onSubmit,
|
|
406
|
+
className: "sh:flex sh:flex-col sh:gap-4",
|
|
407
|
+
children: [
|
|
408
|
+
device?.authState === "required" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordFieldRow, {
|
|
409
|
+
label: "Current password",
|
|
410
|
+
value: currentPassword,
|
|
411
|
+
onChange: setCurrentPassword,
|
|
412
|
+
description: "Required because this device already has authentication enabled.",
|
|
413
|
+
autoComplete: "current-password",
|
|
414
|
+
dataCy: "shelly-auth-current-password"
|
|
415
|
+
}),
|
|
416
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordFieldRow, {
|
|
417
|
+
label: "New admin password",
|
|
418
|
+
value: password,
|
|
419
|
+
onChange: setPassword,
|
|
420
|
+
description: "Protects the device's local web interface and API.",
|
|
421
|
+
required: true,
|
|
422
|
+
autoComplete: "new-password",
|
|
423
|
+
dataCy: "shelly-auth-password"
|
|
424
|
+
}),
|
|
425
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
|
|
426
|
+
status: "danger",
|
|
427
|
+
title: "Could not set password",
|
|
428
|
+
dataCy: "shelly-auth-error",
|
|
429
|
+
children: error
|
|
430
|
+
}),
|
|
431
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
432
|
+
type: "submit",
|
|
433
|
+
hidden: true
|
|
434
|
+
})
|
|
435
|
+
]
|
|
436
|
+
}) }),
|
|
437
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DrawerFooter$2, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$4, {
|
|
438
|
+
variant: "secondary",
|
|
439
|
+
onPress: close,
|
|
440
|
+
children: "Cancel"
|
|
441
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$4, {
|
|
442
|
+
variant: "primary",
|
|
443
|
+
isPending: submitting,
|
|
444
|
+
onPress: () => void submit(),
|
|
445
|
+
"data-cy": "shelly-auth-submit",
|
|
446
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyRoundIcon$1, { className: "sh:h-4 sh:w-4" }), " Save password"]
|
|
447
|
+
})] })
|
|
448
|
+
]
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
//#endregion
|
|
452
|
+
//#region frontend/src/DeviceInfoDrawer.tsx
|
|
453
|
+
var { Button: Button$3, Card, DrawerBody: DrawerBody$2, DrawerFooter: DrawerFooter$1, DrawerHeader: DrawerHeader$2, Form: Form$1, Skeleton } = await importShared("@heroui/react");
|
|
454
|
+
var { EyeIcon, EyeOffIcon, InfoIcon: InfoIcon$1, RefreshCwIcon: RefreshCwIcon$2, XIcon: XIcon$2 } = await importShared("lucide-react");
|
|
455
|
+
var { useCallback: useCallback$3, useEffect: useEffect$2, useState: useState$3 } = await importShared("react");
|
|
456
|
+
function generationLabel$1(generation) {
|
|
457
|
+
if (generation === null) return "Unknown";
|
|
458
|
+
return generation === 1 ? "Gen 1" : `Gen ${generation}+`;
|
|
459
|
+
}
|
|
460
|
+
function isRecord(value) {
|
|
461
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
462
|
+
}
|
|
463
|
+
function readPath(source, path) {
|
|
464
|
+
return path.split(".").reduce((value, key) => {
|
|
465
|
+
if (Array.isArray(value)) return value[Number(key)];
|
|
466
|
+
return isRecord(value) ? value[key] : void 0;
|
|
467
|
+
}, source);
|
|
468
|
+
}
|
|
469
|
+
function firstValue(source, paths) {
|
|
470
|
+
for (const path of paths) {
|
|
471
|
+
const value = readPath(source, path);
|
|
472
|
+
if (value !== void 0 && value !== null && value !== "") return value;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
function formatValue(value, suffix = "") {
|
|
476
|
+
if (value === void 0 || value === null || value === "") return "Not reported";
|
|
477
|
+
if (typeof value === "boolean") return value ? "On" : "Off";
|
|
478
|
+
if (typeof value === "number") return `${Number.isInteger(value) ? value : value.toFixed(1)}${suffix}`;
|
|
479
|
+
return String(value);
|
|
480
|
+
}
|
|
481
|
+
function formatUptime(seconds) {
|
|
482
|
+
if (typeof seconds !== "number") return formatValue(seconds);
|
|
483
|
+
const days = Math.floor(seconds / 86400);
|
|
484
|
+
const hours = Math.floor(seconds % 86400 / 3600);
|
|
485
|
+
const minutes = Math.floor(seconds % 3600 / 60);
|
|
486
|
+
if (days > 0) return `${days}d ${hours}h ${minutes}m`;
|
|
487
|
+
if (hours > 0) return `${hours}h ${minutes}m`;
|
|
488
|
+
return `${minutes}m`;
|
|
489
|
+
}
|
|
490
|
+
function AuthProtectedForm({ authState, currentPassword, onChange, loading, onLoad }) {
|
|
491
|
+
const [visible, setVisible] = useState$3(false);
|
|
492
|
+
if (authState !== "required") return null;
|
|
493
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
494
|
+
className: "sh:rounded-md sh:border-l-4 sh:border-l-warning sh:bg-warning/5 sh:p-4",
|
|
495
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Form$1, {
|
|
496
|
+
onSubmit: (e) => {
|
|
497
|
+
e.preventDefault();
|
|
498
|
+
onLoad();
|
|
499
|
+
},
|
|
500
|
+
className: "sh:flex sh:flex-col sh:gap-3",
|
|
501
|
+
children: [
|
|
502
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
503
|
+
className: "sh:text-sm",
|
|
504
|
+
children: "This device requires authentication. Enter its admin password to read protected info."
|
|
505
|
+
}),
|
|
506
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
507
|
+
className: "sh:relative",
|
|
508
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextFieldRow, {
|
|
509
|
+
label: "Admin password",
|
|
510
|
+
value: currentPassword,
|
|
511
|
+
onChange,
|
|
512
|
+
placeholder: "device admin password",
|
|
513
|
+
dataCy: "shelly-info-current-password"
|
|
514
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$3, {
|
|
515
|
+
isIconOnly: true,
|
|
516
|
+
variant: "ghost",
|
|
517
|
+
size: "sm",
|
|
518
|
+
"aria-label": visible ? "Hide password" : "Show password",
|
|
519
|
+
className: "sh:absolute sh:right-1 sh:top-6",
|
|
520
|
+
onPress: () => setVisible((v) => !v),
|
|
521
|
+
children: visible ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EyeOffIcon, { className: "sh:h-4 sh:w-4" }) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EyeIcon, { className: "sh:h-4 sh:w-4" })
|
|
522
|
+
})]
|
|
523
|
+
}),
|
|
524
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
525
|
+
className: "sh:flex sh:justify-end",
|
|
526
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$3, {
|
|
527
|
+
variant: "primary",
|
|
528
|
+
size: "sm",
|
|
529
|
+
isPending: loading,
|
|
530
|
+
onPress: onLoad,
|
|
531
|
+
"data-cy": "shelly-info-unlock",
|
|
532
|
+
children: "Load info"
|
|
533
|
+
})
|
|
534
|
+
}),
|
|
535
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
536
|
+
type: "submit",
|
|
537
|
+
hidden: true
|
|
538
|
+
})
|
|
539
|
+
]
|
|
540
|
+
})
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
function DeviceInfoCards({ info }) {
|
|
544
|
+
const status = info.status;
|
|
545
|
+
const config = info.config;
|
|
546
|
+
const output = firstValue(status, [
|
|
547
|
+
"switch:0.output",
|
|
548
|
+
"relays.0.ison",
|
|
549
|
+
"lights.0.ison"
|
|
550
|
+
]);
|
|
551
|
+
const power = firstValue(status, [
|
|
552
|
+
"switch:0.apower",
|
|
553
|
+
"meters.0.power",
|
|
554
|
+
"lights.0.power"
|
|
555
|
+
]);
|
|
556
|
+
const voltage = firstValue(status, ["switch:0.voltage", "meters.0.voltage"]);
|
|
557
|
+
const current = firstValue(status, ["switch:0.current", "meters.0.current"]);
|
|
558
|
+
const cards = [
|
|
559
|
+
{
|
|
560
|
+
title: "Device",
|
|
561
|
+
rows: [
|
|
562
|
+
{
|
|
563
|
+
label: "Name",
|
|
564
|
+
value: formatValue(firstValue(config, [
|
|
565
|
+
"sys.device.name",
|
|
566
|
+
"name",
|
|
567
|
+
"device.name"
|
|
568
|
+
]))
|
|
569
|
+
},
|
|
570
|
+
{
|
|
571
|
+
label: "Generation",
|
|
572
|
+
value: generationLabel$1(info.generation)
|
|
573
|
+
},
|
|
574
|
+
{
|
|
575
|
+
label: "Timezone",
|
|
576
|
+
value: formatValue(firstValue(config, ["sys.location.tz", "timezone"]))
|
|
577
|
+
},
|
|
578
|
+
{
|
|
579
|
+
label: "Uptime",
|
|
580
|
+
value: formatUptime(firstValue(status, ["sys.uptime", "uptime"]))
|
|
581
|
+
}
|
|
582
|
+
]
|
|
583
|
+
},
|
|
584
|
+
{
|
|
585
|
+
title: "Network",
|
|
586
|
+
rows: [
|
|
587
|
+
{
|
|
588
|
+
label: "IP address",
|
|
589
|
+
value: formatValue(firstValue(status, [
|
|
590
|
+
"wifi.sta_ip",
|
|
591
|
+
"wifi_sta.ip",
|
|
592
|
+
"sta_ip"
|
|
593
|
+
]))
|
|
594
|
+
},
|
|
595
|
+
{
|
|
596
|
+
label: "Wi-Fi network",
|
|
597
|
+
value: formatValue(firstValue(status, [
|
|
598
|
+
"wifi.ssid",
|
|
599
|
+
"wifi_sta.ssid",
|
|
600
|
+
"ssid"
|
|
601
|
+
]))
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
label: "Signal",
|
|
605
|
+
value: formatValue(firstValue(status, ["wifi.rssi", "wifi_sta.rssi"]), " dBm")
|
|
606
|
+
}
|
|
607
|
+
]
|
|
608
|
+
},
|
|
609
|
+
{
|
|
610
|
+
title: "Output",
|
|
611
|
+
rows: [
|
|
612
|
+
{
|
|
613
|
+
label: "State",
|
|
614
|
+
value: formatValue(output)
|
|
615
|
+
},
|
|
616
|
+
{
|
|
617
|
+
label: "Power",
|
|
618
|
+
value: formatValue(power, " W")
|
|
619
|
+
},
|
|
620
|
+
{
|
|
621
|
+
label: "Voltage",
|
|
622
|
+
value: formatValue(voltage, " V")
|
|
623
|
+
},
|
|
624
|
+
{
|
|
625
|
+
label: "Current",
|
|
626
|
+
value: formatValue(current, " A")
|
|
627
|
+
}
|
|
628
|
+
]
|
|
629
|
+
}
|
|
630
|
+
];
|
|
631
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
632
|
+
className: "sh:grid sh:gap-4",
|
|
633
|
+
children: cards.map((card) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Card, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Card.Header, {
|
|
634
|
+
className: "sh:pb-0",
|
|
635
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
636
|
+
className: "sh:text-sm sh:font-semibold",
|
|
637
|
+
children: card.title
|
|
638
|
+
})
|
|
639
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Card.Content, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dl", {
|
|
640
|
+
className: "sh:grid sh:grid-cols-1 sh:gap-3 sh:sm:grid-cols-2",
|
|
641
|
+
children: card.rows.map((row) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
642
|
+
className: "sh:min-w-0",
|
|
643
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("dt", {
|
|
644
|
+
className: "sh:text-xs sh:font-medium sh:uppercase sh:tracking-wide sh:text-default-500",
|
|
645
|
+
children: row.label
|
|
646
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("dd", {
|
|
647
|
+
className: "sh:mt-1 sh:truncate sh:text-sm sh:text-default-800",
|
|
648
|
+
title: row.value,
|
|
649
|
+
children: row.value
|
|
650
|
+
})]
|
|
651
|
+
}, row.label))
|
|
652
|
+
}) })] }, card.title))
|
|
653
|
+
});
|
|
654
|
+
}
|
|
655
|
+
function DeviceInfoDrawer({ device, onOpenChange }) {
|
|
656
|
+
const [info, setInfo] = useState$3(null);
|
|
657
|
+
const [currentPassword, setCurrentPassword] = useState$3("");
|
|
658
|
+
const [loading, setLoading] = useState$3(false);
|
|
659
|
+
const [error, setError] = useState$3(null);
|
|
660
|
+
useEffect$2(() => {
|
|
661
|
+
if (device) {
|
|
662
|
+
setInfo(null);
|
|
663
|
+
setCurrentPassword("");
|
|
664
|
+
setError(null);
|
|
665
|
+
}
|
|
666
|
+
}, [device]);
|
|
667
|
+
const close = useCallback$3(() => onOpenChange(false), [onOpenChange]);
|
|
668
|
+
const load = useCallback$3(async () => {
|
|
669
|
+
if (!device) return;
|
|
670
|
+
setLoading(true);
|
|
671
|
+
setError(null);
|
|
672
|
+
try {
|
|
673
|
+
setInfo(await getDeviceInfo(device.id, { currentPassword: currentPassword || void 0 }));
|
|
674
|
+
} catch (err) {
|
|
675
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
676
|
+
} finally {
|
|
677
|
+
setLoading(false);
|
|
678
|
+
}
|
|
679
|
+
}, [device, currentPassword]);
|
|
680
|
+
useEffect$2(() => {
|
|
681
|
+
if (device && device.authState !== "required") load();
|
|
682
|
+
}, [device]);
|
|
683
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(StandardDrawer, {
|
|
684
|
+
isOpen: !!device,
|
|
685
|
+
onOpenChange,
|
|
686
|
+
children: [
|
|
687
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerHeader$2, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
688
|
+
className: "sh:flex sh:w-full sh:items-start sh:justify-between sh:gap-3",
|
|
689
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
690
|
+
className: "sh:flex sh:min-w-0 sh:flex-col sh:gap-1",
|
|
691
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
692
|
+
className: "sh:flex sh:items-center sh:gap-2",
|
|
693
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(InfoIcon$1, { className: "sh:h-5 sh:w-5 sh:shrink-0 sh:text-accent-soft-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
|
|
694
|
+
className: "sh:text-lg sh:font-semibold",
|
|
695
|
+
children: device?.name ?? "Device info"
|
|
696
|
+
})]
|
|
697
|
+
}), device && /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
698
|
+
className: "sh:text-sm sh:text-muted",
|
|
699
|
+
children: device.ipAddress
|
|
700
|
+
})]
|
|
701
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$3, {
|
|
702
|
+
isIconOnly: true,
|
|
703
|
+
variant: "ghost",
|
|
704
|
+
"aria-label": "Close",
|
|
705
|
+
onPress: close,
|
|
706
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon$2, { size: 16 })
|
|
707
|
+
})]
|
|
708
|
+
}) }),
|
|
709
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerBody$2, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
710
|
+
className: "sh:flex sh:flex-col sh:gap-4",
|
|
711
|
+
children: [
|
|
712
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthProtectedForm, {
|
|
713
|
+
authState: device?.authState ?? "unknown",
|
|
714
|
+
currentPassword,
|
|
715
|
+
onChange: setCurrentPassword,
|
|
716
|
+
loading,
|
|
717
|
+
onLoad: () => void load()
|
|
718
|
+
}),
|
|
719
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
|
|
720
|
+
status: "danger",
|
|
721
|
+
title: "Could not load device info",
|
|
722
|
+
children: error
|
|
723
|
+
}),
|
|
724
|
+
loading && !info ? /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
725
|
+
className: "sh:flex sh:flex-col sh:gap-4",
|
|
726
|
+
"aria-hidden": "true",
|
|
727
|
+
children: [
|
|
728
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "sh:h-32 sh:w-full sh:rounded-xl" }),
|
|
729
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "sh:h-32 sh:w-full sh:rounded-xl" }),
|
|
730
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Skeleton, { className: "sh:h-32 sh:w-full sh:rounded-xl" })
|
|
731
|
+
]
|
|
732
|
+
}) : info ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeviceInfoCards, { info }) : null
|
|
733
|
+
]
|
|
734
|
+
}) }),
|
|
735
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerFooter$1, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
736
|
+
className: "sh:flex sh:w-full sh:items-center sh:justify-between sh:gap-3",
|
|
737
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
738
|
+
className: "sh:text-xs sh:text-default-500",
|
|
739
|
+
children: info ? `Updated ${new Date(info.fetchedAt).toLocaleTimeString()}` : ""
|
|
740
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$3, {
|
|
741
|
+
variant: "secondary",
|
|
742
|
+
onPress: () => void load(),
|
|
743
|
+
isPending: loading,
|
|
744
|
+
"data-cy": "shelly-info-refresh",
|
|
745
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCwIcon$2, { className: "sh:h-4 sh:w-4" }), " Refresh"]
|
|
746
|
+
})]
|
|
747
|
+
}) })
|
|
748
|
+
]
|
|
749
|
+
});
|
|
750
|
+
}
|
|
751
|
+
//#endregion
|
|
752
|
+
//#region frontend/src/DiscoverDrawer.tsx
|
|
753
|
+
var { Button: Button$2, Chip: Chip$1, DrawerBody: DrawerBody$1, DrawerHeader: DrawerHeader$1, Form, Spinner: Spinner$2 } = await importShared("@heroui/react");
|
|
754
|
+
var { SearchIcon: SearchIcon$1, XIcon: XIcon$1 } = await importShared("lucide-react");
|
|
755
|
+
var { useCallback: useCallback$2, useRef: useRef$1, useState: useState$2 } = await importShared("react");
|
|
756
|
+
function ResultSummary({ result }) {
|
|
757
|
+
const added = result.devices.filter((device) => device.isNew).length;
|
|
758
|
+
const scanned = result.subnets.length > 0 ? result.subnets.join(", ") : "no subnet (mDNS only)";
|
|
759
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
760
|
+
className: "sh:flex sh:flex-col sh:gap-3",
|
|
761
|
+
"data-cy": "shelly-discover-result",
|
|
762
|
+
children: [
|
|
763
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(StatusAlert, {
|
|
764
|
+
status: result.devices.length > 0 ? "success" : "warning",
|
|
765
|
+
title: result.devices.length > 0 ? `Found ${result.devices.length}, added ${added}` : "No devices found",
|
|
766
|
+
children: [
|
|
767
|
+
"Probed ",
|
|
768
|
+
result.probed,
|
|
769
|
+
" address",
|
|
770
|
+
result.probed === 1 ? "" : "es",
|
|
771
|
+
" in ",
|
|
772
|
+
scanned,
|
|
773
|
+
"."
|
|
774
|
+
]
|
|
775
|
+
}),
|
|
776
|
+
result.devices.length === 0 && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
777
|
+
className: "sh:text-sm sh:text-muted",
|
|
778
|
+
children: [
|
|
779
|
+
"Nothing answered ",
|
|
780
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("code", { children: "GET /shelly" }),
|
|
781
|
+
". If Attraccess runs in a container, enter the subnet your devices are on — the container's own network is not your LAN."
|
|
782
|
+
]
|
|
783
|
+
}),
|
|
784
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("ul", {
|
|
785
|
+
className: "sh:flex sh:flex-col sh:gap-2",
|
|
786
|
+
children: result.devices.map((device) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("li", {
|
|
787
|
+
className: "sh:flex sh:flex-wrap sh:items-center sh:justify-between sh:gap-2 sh:rounded-lg sh:bg-surface sh:px-3 sh:py-2",
|
|
788
|
+
"data-cy": `shelly-discovered-${device.deviceId}`,
|
|
789
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
790
|
+
className: "sh:min-w-0",
|
|
791
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
792
|
+
className: "sh:truncate sh:font-medium sh:text-foreground",
|
|
793
|
+
children: device.name
|
|
794
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
795
|
+
className: "sh:text-xs sh:text-muted",
|
|
796
|
+
children: [
|
|
797
|
+
device.ipAddress,
|
|
798
|
+
" · Gen ",
|
|
799
|
+
device.generation,
|
|
800
|
+
" · via ",
|
|
801
|
+
device.source === "mdns" ? "mDNS" : "subnet scan"
|
|
802
|
+
]
|
|
803
|
+
})]
|
|
804
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip$1, {
|
|
805
|
+
variant: "soft",
|
|
806
|
+
color: device.isNew ? "success" : "default",
|
|
807
|
+
children: device.isNew ? "Added" : "Already known"
|
|
808
|
+
})]
|
|
809
|
+
}, device.deviceId))
|
|
810
|
+
})
|
|
811
|
+
]
|
|
812
|
+
});
|
|
813
|
+
}
|
|
814
|
+
function DiscoverDrawer({ isOpen, onOpenChange, onDiscovered }) {
|
|
815
|
+
const [cidr, setCidr] = useState$2("");
|
|
816
|
+
const [running, setRunning] = useState$2(false);
|
|
817
|
+
const [error, setError] = useState$2(null);
|
|
818
|
+
const [result, setResult] = useState$2(null);
|
|
819
|
+
const close = useCallback$2(() => onOpenChange(false), [onOpenChange]);
|
|
820
|
+
const inFlight = useRef$1(false);
|
|
821
|
+
const submit = useCallback$2(async () => {
|
|
822
|
+
if (inFlight.current) return;
|
|
823
|
+
inFlight.current = true;
|
|
824
|
+
setRunning(true);
|
|
825
|
+
setError(null);
|
|
826
|
+
setResult(null);
|
|
827
|
+
try {
|
|
828
|
+
const discovered = await discoverDevices({ cidr: cidr.trim() || void 0 });
|
|
829
|
+
setResult(discovered);
|
|
830
|
+
onDiscovered();
|
|
831
|
+
} catch (err) {
|
|
832
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
833
|
+
} finally {
|
|
834
|
+
inFlight.current = false;
|
|
835
|
+
setRunning(false);
|
|
836
|
+
}
|
|
837
|
+
}, [cidr, onDiscovered]);
|
|
838
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(StandardDrawer, {
|
|
839
|
+
isOpen,
|
|
840
|
+
onOpenChange,
|
|
841
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerHeader$1, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
842
|
+
className: "sh:flex sh:w-full sh:items-start sh:justify-between sh:gap-3",
|
|
843
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
844
|
+
className: "sh:flex sh:flex-col sh:gap-1",
|
|
845
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
|
|
846
|
+
className: "sh:text-lg sh:font-semibold",
|
|
847
|
+
children: "Discover devices"
|
|
848
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
849
|
+
className: "sh:text-sm sh:text-muted",
|
|
850
|
+
children: "Listens for Shelly devices announcing over mDNS, then probes every address of a subnet. Everything found is added to the registry."
|
|
851
|
+
})]
|
|
852
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$2, {
|
|
853
|
+
isIconOnly: true,
|
|
854
|
+
variant: "ghost",
|
|
855
|
+
"aria-label": "Close",
|
|
856
|
+
onPress: close,
|
|
857
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon$1, { size: 16 })
|
|
858
|
+
})]
|
|
859
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerBody$1, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Form, {
|
|
860
|
+
onSubmit: submit,
|
|
861
|
+
className: "sh:flex sh:flex-col sh:gap-4",
|
|
862
|
+
children: [
|
|
863
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TextFieldRow, {
|
|
864
|
+
label: "Subnet to scan (optional)",
|
|
865
|
+
value: cidr,
|
|
866
|
+
onChange: setCidr,
|
|
867
|
+
placeholder: "192.168.1.0/24",
|
|
868
|
+
description: "Leave empty to scan the server's own networks. Private ranges only, /22 at most. Required when Attraccess runs in a container, since its network is not your LAN.",
|
|
869
|
+
dataCy: "shelly-discover-cidr"
|
|
870
|
+
}),
|
|
871
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
|
|
872
|
+
status: "danger",
|
|
873
|
+
title: "Discovery failed",
|
|
874
|
+
dataCy: "shelly-discover-error",
|
|
875
|
+
children: error
|
|
876
|
+
}),
|
|
877
|
+
running && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
878
|
+
className: "sh:flex sh:items-center sh:gap-3 sh:text-sm sh:text-muted",
|
|
879
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner$2, {
|
|
880
|
+
color: "accent",
|
|
881
|
+
size: "sm"
|
|
882
|
+
}), "Probing addresses — a /24 takes a few seconds."]
|
|
883
|
+
}),
|
|
884
|
+
result && !running && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ResultSummary, { result }),
|
|
885
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
886
|
+
className: "sh:flex sh:justify-end sh:gap-2 sh:pt-2",
|
|
887
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$2, {
|
|
888
|
+
variant: "secondary",
|
|
889
|
+
onPress: close,
|
|
890
|
+
children: result ? "Done" : "Cancel"
|
|
891
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$2, {
|
|
892
|
+
variant: "primary",
|
|
893
|
+
type: "submit",
|
|
894
|
+
isPending: running,
|
|
895
|
+
onPress: submit,
|
|
896
|
+
"data-cy": "shelly-discover-submit",
|
|
897
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SearchIcon$1, { className: "sh:h-4 sh:w-4" }), " Start discovery"]
|
|
898
|
+
})]
|
|
899
|
+
}),
|
|
900
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("input", {
|
|
901
|
+
type: "submit",
|
|
902
|
+
hidden: true
|
|
903
|
+
})
|
|
904
|
+
]
|
|
905
|
+
}) })]
|
|
906
|
+
});
|
|
907
|
+
}
|
|
908
|
+
//#endregion
|
|
909
|
+
//#region frontend/src/FirmwareDrawer.tsx
|
|
910
|
+
var { Button: Button$1, DrawerBody, DrawerFooter, DrawerHeader, Spinner: Spinner$1, Tooltip: Tooltip$1 } = await importShared("@heroui/react");
|
|
911
|
+
var { ArrowUpCircleIcon, CpuIcon: CpuIcon$1, DownloadIcon, RefreshCwIcon: RefreshCwIcon$1, XIcon } = await importShared("lucide-react");
|
|
912
|
+
var { useCallback: useCallback$1, useEffect: useEffect$1, useRef, useState: useState$1 } = await importShared("react");
|
|
913
|
+
var POLL_INTERVAL_MS = 5e3;
|
|
914
|
+
var UPDATE_TIMEOUT_MS = 3e5;
|
|
915
|
+
var STAGE_LABEL = {
|
|
916
|
+
stable: "stable",
|
|
917
|
+
beta: "beta"
|
|
918
|
+
};
|
|
919
|
+
/**
|
|
920
|
+
* Gen1 reports versions as `20230913-114150/v1.14.0` — only the tail is useful
|
|
921
|
+
* at a glance, the full string stays in the tooltip.
|
|
922
|
+
*/
|
|
923
|
+
function shortVersion(version) {
|
|
924
|
+
const tail = version.split("/").pop();
|
|
925
|
+
return tail && tail.length > 0 ? tail : version;
|
|
926
|
+
}
|
|
927
|
+
/** Table cell summarising a device's firmware state from the bulk overview. */
|
|
928
|
+
function FirmwareCell({ entry }) {
|
|
929
|
+
if (!entry) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
930
|
+
className: "sh:text-sm sh:text-default-400",
|
|
931
|
+
children: "Checking…"
|
|
932
|
+
});
|
|
933
|
+
if (entry.error || !entry.status) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
934
|
+
className: "sh:block sh:max-w-40 sh:truncate sh:text-sm sh:text-default-400",
|
|
935
|
+
title: entry.error ?? void 0,
|
|
936
|
+
children: "Unavailable"
|
|
937
|
+
});
|
|
938
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
939
|
+
className: "sh:flex sh:flex-col sh:gap-1",
|
|
940
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
941
|
+
className: "sh:block sh:max-w-40 sh:truncate sh:text-sm sh:text-default-700",
|
|
942
|
+
title: entry.status.currentVersion ?? void 0,
|
|
943
|
+
children: entry.status.currentVersion ? shortVersion(entry.status.currentVersion) : "Unknown"
|
|
944
|
+
}), entry.status.hasUpdate && entry.status.available.stable && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
945
|
+
className: "sh:max-w-40 sh:truncate sh:text-xs sh:text-warning-600",
|
|
946
|
+
title: entry.status.available.stable,
|
|
947
|
+
children: [shortVersion(entry.status.available.stable), " available"]
|
|
948
|
+
})]
|
|
949
|
+
});
|
|
950
|
+
}
|
|
951
|
+
/**
|
|
952
|
+
* "Update available" marker next to the device name, so the signal survives the
|
|
953
|
+
* breakpoints where the Firmware column is hidden. Icon-only on purpose: a text
|
|
954
|
+
* chip here widens the Device column enough to push the row actions out of the
|
|
955
|
+
* table's visible width on tablets.
|
|
956
|
+
*/
|
|
957
|
+
function UpdateAvailableIndicator({ entry }) {
|
|
958
|
+
const version = entry?.status?.hasUpdate ? entry.status.available.stable : null;
|
|
959
|
+
if (!version) return null;
|
|
960
|
+
const label = `Firmware update available: ${shortVersion(version)}`;
|
|
961
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip$1, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip$1.Trigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
|
|
962
|
+
variant: "ghost",
|
|
963
|
+
size: "sm",
|
|
964
|
+
isIconOnly: true,
|
|
965
|
+
"aria-label": label,
|
|
966
|
+
className: "sh:h-6 sh:w-6 sh:min-w-6 sh:text-warning",
|
|
967
|
+
"data-cy": "shelly-update-available",
|
|
968
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ArrowUpCircleIcon, { className: "sh:h-4 sh:w-4" })
|
|
969
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip$1.Content, { children: label })] });
|
|
970
|
+
}
|
|
971
|
+
function VersionRow({ label, value }) {
|
|
972
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
973
|
+
className: "sh:flex sh:items-baseline sh:justify-between sh:gap-3 sh:border-b sh:border-default-200 sh:py-2 sh:last:border-b-0",
|
|
974
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
975
|
+
className: "sh:text-xs sh:font-medium sh:uppercase sh:tracking-wide sh:text-default-500",
|
|
976
|
+
children: label
|
|
977
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
978
|
+
className: "sh:min-w-0 sh:truncate sh:text-sm sh:text-default-800",
|
|
979
|
+
title: value,
|
|
980
|
+
children: value
|
|
981
|
+
})]
|
|
982
|
+
});
|
|
983
|
+
}
|
|
984
|
+
function FirmwareDetails({ status }) {
|
|
985
|
+
if (!status) return /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
986
|
+
className: "sh:rounded-xl sh:border sh:border-dashed sh:border-default-300 sh:p-4 sh:text-sm sh:text-default-500",
|
|
987
|
+
children: "No firmware info loaded yet."
|
|
988
|
+
});
|
|
989
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("section", {
|
|
990
|
+
className: "sh:rounded-xl sh:border sh:border-default-200 sh:bg-surface sh:p-4",
|
|
991
|
+
"data-cy": "shelly-firmware-details",
|
|
992
|
+
children: [
|
|
993
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionRow, {
|
|
994
|
+
label: "Installed",
|
|
995
|
+
value: status.currentVersion ?? "Unknown"
|
|
996
|
+
}),
|
|
997
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionRow, {
|
|
998
|
+
label: "Stable channel",
|
|
999
|
+
value: status.available.stable ?? "Up to date"
|
|
1000
|
+
}),
|
|
1001
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionRow, {
|
|
1002
|
+
label: "Beta channel",
|
|
1003
|
+
value: status.available.beta ?? "Nothing newer"
|
|
1004
|
+
}),
|
|
1005
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(VersionRow, {
|
|
1006
|
+
label: "Checked",
|
|
1007
|
+
value: new Date(status.fetchedAt).toLocaleString()
|
|
1008
|
+
})
|
|
1009
|
+
]
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
function FirmwareDrawer({ device, onOpenChange, onUpdated }) {
|
|
1013
|
+
const [status, setStatus] = useState$1(null);
|
|
1014
|
+
const [currentPassword, setCurrentPassword] = useState$1("");
|
|
1015
|
+
const [loading, setLoading] = useState$1(false);
|
|
1016
|
+
const [error, setError] = useState$1(null);
|
|
1017
|
+
const [installing, setInstalling] = useState$1(null);
|
|
1018
|
+
const [installedVersion, setInstalledVersion] = useState$1(null);
|
|
1019
|
+
const targetVersion = useRef(null);
|
|
1020
|
+
const deadline = useRef(0);
|
|
1021
|
+
const close = useCallback$1(() => onOpenChange(false), [onOpenChange]);
|
|
1022
|
+
const fetchStatus = useCallback$1(async () => {
|
|
1023
|
+
if (!device) return null;
|
|
1024
|
+
return getFirmware(device.id, { currentPassword: currentPassword || void 0 });
|
|
1025
|
+
}, [device, currentPassword]);
|
|
1026
|
+
const load = useCallback$1(async () => {
|
|
1027
|
+
if (!device) return;
|
|
1028
|
+
setLoading(true);
|
|
1029
|
+
setError(null);
|
|
1030
|
+
try {
|
|
1031
|
+
setStatus(await fetchStatus());
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1034
|
+
} finally {
|
|
1035
|
+
setLoading(false);
|
|
1036
|
+
}
|
|
1037
|
+
}, [device, fetchStatus]);
|
|
1038
|
+
useEffect$1(() => {
|
|
1039
|
+
if (!device) return;
|
|
1040
|
+
setStatus(null);
|
|
1041
|
+
setInstalling(null);
|
|
1042
|
+
setInstalledVersion(null);
|
|
1043
|
+
setError(null);
|
|
1044
|
+
load();
|
|
1045
|
+
}, [device]);
|
|
1046
|
+
const install = useCallback$1(async (stage) => {
|
|
1047
|
+
if (!device) return;
|
|
1048
|
+
setError(null);
|
|
1049
|
+
setInstalling(stage);
|
|
1050
|
+
targetVersion.current = status?.available[stage] ?? null;
|
|
1051
|
+
deadline.current = Date.now() + UPDATE_TIMEOUT_MS;
|
|
1052
|
+
try {
|
|
1053
|
+
await startFirmwareUpdate(device.id, {
|
|
1054
|
+
stage,
|
|
1055
|
+
currentPassword: currentPassword || void 0
|
|
1056
|
+
});
|
|
1057
|
+
} catch (err) {
|
|
1058
|
+
setInstalling(null);
|
|
1059
|
+
setError(err instanceof Error ? err.message : String(err));
|
|
1060
|
+
}
|
|
1061
|
+
}, [
|
|
1062
|
+
currentPassword,
|
|
1063
|
+
device,
|
|
1064
|
+
status
|
|
1065
|
+
]);
|
|
1066
|
+
useEffect$1(() => {
|
|
1067
|
+
if (!installing || !device) return;
|
|
1068
|
+
let cancelled = false;
|
|
1069
|
+
const poll = async () => {
|
|
1070
|
+
if (cancelled) return;
|
|
1071
|
+
if (Date.now() > deadline.current) {
|
|
1072
|
+
setInstalling(null);
|
|
1073
|
+
setError("The device did not report the expected firmware version within 5 minutes. Check it and re-check the firmware manually.");
|
|
1074
|
+
return;
|
|
1075
|
+
}
|
|
1076
|
+
try {
|
|
1077
|
+
const next = await fetchStatus();
|
|
1078
|
+
if (cancelled || !next) return;
|
|
1079
|
+
setStatus(next);
|
|
1080
|
+
if ((targetVersion.current ? next.currentVersion === targetVersion.current : !next.hasUpdate) && next.state !== "updating" && next.state !== "pending") {
|
|
1081
|
+
setInstalling(null);
|
|
1082
|
+
setInstalledVersion(next.currentVersion);
|
|
1083
|
+
onUpdated();
|
|
1084
|
+
}
|
|
1085
|
+
} catch {}
|
|
1086
|
+
};
|
|
1087
|
+
const timer = setInterval(poll, POLL_INTERVAL_MS);
|
|
1088
|
+
return () => {
|
|
1089
|
+
cancelled = true;
|
|
1090
|
+
clearInterval(timer);
|
|
1091
|
+
};
|
|
1092
|
+
}, [
|
|
1093
|
+
installing,
|
|
1094
|
+
device,
|
|
1095
|
+
fetchStatus,
|
|
1096
|
+
onUpdated
|
|
1097
|
+
]);
|
|
1098
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(StandardDrawer, {
|
|
1099
|
+
isOpen: !!device,
|
|
1100
|
+
onOpenChange,
|
|
1101
|
+
children: [
|
|
1102
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1103
|
+
className: "sh:flex sh:w-full sh:items-start sh:justify-between sh:gap-3",
|
|
1104
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1105
|
+
className: "sh:flex sh:min-w-0 sh:flex-col sh:gap-1",
|
|
1106
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1107
|
+
className: "sh:flex sh:items-center sh:gap-2",
|
|
1108
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CpuIcon$1, { className: "sh:h-5 sh:w-5 sh:shrink-0 sh:text-accent-soft-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("h2", {
|
|
1109
|
+
className: "sh:text-lg sh:font-semibold",
|
|
1110
|
+
children: "Firmware"
|
|
1111
|
+
})]
|
|
1112
|
+
}), device && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", {
|
|
1113
|
+
className: "sh:text-sm sh:text-muted",
|
|
1114
|
+
children: [
|
|
1115
|
+
"Check for and install firmware updates on ",
|
|
1116
|
+
device.name,
|
|
1117
|
+
" (",
|
|
1118
|
+
device.ipAddress,
|
|
1119
|
+
")."
|
|
1120
|
+
]
|
|
1121
|
+
})]
|
|
1122
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button$1, {
|
|
1123
|
+
isIconOnly: true,
|
|
1124
|
+
variant: "ghost",
|
|
1125
|
+
"aria-label": "Close",
|
|
1126
|
+
onPress: close,
|
|
1127
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(XIcon, { size: 16 })
|
|
1128
|
+
})]
|
|
1129
|
+
}) }),
|
|
1130
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DrawerBody, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1131
|
+
className: "sh:flex sh:flex-col sh:gap-4",
|
|
1132
|
+
children: [
|
|
1133
|
+
device?.authState === "required" && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(PasswordFieldRow, {
|
|
1134
|
+
label: "Current password",
|
|
1135
|
+
value: currentPassword,
|
|
1136
|
+
onChange: setCurrentPassword,
|
|
1137
|
+
description: "Required because this device already has authentication enabled.",
|
|
1138
|
+
autoComplete: "current-password",
|
|
1139
|
+
dataCy: "shelly-firmware-current-password"
|
|
1140
|
+
}),
|
|
1141
|
+
error && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
|
|
1142
|
+
status: "danger",
|
|
1143
|
+
title: "Firmware check failed",
|
|
1144
|
+
dataCy: "shelly-firmware-error",
|
|
1145
|
+
children: error
|
|
1146
|
+
}),
|
|
1147
|
+
installing && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
|
|
1148
|
+
status: "accent",
|
|
1149
|
+
title: "Update running",
|
|
1150
|
+
dataCy: "shelly-firmware-progress",
|
|
1151
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("span", {
|
|
1152
|
+
className: "sh:flex sh:items-center sh:gap-2",
|
|
1153
|
+
children: [
|
|
1154
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner$1, {
|
|
1155
|
+
size: "sm",
|
|
1156
|
+
color: "accent"
|
|
1157
|
+
}),
|
|
1158
|
+
"Installing the ",
|
|
1159
|
+
STAGE_LABEL[installing],
|
|
1160
|
+
" firmware",
|
|
1161
|
+
targetVersion.current ? ` (${targetVersion.current})` : "",
|
|
1162
|
+
". The device reboots during the update and is offline for a moment — this page keeps checking."
|
|
1163
|
+
]
|
|
1164
|
+
})
|
|
1165
|
+
}),
|
|
1166
|
+
installedVersion && !installing && /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(StatusAlert, {
|
|
1167
|
+
status: "success",
|
|
1168
|
+
title: "Update finished",
|
|
1169
|
+
dataCy: "shelly-firmware-success",
|
|
1170
|
+
children: [
|
|
1171
|
+
"The device now runs ",
|
|
1172
|
+
installedVersion,
|
|
1173
|
+
"."
|
|
1174
|
+
]
|
|
1175
|
+
}),
|
|
1176
|
+
loading && !status ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1177
|
+
className: "sh:flex sh:items-center sh:justify-center sh:p-6",
|
|
1178
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner$1, { color: "accent" })
|
|
1179
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FirmwareDetails, { status })
|
|
1180
|
+
]
|
|
1181
|
+
}) }),
|
|
1182
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DrawerFooter, {
|
|
1183
|
+
className: "sh:flex-wrap",
|
|
1184
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
|
|
1185
|
+
variant: "secondary",
|
|
1186
|
+
onPress: load,
|
|
1187
|
+
isPending: loading,
|
|
1188
|
+
isDisabled: !!installing,
|
|
1189
|
+
"data-cy": "shelly-firmware-refresh",
|
|
1190
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCwIcon$1, { className: "sh:h-4 sh:w-4" }), " Check again"]
|
|
1191
|
+
}), ["stable", "beta"].map((stage) => {
|
|
1192
|
+
const version = status?.available[stage];
|
|
1193
|
+
if (!version) return null;
|
|
1194
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button$1, {
|
|
1195
|
+
variant: stage === "stable" ? "primary" : "secondary",
|
|
1196
|
+
onPress: () => install(stage),
|
|
1197
|
+
isPending: installing === stage,
|
|
1198
|
+
isDisabled: !!installing,
|
|
1199
|
+
"data-cy": `shelly-firmware-install-${stage}`,
|
|
1200
|
+
"aria-label": `Install ${STAGE_LABEL[stage]} firmware ${version}`,
|
|
1201
|
+
children: [
|
|
1202
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DownloadIcon, { className: "sh:h-4 sh:w-4" }),
|
|
1203
|
+
" Install ",
|
|
1204
|
+
STAGE_LABEL[stage],
|
|
1205
|
+
" ",
|
|
1206
|
+
shortVersion(version)
|
|
1207
|
+
]
|
|
1208
|
+
}, stage);
|
|
1209
|
+
})]
|
|
1210
|
+
})
|
|
1211
|
+
]
|
|
1212
|
+
});
|
|
1213
|
+
}
|
|
1214
|
+
//#endregion
|
|
1215
|
+
//#region frontend/src/DevicesPage.tsx
|
|
1216
|
+
var { Button, Chip, Dropdown, DropdownItem, DropdownMenu, DropdownPopover, DropdownTrigger, Modal, ModalBackdrop, ModalBody, ModalContainer, ModalDialog, ModalFooter, ModalHeader, ModalHeading, Spinner, Table, TableBody, TableCell, TableColumn, TableContent, TableHeader, TableRow, TableScrollContainer, Tooltip, useOverlayState } = await importShared("@heroui/react");
|
|
1217
|
+
var { CpuIcon, InfoIcon, KeyRoundIcon, MehIcon, MoreVerticalIcon, PlusIcon, RefreshCwIcon, SearchIcon, Trash2Icon, TriangleAlertIcon, WifiIcon: WifiIcon$1 } = await importShared("lucide-react");
|
|
1218
|
+
var { useCallback, useEffect, useState } = await importShared("react");
|
|
1219
|
+
function generationLabel(generation) {
|
|
1220
|
+
if (generation === null) return "Unknown";
|
|
1221
|
+
return generation === 1 ? "Gen 1" : `Gen ${generation}+`;
|
|
1222
|
+
}
|
|
1223
|
+
function AuthChip({ state }) {
|
|
1224
|
+
const { color, label } = {
|
|
1225
|
+
none: {
|
|
1226
|
+
color: "success",
|
|
1227
|
+
label: "No auth"
|
|
1228
|
+
},
|
|
1229
|
+
required: {
|
|
1230
|
+
color: "warning",
|
|
1231
|
+
label: "Auth required"
|
|
1232
|
+
},
|
|
1233
|
+
unknown: {
|
|
1234
|
+
color: "default",
|
|
1235
|
+
label: "Unknown"
|
|
1236
|
+
}
|
|
1237
|
+
}[state];
|
|
1238
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
|
|
1239
|
+
variant: "soft",
|
|
1240
|
+
color,
|
|
1241
|
+
size: "sm",
|
|
1242
|
+
className: "sh:whitespace-nowrap",
|
|
1243
|
+
children: label
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
function ProbeErrorIndicator({ message }) {
|
|
1247
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip.Trigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
1248
|
+
variant: "ghost",
|
|
1249
|
+
size: "sm",
|
|
1250
|
+
isIconOnly: true,
|
|
1251
|
+
"aria-label": `Probe failed: ${message}`,
|
|
1252
|
+
className: "sh:h-6 sh:w-6 sh:min-w-6 sh:text-warning",
|
|
1253
|
+
"data-cy": "shelly-device-probe-error",
|
|
1254
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TriangleAlertIcon, { className: "sh:h-4 sh:w-4" })
|
|
1255
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip.Content, { children: ["Probe failed: ", message] })] });
|
|
1256
|
+
}
|
|
1257
|
+
function RowActions({ deviceId, isBusy, onInfo, onFirmware, onAuth, onReprobe, onDelete }) {
|
|
1258
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1259
|
+
className: "sh:flex sh:flex-row sh:items-center sh:justify-end sh:gap-1 sh:whitespace-nowrap",
|
|
1260
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Tooltip, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip.Trigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
1261
|
+
variant: "ghost",
|
|
1262
|
+
size: "sm",
|
|
1263
|
+
isIconOnly: true,
|
|
1264
|
+
"aria-label": "View device info",
|
|
1265
|
+
isDisabled: isBusy,
|
|
1266
|
+
onPress: onInfo,
|
|
1267
|
+
"data-cy": `shelly-device-info-${deviceId}`,
|
|
1268
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(InfoIcon, { className: "sh:h-4 sh:w-4" })
|
|
1269
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Tooltip.Content, { children: "View device info" })] }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Dropdown, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownTrigger, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
1270
|
+
variant: "ghost",
|
|
1271
|
+
size: "sm",
|
|
1272
|
+
isIconOnly: true,
|
|
1273
|
+
"aria-label": "More actions",
|
|
1274
|
+
isPending: isBusy,
|
|
1275
|
+
"data-cy": `shelly-device-menu-${deviceId}`,
|
|
1276
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(MoreVerticalIcon, { className: "sh:h-4 sh:w-4" })
|
|
1277
|
+
}) }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DropdownPopover, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownMenu, {
|
|
1278
|
+
"aria-label": "Device actions",
|
|
1279
|
+
children: [
|
|
1280
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownItem, {
|
|
1281
|
+
id: "firmware",
|
|
1282
|
+
onPress: onFirmware,
|
|
1283
|
+
"data-cy": `shelly-device-firmware-${deviceId}`,
|
|
1284
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(CpuIcon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }), " Manage firmware"]
|
|
1285
|
+
}),
|
|
1286
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownItem, {
|
|
1287
|
+
id: "auth",
|
|
1288
|
+
onPress: onAuth,
|
|
1289
|
+
"data-cy": `shelly-device-auth-${deviceId}`,
|
|
1290
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(KeyRoundIcon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }), " Set admin password"]
|
|
1291
|
+
}),
|
|
1292
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownItem, {
|
|
1293
|
+
id: "reprobe",
|
|
1294
|
+
onPress: onReprobe,
|
|
1295
|
+
"data-cy": `shelly-device-reprobe-${deviceId}`,
|
|
1296
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(RefreshCwIcon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }), " Re-probe device"]
|
|
1297
|
+
}),
|
|
1298
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(DropdownItem, {
|
|
1299
|
+
id: "delete",
|
|
1300
|
+
onPress: onDelete,
|
|
1301
|
+
className: "sh:text-danger",
|
|
1302
|
+
"data-cy": `shelly-device-delete-${deviceId}`,
|
|
1303
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2Icon, { className: "sh:mr-2 sh:inline sh:h-4 sh:w-4" }), " Delete device"]
|
|
1304
|
+
})
|
|
1305
|
+
]
|
|
1306
|
+
}) })] })]
|
|
1307
|
+
});
|
|
1308
|
+
}
|
|
1309
|
+
function EmptyDevices({ onAdd }) {
|
|
1310
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1311
|
+
className: "sh:flex sh:flex-col sh:items-center sh:justify-center sh:gap-3 sh:px-4 sh:py-12",
|
|
1312
|
+
children: [
|
|
1313
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(MehIcon, {
|
|
1314
|
+
size: 36,
|
|
1315
|
+
className: "sh:text-default-300"
|
|
1316
|
+
}),
|
|
1317
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
1318
|
+
className: "sh:text-sm sh:text-default-500",
|
|
1319
|
+
children: "No devices yet. Run discovery, or add your first Shelly by its IP."
|
|
1320
|
+
}),
|
|
1321
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
1322
|
+
variant: "secondary",
|
|
1323
|
+
size: "sm",
|
|
1324
|
+
onPress: onAdd,
|
|
1325
|
+
"data-cy": "shelly-add-open-empty",
|
|
1326
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlusIcon, { className: "sh:h-4 sh:w-4" }), " Add device"]
|
|
1327
|
+
})
|
|
1328
|
+
]
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
function DevicesPage() {
|
|
1332
|
+
const [devices, setDevices] = useState([]);
|
|
1333
|
+
const [loading, setLoading] = useState(true);
|
|
1334
|
+
const [pageError, setPageError] = useState(null);
|
|
1335
|
+
const [rowBusyId, setRowBusyId] = useState(null);
|
|
1336
|
+
const [infoDevice, setInfoDevice] = useState(null);
|
|
1337
|
+
const [authDevice, setAuthDevice] = useState(null);
|
|
1338
|
+
const [firmwareDevice, setFirmwareDevice] = useState(null);
|
|
1339
|
+
const [firmware, setFirmware] = useState({});
|
|
1340
|
+
const [deleteTarget, setDeleteTarget] = useState(null);
|
|
1341
|
+
const [deleting, setDeleting] = useState(false);
|
|
1342
|
+
const addDrawer = useOverlayState();
|
|
1343
|
+
const discoverDrawer = useOverlayState();
|
|
1344
|
+
const refreshFirmware = useCallback(async (known = []) => {
|
|
1345
|
+
try {
|
|
1346
|
+
const entries = await listFirmware();
|
|
1347
|
+
setFirmware(Object.fromEntries(entries.map((entry) => [entry.deviceId, entry])));
|
|
1348
|
+
} catch (err) {
|
|
1349
|
+
const error = err instanceof Error ? err.message : String(err);
|
|
1350
|
+
setFirmware(Object.fromEntries(known.map((device) => [device.id, {
|
|
1351
|
+
deviceId: device.id,
|
|
1352
|
+
status: null,
|
|
1353
|
+
error
|
|
1354
|
+
}])));
|
|
1355
|
+
}
|
|
1356
|
+
}, []);
|
|
1357
|
+
const refresh = useCallback(async () => {
|
|
1358
|
+
try {
|
|
1359
|
+
const next = await listDevices();
|
|
1360
|
+
setDevices(next);
|
|
1361
|
+
setPageError(null);
|
|
1362
|
+
refreshFirmware(next);
|
|
1363
|
+
} catch (err) {
|
|
1364
|
+
setPageError(err instanceof Error ? err.message : String(err));
|
|
1365
|
+
} finally {
|
|
1366
|
+
setLoading(false);
|
|
1367
|
+
}
|
|
1368
|
+
}, [refreshFirmware]);
|
|
1369
|
+
useEffect(() => {
|
|
1370
|
+
refresh();
|
|
1371
|
+
}, [refresh]);
|
|
1372
|
+
const withRowBusy = useCallback(async (id, action) => {
|
|
1373
|
+
setRowBusyId(id);
|
|
1374
|
+
try {
|
|
1375
|
+
await action();
|
|
1376
|
+
await refresh();
|
|
1377
|
+
} catch (err) {
|
|
1378
|
+
setPageError(err instanceof Error ? err.message : String(err));
|
|
1379
|
+
} finally {
|
|
1380
|
+
setRowBusyId(null);
|
|
1381
|
+
}
|
|
1382
|
+
}, [refresh]);
|
|
1383
|
+
const confirmDelete = useCallback(async () => {
|
|
1384
|
+
if (!deleteTarget) return;
|
|
1385
|
+
setDeleting(true);
|
|
1386
|
+
try {
|
|
1387
|
+
await deleteDevice(deleteTarget.id);
|
|
1388
|
+
await refresh();
|
|
1389
|
+
setDeleteTarget(null);
|
|
1390
|
+
} catch (err) {
|
|
1391
|
+
setPageError(err instanceof Error ? err.message : String(err));
|
|
1392
|
+
setDeleteTarget(null);
|
|
1393
|
+
} finally {
|
|
1394
|
+
setDeleting(false);
|
|
1395
|
+
}
|
|
1396
|
+
}, [deleteTarget, refresh]);
|
|
1397
|
+
return /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1398
|
+
className: "sh:mx-auto sh:flex sh:w-full sh:max-w-5xl sh:flex-col sh:gap-6 sh:p-4 sh:md:p-6",
|
|
1399
|
+
children: [
|
|
1400
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1401
|
+
className: "sh:flex sh:w-full sh:flex-wrap sh:items-center sh:justify-between sh:gap-y-4",
|
|
1402
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1403
|
+
className: "sh:flex sh:items-center sh:gap-3",
|
|
1404
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(WifiIcon$1, { className: "sh:h-6 sh:w-6 sh:text-accent-soft-foreground" }), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("h1", {
|
|
1405
|
+
className: "sh:text-2xl sh:font-bold",
|
|
1406
|
+
children: "Shelly Devices"
|
|
1407
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("p", {
|
|
1408
|
+
className: "sh:mt-1 sh:text-sm sh:text-muted",
|
|
1409
|
+
children: "Discovered and manually added Shelly devices."
|
|
1410
|
+
})] })]
|
|
1411
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1412
|
+
className: "sh:flex sh:flex-wrap sh:gap-2",
|
|
1413
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
1414
|
+
variant: "secondary",
|
|
1415
|
+
onPress: discoverDrawer.open,
|
|
1416
|
+
"data-cy": "shelly-discover-open",
|
|
1417
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(SearchIcon, { className: "sh:h-4 sh:w-4" }), " Discover"]
|
|
1418
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
1419
|
+
variant: "primary",
|
|
1420
|
+
onPress: addDrawer.open,
|
|
1421
|
+
"data-cy": "shelly-add-open",
|
|
1422
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(PlusIcon, { className: "sh:h-4 sh:w-4" }), " Add device"]
|
|
1423
|
+
})]
|
|
1424
|
+
})]
|
|
1425
|
+
}),
|
|
1426
|
+
pageError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(StatusAlert, {
|
|
1427
|
+
status: "danger",
|
|
1428
|
+
title: "Failed to load devices",
|
|
1429
|
+
children: pageError
|
|
1430
|
+
}),
|
|
1431
|
+
loading ? /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1432
|
+
className: "sh:flex sh:items-center sh:justify-center sh:p-6",
|
|
1433
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Spinner, { color: "accent" })
|
|
1434
|
+
}) : /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Table, {
|
|
1435
|
+
"data-cy": "shelly-device-table",
|
|
1436
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableScrollContainer, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableContent, {
|
|
1437
|
+
"aria-label": "Shelly devices",
|
|
1438
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableHeader, { children: [
|
|
1439
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
|
|
1440
|
+
isRowHeader: true,
|
|
1441
|
+
children: "Device"
|
|
1442
|
+
}),
|
|
1443
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
|
|
1444
|
+
className: "sh:hidden sh:sm:table-cell sh:md:hidden sh:lg:table-cell",
|
|
1445
|
+
children: "Address"
|
|
1446
|
+
}),
|
|
1447
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
|
|
1448
|
+
className: "sh:hidden sh:lg:table-cell",
|
|
1449
|
+
children: "Model"
|
|
1450
|
+
}),
|
|
1451
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
|
|
1452
|
+
className: "sh:hidden sh:sm:table-cell",
|
|
1453
|
+
children: "Auth"
|
|
1454
|
+
}),
|
|
1455
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
|
|
1456
|
+
className: "sh:hidden sh:xl:table-cell",
|
|
1457
|
+
children: "Firmware"
|
|
1458
|
+
}),
|
|
1459
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableColumn, {
|
|
1460
|
+
className: "sh:text-end",
|
|
1461
|
+
children: "Actions"
|
|
1462
|
+
})
|
|
1463
|
+
] }), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableBody, {
|
|
1464
|
+
items: devices,
|
|
1465
|
+
dependencies: [firmware, rowBusyId],
|
|
1466
|
+
renderEmptyState: () => /* @__PURE__ */ (0, import_jsx_runtime.jsx)(EmptyDevices, { onAdd: addDrawer.open }),
|
|
1467
|
+
children: (device) => /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableRow, {
|
|
1468
|
+
id: device.id,
|
|
1469
|
+
"data-cy": `shelly-device-row-${device.id}`,
|
|
1470
|
+
children: [
|
|
1471
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(TableCell, {
|
|
1472
|
+
className: "sh:whitespace-nowrap",
|
|
1473
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1474
|
+
className: "sh:flex sh:items-center sh:gap-1",
|
|
1475
|
+
children: [
|
|
1476
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1477
|
+
className: "sh:max-w-36 sh:truncate sh:font-medium sh:text-default-800 sh:sm:max-w-48",
|
|
1478
|
+
title: device.name,
|
|
1479
|
+
children: device.name
|
|
1480
|
+
}),
|
|
1481
|
+
device.lastProbeError && /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ProbeErrorIndicator, { message: device.lastProbeError }),
|
|
1482
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(UpdateAvailableIndicator, { entry: firmware[device.id] })
|
|
1483
|
+
]
|
|
1484
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)("div", {
|
|
1485
|
+
className: "sh:text-xs sh:text-default-500 sh:sm:hidden sh:md:block sh:lg:hidden",
|
|
1486
|
+
children: device.ipAddress
|
|
1487
|
+
})]
|
|
1488
|
+
}),
|
|
1489
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
|
|
1490
|
+
className: "sh:hidden sh:whitespace-nowrap sh:text-default-600 sh:sm:table-cell sh:md:hidden sh:lg:table-cell",
|
|
1491
|
+
children: device.ipAddress
|
|
1492
|
+
}),
|
|
1493
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
|
|
1494
|
+
className: "sh:hidden sh:whitespace-nowrap sh:lg:table-cell",
|
|
1495
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("div", {
|
|
1496
|
+
className: "sh:flex sh:items-center sh:gap-1.5",
|
|
1497
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1498
|
+
className: "sh:max-w-36 sh:truncate",
|
|
1499
|
+
title: device.model ?? void 0,
|
|
1500
|
+
children: device.model ?? "—"
|
|
1501
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsx)(Chip, {
|
|
1502
|
+
variant: "soft",
|
|
1503
|
+
size: "sm",
|
|
1504
|
+
className: "sh:whitespace-nowrap",
|
|
1505
|
+
children: generationLabel(device.generation)
|
|
1506
|
+
})]
|
|
1507
|
+
})
|
|
1508
|
+
}),
|
|
1509
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
|
|
1510
|
+
className: "sh:hidden sh:whitespace-nowrap sh:sm:table-cell",
|
|
1511
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(AuthChip, { state: device.authState })
|
|
1512
|
+
}),
|
|
1513
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
|
|
1514
|
+
className: "sh:hidden sh:whitespace-nowrap sh:xl:table-cell",
|
|
1515
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(FirmwareCell, { entry: firmware[device.id] })
|
|
1516
|
+
}),
|
|
1517
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(TableCell, {
|
|
1518
|
+
className: "sh:whitespace-nowrap",
|
|
1519
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(RowActions, {
|
|
1520
|
+
deviceId: device.id,
|
|
1521
|
+
isBusy: rowBusyId === device.id,
|
|
1522
|
+
onInfo: () => setInfoDevice(device),
|
|
1523
|
+
onFirmware: () => setFirmwareDevice(device),
|
|
1524
|
+
onAuth: () => setAuthDevice(device),
|
|
1525
|
+
onReprobe: () => withRowBusy(device.id, () => reprobeDevice(device.id)),
|
|
1526
|
+
onDelete: () => setDeleteTarget(device)
|
|
1527
|
+
})
|
|
1528
|
+
})
|
|
1529
|
+
]
|
|
1530
|
+
}, device.id)
|
|
1531
|
+
})]
|
|
1532
|
+
}) })
|
|
1533
|
+
}),
|
|
1534
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AddDeviceDrawer, {
|
|
1535
|
+
isOpen: addDrawer.isOpen,
|
|
1536
|
+
onOpenChange: addDrawer.setOpen,
|
|
1537
|
+
onAdded: refresh
|
|
1538
|
+
}),
|
|
1539
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DiscoverDrawer, {
|
|
1540
|
+
isOpen: discoverDrawer.isOpen,
|
|
1541
|
+
onOpenChange: discoverDrawer.setOpen,
|
|
1542
|
+
onDiscovered: refresh
|
|
1543
|
+
}),
|
|
1544
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(DeviceInfoDrawer, {
|
|
1545
|
+
device: infoDevice,
|
|
1546
|
+
onOpenChange: (open) => !open && setInfoDevice(null)
|
|
1547
|
+
}),
|
|
1548
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(AdminPasswordDrawer, {
|
|
1549
|
+
device: authDevice,
|
|
1550
|
+
onOpenChange: (open) => !open && setAuthDevice(null),
|
|
1551
|
+
onSaved: refresh
|
|
1552
|
+
}),
|
|
1553
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(FirmwareDrawer, {
|
|
1554
|
+
device: firmwareDevice,
|
|
1555
|
+
onOpenChange: (open) => !open && setFirmwareDevice(null),
|
|
1556
|
+
onUpdated: () => void refreshFirmware(devices)
|
|
1557
|
+
}),
|
|
1558
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Modal, {
|
|
1559
|
+
isOpen: !!deleteTarget,
|
|
1560
|
+
onOpenChange: (open) => {
|
|
1561
|
+
if (!open) setDeleteTarget(null);
|
|
1562
|
+
},
|
|
1563
|
+
"data-cy": "shelly-delete-confirmation-modal",
|
|
1564
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalBackdrop, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalContainer, {
|
|
1565
|
+
size: "sm",
|
|
1566
|
+
children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalDialog, { children: [
|
|
1567
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHeader, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalHeading, { children: "Delete device" }) }),
|
|
1568
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)(ModalBody, { children: /* @__PURE__ */ (0, import_jsx_runtime.jsxs)("p", { children: [
|
|
1569
|
+
"Remove ",
|
|
1570
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsx)("span", {
|
|
1571
|
+
className: "sh:font-semibold",
|
|
1572
|
+
children: deleteTarget?.name
|
|
1573
|
+
}),
|
|
1574
|
+
" (",
|
|
1575
|
+
deleteTarget?.ipAddress,
|
|
1576
|
+
") from the registry? The device itself is not changed."
|
|
1577
|
+
] }) }),
|
|
1578
|
+
/* @__PURE__ */ (0, import_jsx_runtime.jsxs)(ModalFooter, { children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Button, {
|
|
1579
|
+
variant: "secondary",
|
|
1580
|
+
onPress: () => setDeleteTarget(null),
|
|
1581
|
+
"data-cy": "shelly-delete-cancel",
|
|
1582
|
+
children: "Cancel"
|
|
1583
|
+
}), /* @__PURE__ */ (0, import_jsx_runtime.jsxs)(Button, {
|
|
1584
|
+
variant: "danger",
|
|
1585
|
+
onPress: confirmDelete,
|
|
1586
|
+
isPending: deleting,
|
|
1587
|
+
"data-cy": "shelly-delete-confirm",
|
|
1588
|
+
children: [/* @__PURE__ */ (0, import_jsx_runtime.jsx)(Trash2Icon, { className: "sh:h-4 sh:w-4" }), " Delete"]
|
|
1589
|
+
})] })
|
|
1590
|
+
] })
|
|
1591
|
+
}) })
|
|
1592
|
+
})
|
|
1593
|
+
]
|
|
1594
|
+
});
|
|
1595
|
+
}
|
|
1596
|
+
//#endregion
|
|
1597
|
+
//#region frontend/src/plugin.tsx
|
|
1598
|
+
var { WifiIcon } = await importShared("lucide-react");
|
|
1599
|
+
var ShellyPlugin = class {
|
|
1600
|
+
getPluginName() {
|
|
1601
|
+
return "shelly-plugin@0.1.0";
|
|
1602
|
+
}
|
|
1603
|
+
getDependencies() {
|
|
1604
|
+
return [];
|
|
1605
|
+
}
|
|
1606
|
+
init(_store) {}
|
|
1607
|
+
activate() {}
|
|
1608
|
+
deactivate() {}
|
|
1609
|
+
onApiAuthStateChange(_authData) {}
|
|
1610
|
+
onApiEndpointChange(_endpoint) {}
|
|
1611
|
+
getRoutes() {
|
|
1612
|
+
return [{
|
|
1613
|
+
path: "/shelly",
|
|
1614
|
+
authRequired: "resources.update",
|
|
1615
|
+
element: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(DevicesPage, {})
|
|
1616
|
+
}];
|
|
1617
|
+
}
|
|
1618
|
+
getSidebarItems() {
|
|
1619
|
+
return [{
|
|
1620
|
+
label: "Shelly",
|
|
1621
|
+
path: "/shelly",
|
|
1622
|
+
icon: /* @__PURE__ */ (0, import_jsx_runtime.jsx)(WifiIcon, { className: "sh:w-5 sh:h-5" })
|
|
1623
|
+
}];
|
|
1624
|
+
}
|
|
1625
|
+
};
|
|
1626
|
+
//#endregion
|
|
1627
|
+
export { ShellyPlugin as default };
|