@tomorrowos/sdk 0.2.4 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -0
- package/dist/pairing-code.d.ts +8 -0
- package/dist/pairing-code.d.ts.map +1 -0
- package/dist/pairing-code.js +23 -0
- package/dist/playlist-catalog.d.ts +35 -0
- package/dist/playlist-catalog.d.ts.map +1 -0
- package/dist/playlist-catalog.js +133 -0
- package/dist/store/memory-store.d.ts +17 -1
- package/dist/store/memory-store.d.ts.map +1 -1
- package/dist/store/memory-store.js +57 -0
- package/dist/store/types.d.ts +55 -0
- package/dist/store/types.d.ts.map +1 -1
- package/dist/tomorrowos.d.ts +19 -1
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +237 -38
- package/package.json +1 -1
- package/templates/cms-starter/policy.example.json +30 -30
- package/templates/cms-starter/public/index.html +43 -15
- package/templates/cms-starter/public/methods.js +455 -368
- package/templates/cms-starter/public/panel.css +458 -329
- package/templates/cms-starter/public/uploads/.gitkeep +1 -0
- package/templates/cms-starter/server.ts +7 -0
|
@@ -1,13 +1,20 @@
|
|
|
1
|
-
const PANEL_PLAYLIST_KEY = "tomorrowos.panel.playlistDraft";
|
|
2
|
-
const PANEL_SCHEDULE_KEY = "tomorrowos.panel.scheduleDraft";
|
|
3
1
|
const PANEL_MEDIA_BASE_KEY = "tomorrowos.panel.mediaBaseUrl";
|
|
4
2
|
|
|
5
|
-
/** @type {
|
|
6
|
-
let
|
|
3
|
+
/** @type {Array<Record<string, unknown>>} */
|
|
4
|
+
let playlistsCatalog = [];
|
|
7
5
|
|
|
8
6
|
/** @type {Array<Record<string, unknown>>} */
|
|
9
7
|
let devicesCache = [];
|
|
10
8
|
|
|
9
|
+
/** @type {string|null} */
|
|
10
|
+
let selectedPlaylistId = null;
|
|
11
|
+
|
|
12
|
+
/** @type {{ id: string, url: string, name: string, type: string, durationMs: number }[]} */
|
|
13
|
+
let editorItems = [];
|
|
14
|
+
|
|
15
|
+
/** @type {string|null} */
|
|
16
|
+
let publishModalDeviceId = null;
|
|
17
|
+
|
|
11
18
|
let devicePollTimer = null;
|
|
12
19
|
|
|
13
20
|
function escapeHtml(value) {
|
|
@@ -37,17 +44,9 @@ function formatDateTimeSeconds(iso) {
|
|
|
37
44
|
if (!iso) return "—";
|
|
38
45
|
const d = new Date(iso);
|
|
39
46
|
if (Number.isNaN(d.getTime())) return "—";
|
|
40
|
-
return d.toLocaleString(
|
|
41
|
-
year: "numeric",
|
|
42
|
-
month: "short",
|
|
43
|
-
day: "numeric",
|
|
44
|
-
hour: "2-digit",
|
|
45
|
-
minute: "2-digit",
|
|
46
|
-
second: "2-digit"
|
|
47
|
-
});
|
|
47
|
+
return d.toLocaleString();
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
/** TV uptime: now minus last boot time (device.lastBootAt). Not active when app is offline. */
|
|
51
50
|
function formatDeviceOnlineLabel(device) {
|
|
52
51
|
if (!device.connected) return "Not active";
|
|
53
52
|
const bootIso = device.lastBootAt;
|
|
@@ -57,142 +56,6 @@ function formatDeviceOnlineLabel(device) {
|
|
|
57
56
|
return formatDurationMs(Date.now() - bootMs);
|
|
58
57
|
}
|
|
59
58
|
|
|
60
|
-
async function fetchDevices() {
|
|
61
|
-
try {
|
|
62
|
-
const res = await fetch("/devices");
|
|
63
|
-
const data = await res.json();
|
|
64
|
-
if (Array.isArray(data.devices)) {
|
|
65
|
-
devicesCache = data.devices;
|
|
66
|
-
renderDeviceCards();
|
|
67
|
-
}
|
|
68
|
-
} catch (err) {
|
|
69
|
-
showResult({ status: "failed", error: err.message });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function renderDeviceCards() {
|
|
74
|
-
const grid = document.getElementById("devicesGrid");
|
|
75
|
-
if (!grid) return;
|
|
76
|
-
|
|
77
|
-
grid.innerHTML = "";
|
|
78
|
-
|
|
79
|
-
if (devicesCache.length === 0) {
|
|
80
|
-
const empty = document.createElement("p");
|
|
81
|
-
empty.className = "devices-empty";
|
|
82
|
-
empty.textContent = "No paired devices yet. Enter a code above.";
|
|
83
|
-
grid.appendChild(empty);
|
|
84
|
-
return;
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
for (const device of devicesCache) {
|
|
88
|
-
const card = document.createElement("article");
|
|
89
|
-
card.className = "device-card";
|
|
90
|
-
card.dataset.deviceId = device.deviceId;
|
|
91
|
-
|
|
92
|
-
const header = document.createElement("div");
|
|
93
|
-
header.className = "device-card-header";
|
|
94
|
-
|
|
95
|
-
const led = document.createElement("span");
|
|
96
|
-
led.className = `status-led ${device.connected ? "status-led--online" : "status-led--offline"}`;
|
|
97
|
-
led.title = device.connected ? "Connected" : "Disconnected";
|
|
98
|
-
|
|
99
|
-
const title = document.createElement("h3");
|
|
100
|
-
title.className = "device-card-title";
|
|
101
|
-
title.textContent = device.deviceName || "Screen";
|
|
102
|
-
|
|
103
|
-
header.appendChild(led);
|
|
104
|
-
header.appendChild(title);
|
|
105
|
-
|
|
106
|
-
const meta = document.createElement("dl");
|
|
107
|
-
meta.className = "device-meta";
|
|
108
|
-
|
|
109
|
-
const rows = [
|
|
110
|
-
["Device ID", device.deviceId],
|
|
111
|
-
["System", device.system || device.platform || "—"],
|
|
112
|
-
[
|
|
113
|
-
"Device online",
|
|
114
|
-
formatDeviceOnlineLabel(device)
|
|
115
|
-
],
|
|
116
|
-
["Last boot", formatDateTimeSeconds(device.lastBootAt)],
|
|
117
|
-
["Latest content push", formatDateTimeSeconds(device.lastPolicyPushAt)]
|
|
118
|
-
];
|
|
119
|
-
|
|
120
|
-
for (const [label, value] of rows) {
|
|
121
|
-
const row = document.createElement("div");
|
|
122
|
-
row.className = "device-meta-row";
|
|
123
|
-
const dt = document.createElement("dt");
|
|
124
|
-
dt.textContent = label;
|
|
125
|
-
const dd = document.createElement("dd");
|
|
126
|
-
if (label === "Device online") {
|
|
127
|
-
dd.className = "device-online-time";
|
|
128
|
-
dd.title = "Current time minus this TV last boot time";
|
|
129
|
-
}
|
|
130
|
-
dd.textContent = value;
|
|
131
|
-
row.appendChild(dt);
|
|
132
|
-
row.appendChild(dd);
|
|
133
|
-
meta.appendChild(row);
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const actions = document.createElement("div");
|
|
137
|
-
actions.className = "device-card-actions";
|
|
138
|
-
|
|
139
|
-
const publishBtn = document.createElement("button");
|
|
140
|
-
publishBtn.type = "button";
|
|
141
|
-
publishBtn.className = "primary";
|
|
142
|
-
publishBtn.textContent = "Publish";
|
|
143
|
-
publishBtn.addEventListener("click", () => publishToDevice(device.deviceId));
|
|
144
|
-
|
|
145
|
-
const infoBtn = document.createElement("button");
|
|
146
|
-
infoBtn.type = "button";
|
|
147
|
-
infoBtn.textContent = "Info";
|
|
148
|
-
infoBtn.addEventListener("click", () => deviceAction(device.deviceId, "get-info"));
|
|
149
|
-
|
|
150
|
-
const capsBtn = document.createElement("button");
|
|
151
|
-
capsBtn.type = "button";
|
|
152
|
-
capsBtn.textContent = "Capabilities";
|
|
153
|
-
capsBtn.addEventListener("click", () =>
|
|
154
|
-
deviceAction(device.deviceId, "get-capabilities")
|
|
155
|
-
);
|
|
156
|
-
|
|
157
|
-
const rebootBtn = document.createElement("button");
|
|
158
|
-
rebootBtn.type = "button";
|
|
159
|
-
rebootBtn.textContent = "Reboot";
|
|
160
|
-
rebootBtn.addEventListener("click", () => deviceAction(device.deviceId, "reboot"));
|
|
161
|
-
|
|
162
|
-
const clearBtn = document.createElement("button");
|
|
163
|
-
clearBtn.type = "button";
|
|
164
|
-
clearBtn.textContent = "Clear";
|
|
165
|
-
clearBtn.addEventListener("click", () => deviceAction(device.deviceId, "content/clear"));
|
|
166
|
-
|
|
167
|
-
const unpairBtn = document.createElement("button");
|
|
168
|
-
unpairBtn.type = "button";
|
|
169
|
-
unpairBtn.className = "danger";
|
|
170
|
-
unpairBtn.textContent = "Unpair";
|
|
171
|
-
unpairBtn.addEventListener("click", () => unpairDevice(device.deviceId));
|
|
172
|
-
|
|
173
|
-
actions.appendChild(publishBtn);
|
|
174
|
-
actions.appendChild(infoBtn);
|
|
175
|
-
actions.appendChild(capsBtn);
|
|
176
|
-
actions.appendChild(rebootBtn);
|
|
177
|
-
actions.appendChild(clearBtn);
|
|
178
|
-
actions.appendChild(unpairBtn);
|
|
179
|
-
|
|
180
|
-
card.appendChild(header);
|
|
181
|
-
card.appendChild(meta);
|
|
182
|
-
card.appendChild(actions);
|
|
183
|
-
grid.appendChild(card);
|
|
184
|
-
}
|
|
185
|
-
}
|
|
186
|
-
|
|
187
|
-
function startDevicePolling() {
|
|
188
|
-
if (devicePollTimer) clearInterval(devicePollTimer);
|
|
189
|
-
|
|
190
|
-
void fetchDevices();
|
|
191
|
-
devicePollTimer = setInterval(() => {
|
|
192
|
-
void fetchDevices();
|
|
193
|
-
}, 8000);
|
|
194
|
-
}
|
|
195
|
-
|
|
196
59
|
function showResult(data) {
|
|
197
60
|
document.getElementById("result").textContent = JSON.stringify(data, null, 2);
|
|
198
61
|
}
|
|
@@ -205,12 +68,9 @@ function isLocalPanelHost(hostname) {
|
|
|
205
68
|
function normalizeMediaBaseUrl(raw) {
|
|
206
69
|
let s = String(raw || "").trim();
|
|
207
70
|
if (!s) return "";
|
|
208
|
-
if (!/^https?:\/\//i.test(s)) {
|
|
209
|
-
s = `http://${s}`;
|
|
210
|
-
}
|
|
71
|
+
if (!/^https?:\/\//i.test(s)) s = `http://${s}`;
|
|
211
72
|
try {
|
|
212
|
-
|
|
213
|
-
return u.origin;
|
|
73
|
+
return new URL(s).origin;
|
|
214
74
|
} catch {
|
|
215
75
|
return "";
|
|
216
76
|
}
|
|
@@ -223,11 +83,7 @@ function getMediaBaseOrigin() {
|
|
|
223
83
|
""
|
|
224
84
|
);
|
|
225
85
|
if (fromInput) return fromInput;
|
|
226
|
-
|
|
227
|
-
if (!isLocalPanelHost(window.location.hostname)) {
|
|
228
|
-
return window.location.origin;
|
|
229
|
-
}
|
|
230
|
-
|
|
86
|
+
if (!isLocalPanelHost(window.location.hostname)) return window.location.origin;
|
|
231
87
|
return "";
|
|
232
88
|
}
|
|
233
89
|
|
|
@@ -248,12 +104,9 @@ function absoluteMediaUrl(path) {
|
|
|
248
104
|
const p = String(path || "").trim();
|
|
249
105
|
if (!p) return "";
|
|
250
106
|
if (/^https?:\/\//i.test(p)) return p;
|
|
251
|
-
|
|
252
107
|
const base = getMediaBaseOrigin();
|
|
253
108
|
if (!base) {
|
|
254
|
-
throw new Error(
|
|
255
|
-
"Local CMS only: set CMS URL for screens (your PC LAN IP, e.g. http://192.168.1.105:3000 — not localhost)."
|
|
256
|
-
);
|
|
109
|
+
throw new Error("Set CMS URL for screens (LAN IP, not localhost).");
|
|
257
110
|
}
|
|
258
111
|
return `${base}${p.startsWith("/") ? p : `/${p}`}`;
|
|
259
112
|
}
|
|
@@ -276,94 +129,144 @@ function defaultDurationMs(type) {
|
|
|
276
129
|
}
|
|
277
130
|
|
|
278
131
|
function normalizeDurationMs(item) {
|
|
279
|
-
const maxMs = 3600 * 1000;
|
|
280
132
|
const minMs = 1000;
|
|
133
|
+
const maxMs = 3600 * 1000;
|
|
281
134
|
let ms = Number(item?.durationMs);
|
|
282
|
-
if (!Number.isFinite(ms) || ms < minMs)
|
|
283
|
-
|
|
284
|
-
}
|
|
285
|
-
if (ms === 1000000) {
|
|
286
|
-
return defaultDurationMs(item?.type);
|
|
287
|
-
}
|
|
135
|
+
if (!Number.isFinite(ms) || ms < minMs) return defaultDurationMs(item?.type);
|
|
136
|
+
if (ms === 1000000) return defaultDurationMs(item?.type);
|
|
288
137
|
return Math.min(maxMs, ms);
|
|
289
138
|
}
|
|
290
139
|
|
|
291
|
-
function
|
|
292
|
-
|
|
140
|
+
function buildScheduleFromForm() {
|
|
141
|
+
const schedule = {};
|
|
142
|
+
const startDate = document.getElementById("scheduleStartDate")?.value?.trim();
|
|
143
|
+
const endDate = document.getElementById("scheduleEndDate")?.value?.trim();
|
|
144
|
+
const startTime = document.getElementById("scheduleStartTime")?.value?.trim();
|
|
145
|
+
const endTime = document.getElementById("scheduleEndTime")?.value?.trim();
|
|
146
|
+
const days = [...document.querySelectorAll(".day-checkbox:checked")].map((el) =>
|
|
147
|
+
Number(el.value)
|
|
148
|
+
);
|
|
149
|
+
if (startDate) schedule.startDate = startDate;
|
|
150
|
+
if (endDate) schedule.endDate = endDate;
|
|
151
|
+
if (startTime) schedule.start = startTime;
|
|
152
|
+
if (endTime) schedule.end = endTime;
|
|
153
|
+
if (days.length > 0) schedule.daysOfWeek = days;
|
|
154
|
+
return Object.keys(schedule).length > 0 ? schedule : undefined;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function loadScheduleIntoForm(schedule) {
|
|
158
|
+
const s = schedule || {};
|
|
159
|
+
document.getElementById("scheduleStartDate").value = s.startDate || "";
|
|
160
|
+
document.getElementById("scheduleEndDate").value = s.endDate || "";
|
|
161
|
+
document.getElementById("scheduleStartTime").value = s.start || "";
|
|
162
|
+
document.getElementById("scheduleEndTime").value = s.end || "";
|
|
163
|
+
document.querySelectorAll(".day-checkbox").forEach((el) => {
|
|
164
|
+
el.checked =
|
|
165
|
+
Array.isArray(s.daysOfWeek) && s.daysOfWeek.includes(Number(el.value));
|
|
166
|
+
});
|
|
293
167
|
}
|
|
294
168
|
|
|
295
|
-
function
|
|
296
|
-
|
|
297
|
-
startDate: document.getElementById("scheduleStartDate")?.value || "",
|
|
298
|
-
endDate: document.getElementById("scheduleEndDate")?.value || "",
|
|
299
|
-
startTime: document.getElementById("scheduleStartTime")?.value || "",
|
|
300
|
-
endTime: document.getElementById("scheduleEndTime")?.value || "",
|
|
301
|
-
days: [...document.querySelectorAll(".day-checkbox:checked")].map((el) => Number(el.value))
|
|
302
|
-
};
|
|
303
|
-
localStorage.setItem(PANEL_SCHEDULE_KEY, JSON.stringify(draft));
|
|
169
|
+
function getSelectedPlaylist() {
|
|
170
|
+
return playlistsCatalog.find((p) => p.id === selectedPlaylistId) || null;
|
|
304
171
|
}
|
|
305
172
|
|
|
306
|
-
function
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
if (
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
173
|
+
async function fetchPlaylists() {
|
|
174
|
+
const res = await fetch("/playlists");
|
|
175
|
+
const data = await res.json();
|
|
176
|
+
if (Array.isArray(data.playlists)) {
|
|
177
|
+
playlistsCatalog = data.playlists;
|
|
178
|
+
renderPlaylistCatalog();
|
|
179
|
+
if (selectedPlaylistId && !getSelectedPlaylist()) {
|
|
180
|
+
selectedPlaylistId = playlistsCatalog[0]?.id || null;
|
|
181
|
+
loadEditorFromSelection();
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function renderPlaylistCatalog() {
|
|
187
|
+
const list = document.getElementById("playlistCatalog");
|
|
188
|
+
if (!list) return;
|
|
189
|
+
list.innerHTML = "";
|
|
190
|
+
|
|
191
|
+
if (playlistsCatalog.length === 0) {
|
|
192
|
+
const li = document.createElement("li");
|
|
193
|
+
li.className = "playlist-catalog-item";
|
|
194
|
+
li.textContent = "No playlists yet. Tap +.";
|
|
195
|
+
list.appendChild(li);
|
|
196
|
+
return;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
for (const pl of playlistsCatalog) {
|
|
200
|
+
const li = document.createElement("li");
|
|
201
|
+
li.className = "playlist-catalog-item";
|
|
202
|
+
if (pl.id === selectedPlaylistId) li.classList.add("playlist-catalog-item--active");
|
|
203
|
+
li.innerHTML = `<strong>${escapeHtml(pl.name)}</strong><small>v${pl.version} · ${(pl.items || []).length} items</small>`;
|
|
204
|
+
li.addEventListener("click", () => {
|
|
205
|
+
selectedPlaylistId = pl.id;
|
|
206
|
+
loadEditorFromSelection();
|
|
207
|
+
renderPlaylistCatalog();
|
|
317
208
|
});
|
|
318
|
-
|
|
319
|
-
/* ignore */
|
|
209
|
+
list.appendChild(li);
|
|
320
210
|
}
|
|
321
211
|
}
|
|
322
212
|
|
|
323
|
-
function
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
}
|
|
336
|
-
} catch {
|
|
337
|
-
playlistItems = [];
|
|
213
|
+
function loadEditorFromSelection() {
|
|
214
|
+
const pl = getSelectedPlaylist();
|
|
215
|
+
const nameInput = document.getElementById("playlistName");
|
|
216
|
+
const editorTitle = document.getElementById("editorTitle");
|
|
217
|
+
|
|
218
|
+
if (!pl) {
|
|
219
|
+
if (editorTitle) editorTitle.textContent = "Playlist editor";
|
|
220
|
+
if (nameInput) nameInput.value = "";
|
|
221
|
+
editorItems = [];
|
|
222
|
+
loadScheduleIntoForm(null);
|
|
223
|
+
renderEditorAssets();
|
|
224
|
+
return;
|
|
338
225
|
}
|
|
226
|
+
|
|
227
|
+
if (editorTitle) editorTitle.textContent = `Edit: ${pl.name}`;
|
|
228
|
+
if (nameInput) nameInput.value = pl.name || "";
|
|
229
|
+
loadScheduleIntoForm(pl.schedule);
|
|
230
|
+
editorItems = (pl.items || []).map((item) => ({
|
|
231
|
+
id: crypto.randomUUID(),
|
|
232
|
+
url: item.url,
|
|
233
|
+
name: item.url?.split("/").pop() || "asset",
|
|
234
|
+
type: item.type || "image",
|
|
235
|
+
durationMs: normalizeDurationMs(item)
|
|
236
|
+
}));
|
|
237
|
+
renderEditorAssets();
|
|
339
238
|
}
|
|
340
239
|
|
|
341
|
-
function
|
|
240
|
+
function renderEditorAssets() {
|
|
342
241
|
const list = document.getElementById("playlistList");
|
|
343
242
|
const empty = document.getElementById("playlistEmpty");
|
|
344
243
|
list.querySelectorAll(".playlist-item").forEach((el) => el.remove());
|
|
345
244
|
|
|
346
|
-
if (
|
|
245
|
+
if (!selectedPlaylistId || editorItems.length === 0) {
|
|
347
246
|
empty.classList.remove("hidden");
|
|
247
|
+
empty.textContent = selectedPlaylistId
|
|
248
|
+
? "No assets yet. Tap +."
|
|
249
|
+
: "Select or create a playlist.";
|
|
348
250
|
return;
|
|
349
251
|
}
|
|
350
252
|
|
|
351
253
|
empty.classList.add("hidden");
|
|
352
254
|
|
|
353
|
-
for (const item of
|
|
255
|
+
for (const item of editorItems) {
|
|
354
256
|
const li = document.createElement("li");
|
|
355
257
|
li.className = "playlist-item";
|
|
356
|
-
li.dataset.id = item.id;
|
|
357
258
|
|
|
358
259
|
if (item.type === "image" || item.type === "video") {
|
|
359
260
|
const thumb = document.createElement(item.type === "video" ? "video" : "img");
|
|
360
261
|
thumb.className = "playlist-item-thumb";
|
|
361
|
-
|
|
262
|
+
try {
|
|
263
|
+
thumb.src = absoluteMediaUrl(item.url);
|
|
264
|
+
} catch {
|
|
265
|
+
thumb.removeAttribute("src");
|
|
266
|
+
}
|
|
362
267
|
if (item.type === "video") {
|
|
363
268
|
thumb.muted = true;
|
|
364
269
|
thumb.playsInline = true;
|
|
365
|
-
} else {
|
|
366
|
-
thumb.alt = item.name;
|
|
367
270
|
}
|
|
368
271
|
li.appendChild(thumb);
|
|
369
272
|
}
|
|
@@ -378,20 +281,14 @@ function renderPlaylist() {
|
|
|
378
281
|
|
|
379
282
|
const actions = document.createElement("div");
|
|
380
283
|
actions.className = "playlist-item-actions";
|
|
381
|
-
|
|
382
|
-
const durLabel = document.createElement("label");
|
|
383
|
-
durLabel.style.fontSize = "0.75rem";
|
|
384
|
-
durLabel.textContent = "sec ";
|
|
385
284
|
const durInput = document.createElement("input");
|
|
386
285
|
durInput.type = "number";
|
|
387
286
|
durInput.min = "1";
|
|
388
287
|
durInput.max = "3600";
|
|
389
288
|
durInput.value = String(Math.round(item.durationMs / 1000));
|
|
390
289
|
durInput.addEventListener("change", () => {
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
meta.textContent = `${item.type} · ${seconds}s`;
|
|
394
|
-
savePlaylistDraft();
|
|
290
|
+
item.durationMs = Math.min(3600, Math.max(1, Number(durInput.value) || 10)) * 1000;
|
|
291
|
+
meta.textContent = `${item.type} · ${Math.round(item.durationMs / 1000)}s`;
|
|
395
292
|
});
|
|
396
293
|
|
|
397
294
|
const removeBtn = document.createElement("button");
|
|
@@ -399,15 +296,12 @@ function renderPlaylist() {
|
|
|
399
296
|
removeBtn.className = "danger";
|
|
400
297
|
removeBtn.textContent = "Remove";
|
|
401
298
|
removeBtn.addEventListener("click", () => {
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
renderPlaylist();
|
|
299
|
+
editorItems = editorItems.filter((x) => x.id !== item.id);
|
|
300
|
+
renderEditorAssets();
|
|
405
301
|
});
|
|
406
302
|
|
|
407
|
-
actions.appendChild(durLabel);
|
|
408
303
|
actions.appendChild(durInput);
|
|
409
304
|
actions.appendChild(removeBtn);
|
|
410
|
-
|
|
411
305
|
li.appendChild(name);
|
|
412
306
|
li.appendChild(meta);
|
|
413
307
|
li.appendChild(actions);
|
|
@@ -415,226 +309,419 @@ function renderPlaylist() {
|
|
|
415
309
|
}
|
|
416
310
|
}
|
|
417
311
|
|
|
418
|
-
async function
|
|
419
|
-
const
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
throw new Error(data.error || `Upload failed (${res.status})`);
|
|
312
|
+
async function saveCurrentPlaylist() {
|
|
313
|
+
const name = String(document.getElementById("playlistName")?.value || "").trim();
|
|
314
|
+
if (!name) {
|
|
315
|
+
alert("Enter a playlist name.");
|
|
316
|
+
return;
|
|
317
|
+
}
|
|
318
|
+
if (editorItems.length === 0) {
|
|
319
|
+
alert("Add at least one asset before saving.");
|
|
320
|
+
return;
|
|
428
321
|
}
|
|
429
|
-
return data;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
async function addAssetFromFile(file) {
|
|
433
|
-
showResult({ status: "uploading", filename: file.name });
|
|
434
|
-
const data = await uploadFile(file);
|
|
435
|
-
const type = inferMediaType(file.name, file.type);
|
|
436
|
-
playlistItems.push({
|
|
437
|
-
id: crypto.randomUUID(),
|
|
438
|
-
url: data.url,
|
|
439
|
-
name: file.name,
|
|
440
|
-
type,
|
|
441
|
-
durationMs: defaultDurationMs(type)
|
|
442
|
-
});
|
|
443
|
-
savePlaylistDraft();
|
|
444
|
-
renderPlaylist();
|
|
445
|
-
showResult({ status: "uploaded", ...data });
|
|
446
|
-
}
|
|
447
|
-
|
|
448
|
-
function buildSchedule() {
|
|
449
|
-
const schedule = {};
|
|
450
|
-
const startDate = document.getElementById("scheduleStartDate")?.value?.trim();
|
|
451
|
-
const endDate = document.getElementById("scheduleEndDate")?.value?.trim();
|
|
452
|
-
const startTime = document.getElementById("scheduleStartTime")?.value?.trim();
|
|
453
|
-
const endTime = document.getElementById("scheduleEndTime")?.value?.trim();
|
|
454
|
-
const days = [...document.querySelectorAll(".day-checkbox:checked")].map((el) =>
|
|
455
|
-
Number(el.value)
|
|
456
|
-
);
|
|
457
|
-
|
|
458
|
-
if (startDate) schedule.startDate = startDate;
|
|
459
|
-
if (endDate) schedule.endDate = endDate;
|
|
460
|
-
if (startTime) schedule.start = startTime;
|
|
461
|
-
if (endTime) schedule.end = endTime;
|
|
462
|
-
if (days.length > 0) schedule.daysOfWeek = days;
|
|
463
|
-
|
|
464
|
-
return Object.keys(schedule).length > 0 ? schedule : undefined;
|
|
465
|
-
}
|
|
466
322
|
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
id: "main",
|
|
471
|
-
name: "Playlist",
|
|
472
|
-
items: playlistItems.map((item) => ({
|
|
323
|
+
let items;
|
|
324
|
+
try {
|
|
325
|
+
items = editorItems.map((item) => ({
|
|
473
326
|
url: absoluteMediaUrl(item.url),
|
|
474
327
|
type: item.type,
|
|
475
328
|
durationMs: item.durationMs
|
|
476
|
-
}))
|
|
477
|
-
}
|
|
478
|
-
|
|
329
|
+
}));
|
|
330
|
+
} catch (err) {
|
|
331
|
+
alert(err.message);
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
479
334
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
335
|
+
const body = {
|
|
336
|
+
id: selectedPlaylistId || undefined,
|
|
337
|
+
name,
|
|
338
|
+
schedule: buildScheduleFromForm(),
|
|
339
|
+
items
|
|
485
340
|
};
|
|
486
|
-
}
|
|
487
|
-
|
|
488
|
-
async function verify() {
|
|
489
|
-
const code = document.getElementById("code").value;
|
|
490
341
|
|
|
491
|
-
const res = await fetch("/
|
|
342
|
+
const res = await fetch("/playlists", {
|
|
492
343
|
method: "POST",
|
|
493
344
|
headers: { "Content-Type": "application/json" },
|
|
494
|
-
body: JSON.stringify(
|
|
345
|
+
body: JSON.stringify(body)
|
|
495
346
|
});
|
|
496
|
-
|
|
497
347
|
const data = await res.json();
|
|
498
348
|
showResult(data);
|
|
499
349
|
|
|
500
|
-
if (
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
await fetchDevices();
|
|
350
|
+
if (!res.ok) {
|
|
351
|
+
alert(data.error || "Save failed");
|
|
352
|
+
return;
|
|
504
353
|
}
|
|
354
|
+
|
|
355
|
+
selectedPlaylistId = data.playlist?.id || selectedPlaylistId;
|
|
356
|
+
await fetchPlaylists();
|
|
357
|
+
loadEditorFromSelection();
|
|
505
358
|
}
|
|
506
359
|
|
|
507
|
-
async function
|
|
360
|
+
async function deleteCurrentPlaylist() {
|
|
361
|
+
if (!selectedPlaylistId) {
|
|
362
|
+
alert("Select a playlist to delete.");
|
|
363
|
+
return;
|
|
364
|
+
}
|
|
365
|
+
const pl = getSelectedPlaylist();
|
|
508
366
|
if (
|
|
509
367
|
!confirm(
|
|
510
|
-
|
|
368
|
+
`Delete playlist "${pl?.name}"? Devices already playing it keep their cached copy until Clear or reboot without sync. New devices cannot receive it.`
|
|
511
369
|
)
|
|
512
370
|
) {
|
|
513
371
|
return;
|
|
514
372
|
}
|
|
515
373
|
|
|
516
|
-
const res = await fetch(
|
|
517
|
-
method: "
|
|
518
|
-
headers: { "Content-Type": "application/json" },
|
|
519
|
-
body: JSON.stringify({ deviceId })
|
|
374
|
+
const res = await fetch(`/playlists/${encodeURIComponent(selectedPlaylistId)}`, {
|
|
375
|
+
method: "DELETE"
|
|
520
376
|
});
|
|
521
|
-
|
|
522
377
|
const data = await res.json();
|
|
523
378
|
showResult(data);
|
|
379
|
+
if (!res.ok) {
|
|
380
|
+
alert(data.error || "Delete failed");
|
|
381
|
+
return;
|
|
382
|
+
}
|
|
524
383
|
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
384
|
+
selectedPlaylistId = null;
|
|
385
|
+
editorItems = [];
|
|
386
|
+
await fetchPlaylists();
|
|
387
|
+
loadEditorFromSelection();
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function newPlaylistDraft() {
|
|
391
|
+
selectedPlaylistId = null;
|
|
392
|
+
document.getElementById("playlistName").value = "";
|
|
393
|
+
loadScheduleIntoForm(null);
|
|
394
|
+
editorItems = [];
|
|
395
|
+
renderPlaylistCatalog();
|
|
396
|
+
renderEditorAssets();
|
|
397
|
+
document.getElementById("editorTitle").textContent = "New playlist";
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
async function fetchDevices() {
|
|
401
|
+
try {
|
|
402
|
+
const res = await fetch("/devices");
|
|
403
|
+
const data = await res.json();
|
|
404
|
+
if (Array.isArray(data.devices)) {
|
|
405
|
+
devicesCache = data.devices;
|
|
406
|
+
renderDeviceCards();
|
|
407
|
+
}
|
|
408
|
+
} catch (err) {
|
|
409
|
+
showResult({ status: "failed", error: err.message });
|
|
529
410
|
}
|
|
530
411
|
}
|
|
531
412
|
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
413
|
+
function renderDeviceCards() {
|
|
414
|
+
const grid = document.getElementById("devicesGrid");
|
|
415
|
+
if (!grid) return;
|
|
416
|
+
grid.innerHTML = "";
|
|
417
|
+
|
|
418
|
+
if (devicesCache.length === 0) {
|
|
419
|
+
const empty = document.createElement("p");
|
|
420
|
+
empty.className = "devices-empty";
|
|
421
|
+
empty.textContent = "No paired devices yet.";
|
|
422
|
+
grid.appendChild(empty);
|
|
535
423
|
return;
|
|
536
424
|
}
|
|
537
425
|
|
|
538
|
-
|
|
539
|
-
|
|
426
|
+
for (const device of devicesCache) {
|
|
427
|
+
const card = document.createElement("article");
|
|
428
|
+
card.className = "device-card";
|
|
429
|
+
|
|
430
|
+
const header = document.createElement("div");
|
|
431
|
+
header.className = "device-card-header";
|
|
432
|
+
const led = document.createElement("span");
|
|
433
|
+
led.className = `status-led ${device.connected ? "status-led--online" : "status-led--offline"}`;
|
|
434
|
+
const title = document.createElement("h3");
|
|
435
|
+
title.className = "device-card-title";
|
|
436
|
+
title.textContent = device.deviceName || "Screen";
|
|
437
|
+
header.appendChild(led);
|
|
438
|
+
header.appendChild(title);
|
|
439
|
+
|
|
440
|
+
const published = document.createElement("ul");
|
|
441
|
+
published.className = "device-published-list";
|
|
442
|
+
const pubs = Array.isArray(device.publishedPlaylists) ? device.publishedPlaylists : [];
|
|
443
|
+
if (pubs.length === 0) {
|
|
444
|
+
const li = document.createElement("li");
|
|
445
|
+
li.textContent = "No playlists published";
|
|
446
|
+
published.appendChild(li);
|
|
447
|
+
} else {
|
|
448
|
+
for (const p of pubs) {
|
|
449
|
+
const li = document.createElement("li");
|
|
450
|
+
const label = document.createElement("span");
|
|
451
|
+
label.textContent = `${p.name} (v${p.version})`;
|
|
452
|
+
const rm = document.createElement("button");
|
|
453
|
+
rm.type = "button";
|
|
454
|
+
rm.textContent = "Remove";
|
|
455
|
+
rm.addEventListener("click", () => removePlaylistFromDevice(device.deviceId, p.playlistId));
|
|
456
|
+
li.appendChild(label);
|
|
457
|
+
li.appendChild(rm);
|
|
458
|
+
published.appendChild(li);
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
const meta = document.createElement("dl");
|
|
463
|
+
meta.className = "device-meta";
|
|
464
|
+
const rows = [
|
|
465
|
+
["Device ID", device.deviceId],
|
|
466
|
+
["System", device.system || device.platform || "—"],
|
|
467
|
+
["Device online", formatDeviceOnlineLabel(device)],
|
|
468
|
+
["Last boot", formatDateTimeSeconds(device.lastBootAt)],
|
|
469
|
+
["Latest push", formatDateTimeSeconds(device.lastPolicyPushAt)]
|
|
470
|
+
];
|
|
471
|
+
for (const [label, value] of rows) {
|
|
472
|
+
const row = document.createElement("div");
|
|
473
|
+
row.className = "device-meta-row";
|
|
474
|
+
const dt = document.createElement("dt");
|
|
475
|
+
dt.textContent = label;
|
|
476
|
+
const dd = document.createElement("dd");
|
|
477
|
+
dd.textContent = value;
|
|
478
|
+
row.appendChild(dt);
|
|
479
|
+
row.appendChild(dd);
|
|
480
|
+
meta.appendChild(row);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
const actions = document.createElement("div");
|
|
484
|
+
actions.className = "device-card-actions";
|
|
485
|
+
|
|
486
|
+
const publishBtn = document.createElement("button");
|
|
487
|
+
publishBtn.type = "button";
|
|
488
|
+
publishBtn.className = "primary";
|
|
489
|
+
publishBtn.textContent = "Publish";
|
|
490
|
+
publishBtn.addEventListener("click", () => openPublishModal(device.deviceId));
|
|
491
|
+
|
|
492
|
+
const infoBtn = document.createElement("button");
|
|
493
|
+
infoBtn.type = "button";
|
|
494
|
+
infoBtn.textContent = "Info";
|
|
495
|
+
infoBtn.addEventListener("click", () => deviceAction(device.deviceId, "get-info"));
|
|
496
|
+
|
|
497
|
+
const rebootBtn = document.createElement("button");
|
|
498
|
+
rebootBtn.type = "button";
|
|
499
|
+
rebootBtn.textContent = "Reboot";
|
|
500
|
+
rebootBtn.addEventListener("click", () => deviceAction(device.deviceId, "reboot"));
|
|
501
|
+
|
|
502
|
+
const clearBtn = document.createElement("button");
|
|
503
|
+
clearBtn.type = "button";
|
|
504
|
+
clearBtn.textContent = "Clear";
|
|
505
|
+
clearBtn.addEventListener("click", () => deviceAction(device.deviceId, "content/clear"));
|
|
506
|
+
|
|
507
|
+
const unpairBtn = document.createElement("button");
|
|
508
|
+
unpairBtn.type = "button";
|
|
509
|
+
unpairBtn.className = "danger";
|
|
510
|
+
unpairBtn.textContent = "Unpair";
|
|
511
|
+
unpairBtn.addEventListener("click", () => unpairDevice(device.deviceId));
|
|
512
|
+
|
|
513
|
+
actions.appendChild(publishBtn);
|
|
514
|
+
actions.appendChild(infoBtn);
|
|
515
|
+
actions.appendChild(rebootBtn);
|
|
516
|
+
actions.appendChild(clearBtn);
|
|
517
|
+
actions.appendChild(unpairBtn);
|
|
518
|
+
|
|
519
|
+
card.appendChild(header);
|
|
520
|
+
card.appendChild(published);
|
|
521
|
+
card.appendChild(meta);
|
|
522
|
+
card.appendChild(actions);
|
|
523
|
+
grid.appendChild(card);
|
|
524
|
+
}
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
function openPublishModal(deviceId) {
|
|
528
|
+
publishModalDeviceId = deviceId;
|
|
529
|
+
const modal = document.getElementById("publishModal");
|
|
530
|
+
const checklist = document.getElementById("publishChecklist");
|
|
531
|
+
const hint = document.getElementById("publishModalHint");
|
|
532
|
+
if (!modal || !checklist) return;
|
|
533
|
+
|
|
534
|
+
if (playlistsCatalog.length === 0) {
|
|
535
|
+
alert("Create and save at least one playlist first.");
|
|
540
536
|
return;
|
|
541
537
|
}
|
|
542
538
|
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
);
|
|
539
|
+
hint.textContent = `Device ${deviceId} — select playlists to publish (snapshot at publish time).`;
|
|
540
|
+
checklist.innerHTML = "";
|
|
541
|
+
|
|
542
|
+
for (const pl of playlistsCatalog) {
|
|
543
|
+
const label = document.createElement("label");
|
|
544
|
+
const cb = document.createElement("input");
|
|
545
|
+
cb.type = "checkbox";
|
|
546
|
+
cb.value = pl.id;
|
|
547
|
+
cb.dataset.name = pl.name;
|
|
548
|
+
const pubs = devicesCache.find((d) => d.deviceId === deviceId)?.publishedPlaylists || [];
|
|
549
|
+
if (pubs.some((p) => p.playlistId === pl.id)) cb.checked = true;
|
|
550
|
+
label.appendChild(cb);
|
|
551
|
+
label.appendChild(document.createTextNode(` ${pl.name} (v${pl.version})`));
|
|
552
|
+
checklist.appendChild(label);
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
modal.classList.remove("hidden");
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function closePublishModal() {
|
|
559
|
+
publishModalDeviceId = null;
|
|
560
|
+
document.getElementById("publishModal")?.classList.add("hidden");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
async function confirmPublishModal() {
|
|
564
|
+
if (!publishModalDeviceId) return;
|
|
565
|
+
const ids = [
|
|
566
|
+
...document.querySelectorAll("#publishChecklist input[type=checkbox]:checked")
|
|
567
|
+
].map((el) => el.value);
|
|
568
|
+
|
|
569
|
+
if (ids.length === 0) {
|
|
570
|
+
alert("Select at least one playlist.");
|
|
548
571
|
return;
|
|
549
572
|
}
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
573
|
+
|
|
574
|
+
const res = await fetch(
|
|
575
|
+
`/device/${encodeURIComponent(publishModalDeviceId)}/assignments`,
|
|
576
|
+
{
|
|
577
|
+
method: "POST",
|
|
578
|
+
headers: { "Content-Type": "application/json" },
|
|
579
|
+
body: JSON.stringify({ playlistIds: ids })
|
|
580
|
+
}
|
|
581
|
+
);
|
|
582
|
+
const data = await res.json();
|
|
583
|
+
showResult({ deviceId: publishModalDeviceId, publish: data });
|
|
584
|
+
if (!res.ok) {
|
|
585
|
+
alert(data.error || "Publish failed");
|
|
586
|
+
return;
|
|
555
587
|
}
|
|
588
|
+
closePublishModal();
|
|
589
|
+
await fetchDevices();
|
|
590
|
+
}
|
|
556
591
|
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
try {
|
|
560
|
-
payload = buildPolicyPayload();
|
|
561
|
-
} catch (err) {
|
|
562
|
-
alert(err.message);
|
|
592
|
+
async function removePlaylistFromDevice(deviceId, playlistId) {
|
|
593
|
+
if (!confirm("Remove this playlist from the device? Currently playing content may continue until Clear or failed reboot sync.")) {
|
|
563
594
|
return;
|
|
564
595
|
}
|
|
596
|
+
const res = await fetch(
|
|
597
|
+
`/device/${encodeURIComponent(deviceId)}/assignments/${encodeURIComponent(playlistId)}`,
|
|
598
|
+
{ method: "DELETE" }
|
|
599
|
+
);
|
|
600
|
+
const data = await res.json();
|
|
601
|
+
showResult({ deviceId, remove: data });
|
|
602
|
+
if (!res.ok) {
|
|
603
|
+
alert(data.error || "Remove failed");
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
await fetchDevices();
|
|
607
|
+
}
|
|
565
608
|
|
|
566
|
-
|
|
609
|
+
async function uploadFile(file) {
|
|
610
|
+
const q = new URLSearchParams({ filename: file.name });
|
|
611
|
+
const res = await fetch(`/media/upload?${q.toString()}`, {
|
|
567
612
|
method: "POST",
|
|
568
|
-
headers: { "Content-Type": "application/
|
|
569
|
-
body:
|
|
613
|
+
headers: { "Content-Type": "application/octet-stream" },
|
|
614
|
+
body: file
|
|
570
615
|
});
|
|
571
|
-
|
|
572
616
|
const data = await res.json();
|
|
573
|
-
|
|
617
|
+
if (!res.ok || data.status === "failed") {
|
|
618
|
+
throw new Error(data.error || `Upload failed (${res.status})`);
|
|
619
|
+
}
|
|
620
|
+
return data;
|
|
621
|
+
}
|
|
574
622
|
|
|
575
|
-
|
|
623
|
+
async function addAssetFromFile(file) {
|
|
624
|
+
if (!selectedPlaylistId) {
|
|
625
|
+
alert("Select or create a playlist first, then Save.");
|
|
626
|
+
return;
|
|
627
|
+
}
|
|
628
|
+
const data = await uploadFile(file);
|
|
629
|
+
const type = inferMediaType(file.name, file.type);
|
|
630
|
+
editorItems.push({
|
|
631
|
+
id: crypto.randomUUID(),
|
|
632
|
+
url: data.url,
|
|
633
|
+
name: file.name,
|
|
634
|
+
type,
|
|
635
|
+
durationMs: defaultDurationMs(type)
|
|
636
|
+
});
|
|
637
|
+
renderEditorAssets();
|
|
638
|
+
showResult({ status: "uploaded", ...data });
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
async function verify() {
|
|
642
|
+
const code = String(document.getElementById("code").value || "")
|
|
643
|
+
.trim()
|
|
644
|
+
.toUpperCase()
|
|
645
|
+
.replace(/[^0-9A-Z]/g, "");
|
|
646
|
+
if (code.length !== 6) {
|
|
647
|
+
showResult({ status: "failed", error: "Enter the 6-character code from the screen." });
|
|
648
|
+
return;
|
|
649
|
+
}
|
|
650
|
+
const res = await fetch("/pairing/verify", {
|
|
651
|
+
method: "POST",
|
|
652
|
+
headers: { "Content-Type": "application/json" },
|
|
653
|
+
body: JSON.stringify({ code })
|
|
654
|
+
});
|
|
655
|
+
const data = await res.json();
|
|
656
|
+
showResult(data);
|
|
657
|
+
if (data.deviceId) {
|
|
658
|
+
document.getElementById("code").value = "";
|
|
576
659
|
await fetchDevices();
|
|
577
660
|
}
|
|
578
661
|
}
|
|
579
662
|
|
|
663
|
+
async function unpairDevice(deviceId) {
|
|
664
|
+
if (!confirm("Unpair this device?")) return;
|
|
665
|
+
const res = await fetch("/pairing/unpair", {
|
|
666
|
+
method: "POST",
|
|
667
|
+
headers: { "Content-Type": "application/json" },
|
|
668
|
+
body: JSON.stringify({ deviceId })
|
|
669
|
+
});
|
|
670
|
+
const data = await res.json();
|
|
671
|
+
showResult(data);
|
|
672
|
+
if (res.ok) await fetchDevices();
|
|
673
|
+
}
|
|
674
|
+
|
|
580
675
|
async function deviceAction(deviceId, action) {
|
|
581
676
|
if (!deviceId) return;
|
|
582
|
-
|
|
583
677
|
if (action === "reboot" && !confirm("Reboot this device?")) return;
|
|
584
678
|
if (action === "content/clear" && !confirm("Clear content on this device?")) return;
|
|
585
|
-
|
|
586
679
|
const res = await fetch(`/device/${encodeURIComponent(deviceId)}/${action}`, {
|
|
587
680
|
method: "POST"
|
|
588
681
|
});
|
|
589
682
|
const data = await res.json();
|
|
590
683
|
showResult({ deviceId, action, ...data });
|
|
684
|
+
if (action === "reboot" && res.ok) await fetchDevices();
|
|
685
|
+
}
|
|
591
686
|
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
687
|
+
function startDevicePolling() {
|
|
688
|
+
if (devicePollTimer) clearInterval(devicePollTimer);
|
|
689
|
+
void fetchDevices();
|
|
690
|
+
devicePollTimer = setInterval(() => void fetchDevices(), 8000);
|
|
595
691
|
}
|
|
596
692
|
|
|
597
693
|
document.addEventListener("DOMContentLoaded", () => {
|
|
598
694
|
const savedMediaBase = localStorage.getItem(PANEL_MEDIA_BASE_KEY);
|
|
599
695
|
const cmsBaseInput = document.getElementById("cmsDeviceBaseUrl");
|
|
600
|
-
if (savedMediaBase && cmsBaseInput)
|
|
601
|
-
|
|
602
|
-
} else if (cmsBaseInput && !isLocalPanelHost(window.location.hostname)) {
|
|
696
|
+
if (savedMediaBase && cmsBaseInput) cmsBaseInput.value = savedMediaBase;
|
|
697
|
+
else if (cmsBaseInput && !isLocalPanelHost(window.location.hostname)) {
|
|
603
698
|
cmsBaseInput.value = window.location.origin;
|
|
604
699
|
}
|
|
605
700
|
|
|
606
|
-
|
|
607
|
-
loadScheduleDraft();
|
|
701
|
+
void fetchPlaylists();
|
|
608
702
|
startDevicePolling();
|
|
609
703
|
|
|
610
|
-
document.getElementById("
|
|
704
|
+
document.getElementById("newPlaylistBtn")?.addEventListener("click", newPlaylistDraft);
|
|
705
|
+
document.getElementById("savePlaylistBtn")?.addEventListener("click", () => void saveCurrentPlaylist());
|
|
706
|
+
document.getElementById("deletePlaylistBtn")?.addEventListener("click", () => void deleteCurrentPlaylist());
|
|
707
|
+
document.getElementById("publishConfirmBtn")?.addEventListener("click", () => void confirmPublishModal());
|
|
708
|
+
|
|
709
|
+
document.querySelectorAll("[data-close-modal]").forEach((el) => {
|
|
710
|
+
el.addEventListener("click", closePublishModal);
|
|
711
|
+
});
|
|
712
|
+
|
|
713
|
+
document.getElementById("addAssetBtn")?.addEventListener("click", () => {
|
|
611
714
|
document.getElementById("fileInput").click();
|
|
612
715
|
});
|
|
613
716
|
|
|
614
|
-
document.getElementById("fileInput")
|
|
717
|
+
document.getElementById("fileInput")?.addEventListener("change", async (ev) => {
|
|
615
718
|
const files = ev.target.files;
|
|
616
719
|
if (!files?.length) return;
|
|
617
720
|
try {
|
|
618
|
-
for (const file of files)
|
|
619
|
-
await addAssetFromFile(file);
|
|
620
|
-
}
|
|
721
|
+
for (const file of files) await addAssetFromFile(file);
|
|
621
722
|
} catch (err) {
|
|
622
723
|
alert(err.message);
|
|
623
|
-
showResult({ status: "failed", error: err.message });
|
|
624
724
|
}
|
|
625
725
|
ev.target.value = "";
|
|
626
726
|
});
|
|
627
|
-
|
|
628
|
-
[
|
|
629
|
-
"scheduleStartDate",
|
|
630
|
-
"scheduleEndDate",
|
|
631
|
-
"scheduleStartTime",
|
|
632
|
-
"scheduleEndTime"
|
|
633
|
-
].forEach((id) => {
|
|
634
|
-
document.getElementById(id)?.addEventListener("change", saveScheduleDraft);
|
|
635
|
-
});
|
|
636
|
-
document.querySelectorAll(".day-checkbox").forEach((el) => {
|
|
637
|
-
el.addEventListener("change", saveScheduleDraft);
|
|
638
|
-
});
|
|
639
|
-
|
|
640
727
|
});
|