@tomorrowos/sdk 0.6.3 → 0.6.7
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/tomorrowos.d.ts +4 -0
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +39 -0
- package/package.json +1 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/public/index.html +6 -0
- package/templates/cms-starter/public/methods.js +335 -58
- package/templates/cms-starter/public/panel.css +88 -1
package/dist/tomorrowos.d.ts
CHANGED
|
@@ -53,6 +53,8 @@ export interface DeviceListItem {
|
|
|
53
53
|
version: number;
|
|
54
54
|
publishedAt: string;
|
|
55
55
|
}>;
|
|
56
|
+
currentPlaylistId: string | null;
|
|
57
|
+
currentPlaylistName: string | null;
|
|
56
58
|
latestErrorAt: string | null;
|
|
57
59
|
latestErrorMessage: string | null;
|
|
58
60
|
}
|
|
@@ -78,6 +80,7 @@ export declare class TomorrowOS extends EventEmitter {
|
|
|
78
80
|
private readonly devices;
|
|
79
81
|
private readonly pendingDeviceMeta;
|
|
80
82
|
private readonly deviceLogs;
|
|
83
|
+
private readonly livePlaybackByDevice;
|
|
81
84
|
private httpServer;
|
|
82
85
|
private wss;
|
|
83
86
|
private staticRoot;
|
|
@@ -109,6 +112,7 @@ export declare class TomorrowOS extends EventEmitter {
|
|
|
109
112
|
};
|
|
110
113
|
/** List paired devices with live connection + timing fields for the CMS panel. */
|
|
111
114
|
listDevices(): Promise<DeviceListItem[]>;
|
|
115
|
+
private refreshLivePlaybackState;
|
|
112
116
|
setDeviceName(deviceId: string, deviceName: string): Promise<{
|
|
113
117
|
deviceId: string;
|
|
114
118
|
deviceName: string;
|
package/dist/tomorrowos.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -186,6 +186,7 @@ export class TomorrowOS extends EventEmitter {
|
|
|
186
186
|
devices = new Map();
|
|
187
187
|
pendingDeviceMeta = new Map();
|
|
188
188
|
deviceLogs = new Map();
|
|
189
|
+
livePlaybackByDevice = new Map();
|
|
189
190
|
httpServer = null;
|
|
190
191
|
wss = null;
|
|
191
192
|
staticRoot = null;
|
|
@@ -312,6 +313,13 @@ export class TomorrowOS extends EventEmitter {
|
|
|
312
313
|
const connected = this.isDeviceConnected(deviceId);
|
|
313
314
|
const reg = await this.store.getDeviceRegistry(deviceId);
|
|
314
315
|
const assignments = await this.store.getDeviceAssignments(deviceId);
|
|
316
|
+
if (connected) {
|
|
317
|
+
await this.refreshLivePlaybackState(deviceId);
|
|
318
|
+
}
|
|
319
|
+
else {
|
|
320
|
+
this.livePlaybackByDevice.delete(deviceId);
|
|
321
|
+
}
|
|
322
|
+
const playback = this.livePlaybackByDevice.get(deviceId);
|
|
315
323
|
let screenOnlineActive = false;
|
|
316
324
|
let screenOnlineLabel = "Not active";
|
|
317
325
|
if (connected && record.lastBootAt) {
|
|
@@ -333,6 +341,8 @@ export class TomorrowOS extends EventEmitter {
|
|
|
333
341
|
version: a.publishedVersion,
|
|
334
342
|
publishedAt: a.publishedAt
|
|
335
343
|
})),
|
|
344
|
+
currentPlaylistId: playback?.playlistId ?? null,
|
|
345
|
+
currentPlaylistName: playback?.playlistName ?? null,
|
|
336
346
|
connected,
|
|
337
347
|
deviceName: record.deviceName ?? null,
|
|
338
348
|
platform: record.platform ?? null,
|
|
@@ -352,6 +362,35 @@ export class TomorrowOS extends EventEmitter {
|
|
|
352
362
|
};
|
|
353
363
|
}));
|
|
354
364
|
}
|
|
365
|
+
async refreshLivePlaybackState(deviceId) {
|
|
366
|
+
const ws = this.devices.get(deviceId);
|
|
367
|
+
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
368
|
+
return;
|
|
369
|
+
try {
|
|
370
|
+
const result = await this.sendCommandToSocket(ws, deviceId, "device.info.get", {}, 5_000);
|
|
371
|
+
if (result.status !== "success" || !result.data || typeof result.data !== "object") {
|
|
372
|
+
return;
|
|
373
|
+
}
|
|
374
|
+
const info = result.data;
|
|
375
|
+
const playback = info.currentPlayback && typeof info.currentPlayback === "object"
|
|
376
|
+
? info.currentPlayback
|
|
377
|
+
: info;
|
|
378
|
+
const playlistId = typeof playback.activePlaylistId === "string" && playback.activePlaylistId.trim()
|
|
379
|
+
? playback.activePlaylistId.trim()
|
|
380
|
+
: null;
|
|
381
|
+
const playlistName = typeof playback.activePlaylistName === "string" && playback.activePlaylistName.trim()
|
|
382
|
+
? playback.activePlaylistName.trim()
|
|
383
|
+
: null;
|
|
384
|
+
this.livePlaybackByDevice.set(deviceId, {
|
|
385
|
+
playlistId,
|
|
386
|
+
playlistName,
|
|
387
|
+
updatedAt: new Date().toISOString()
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
catch {
|
|
391
|
+
// Best-effort only; keep previous value on intermittent timeouts.
|
|
392
|
+
}
|
|
393
|
+
}
|
|
355
394
|
async setDeviceName(deviceId, deviceName) {
|
|
356
395
|
const id = String(deviceId || "").trim();
|
|
357
396
|
if (!id) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "CMS server on @tomorrowos/sdk. Add your UI (React, static files, etc.) alongside this server.",
|
|
5
5
|
"private": true,
|
|
6
6
|
"type": "module",
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
"build-player": "tomorrowos build --platform tizen"
|
|
11
11
|
},
|
|
12
12
|
"dependencies": {
|
|
13
|
-
"@tomorrowos/sdk": "^0.6.
|
|
13
|
+
"@tomorrowos/sdk": "^0.6.7",
|
|
14
14
|
"dotenv": "^17.2.3"
|
|
15
15
|
},
|
|
16
16
|
"devDependencies": {
|
|
@@ -129,6 +129,12 @@
|
|
|
129
129
|
<h2>Publish to device</h2>
|
|
130
130
|
<p class="hint" id="publishModalHint">Select playlists to add to this device. Already-published playlists are not listed here.</p>
|
|
131
131
|
<div id="publishChecklist" class="publish-checklist"></div>
|
|
132
|
+
<div id="publishStatusShell" class="publish-status hidden" aria-live="polite">
|
|
133
|
+
<div id="publishStatusText" class="publish-status-text">Working...</div>
|
|
134
|
+
<div class="upload-progress-track">
|
|
135
|
+
<div id="publishProgressBar" class="upload-progress-bar"></div>
|
|
136
|
+
</div>
|
|
137
|
+
</div>
|
|
132
138
|
<div class="modal-actions">
|
|
133
139
|
<button type="button" id="publishConfirmBtn" class="primary">Publish selected</button>
|
|
134
140
|
<button type="button" data-close-modal="1">Cancel</button>
|
|
@@ -17,6 +17,7 @@ let editorItems = [];
|
|
|
17
17
|
|
|
18
18
|
/** @type {string|null} */
|
|
19
19
|
let publishModalDeviceId = null;
|
|
20
|
+
let publishInProgress = false;
|
|
20
21
|
/** @type {string|null} */
|
|
21
22
|
let editingDeviceNameId = null;
|
|
22
23
|
/** @type {string} */
|
|
@@ -138,8 +139,8 @@ function playlistHasRelativeMediaUrls(playlist) {
|
|
|
138
139
|
|
|
139
140
|
/**
|
|
140
141
|
* Base URL sent on publish only when needed.
|
|
141
|
-
* - Hosted (e.g. Replit): use public origin
|
|
142
|
-
* - Local:
|
|
142
|
+
* - Hosted (e.g. Replit): use public origin when items are relative (/uploads/...).
|
|
143
|
+
* - Local: prefer saved LAN override; otherwise fall back to current origin for dev.
|
|
143
144
|
* - Returns null to omit mediaBaseUrl (playlist already has absolute https URLs).
|
|
144
145
|
*/
|
|
145
146
|
function getPublishMediaBaseUrl(selectedPlaylists) {
|
|
@@ -152,7 +153,181 @@ function getPublishMediaBaseUrl(selectedPlaylists) {
|
|
|
152
153
|
if (!isLocalPanelHost(window.location.hostname)) {
|
|
153
154
|
return window.location.origin;
|
|
154
155
|
}
|
|
155
|
-
|
|
156
|
+
// Local dev: same-origin fallback so panel publish/verify works without LAN override.
|
|
157
|
+
return window.location.origin;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/** Browser-side check URL — always same-origin so localhost panel can reach /uploads. */
|
|
161
|
+
function resolveVerificationMediaUrl(url) {
|
|
162
|
+
const p = String(url || "").trim();
|
|
163
|
+
if (!p) return "";
|
|
164
|
+
if (p.startsWith("/")) return p;
|
|
165
|
+
try {
|
|
166
|
+
const parsed = new URL(p);
|
|
167
|
+
const path = parsed.pathname + parsed.search;
|
|
168
|
+
if (path.startsWith("/uploads/") || path.startsWith("/screenshots/")) {
|
|
169
|
+
return path;
|
|
170
|
+
}
|
|
171
|
+
return p;
|
|
172
|
+
} catch {
|
|
173
|
+
return p;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
function resolvePublishMediaUrl(url, mediaBaseUrl) {
|
|
178
|
+
const p = String(url || "").trim();
|
|
179
|
+
if (!p) return "";
|
|
180
|
+
if (/^https?:\/\//i.test(p)) return p;
|
|
181
|
+
const base =
|
|
182
|
+
normalizeMediaBaseUrl(mediaBaseUrl) ||
|
|
183
|
+
getExplicitLanMediaBase() ||
|
|
184
|
+
window.location.origin;
|
|
185
|
+
if (!base) return "";
|
|
186
|
+
return `${base}${p.startsWith("/") ? p : `/${p}`}`;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
async function verifyMediaAssetReachable(url) {
|
|
190
|
+
if (!url) return { ok: false, reason: "missing URL" };
|
|
191
|
+
|
|
192
|
+
const candidates = [];
|
|
193
|
+
const sameOriginPath = resolveVerificationMediaUrl(url);
|
|
194
|
+
if (sameOriginPath) candidates.push(sameOriginPath);
|
|
195
|
+
if (url && url !== sameOriginPath) candidates.push(url);
|
|
196
|
+
|
|
197
|
+
let lastReason = "unreachable";
|
|
198
|
+
for (const target of candidates) {
|
|
199
|
+
try {
|
|
200
|
+
// CMS static files are served on GET only (no HEAD / Range support).
|
|
201
|
+
const res = await fetch(target, { method: "GET", cache: "no-store" });
|
|
202
|
+
if (res.ok) return { ok: true };
|
|
203
|
+
lastReason = `HTTP ${res.status}`;
|
|
204
|
+
} catch (err) {
|
|
205
|
+
lastReason = err?.message || "network error";
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return { ok: false, reason: lastReason };
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
async function verifyPlaylistsAssetsReady(playlists, mediaBaseUrl, onProgress) {
|
|
212
|
+
const tasks = [];
|
|
213
|
+
for (const pl of playlists) {
|
|
214
|
+
for (const item of pl.items || []) {
|
|
215
|
+
const name = String(item?.name || item?.url || "asset").split("/").pop() || "asset";
|
|
216
|
+
const resolvedUrl = resolveVerificationMediaUrl(item?.url);
|
|
217
|
+
tasks.push({
|
|
218
|
+
playlist: pl.name || pl.id,
|
|
219
|
+
name,
|
|
220
|
+
url: item?.url,
|
|
221
|
+
resolvedUrl
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (tasks.length === 0) {
|
|
227
|
+
return { ok: false, failures: [{ playlist: "—", name: "—", reason: "no assets in playlist" }] };
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const failures = [];
|
|
231
|
+
let done = 0;
|
|
232
|
+
for (const task of tasks) {
|
|
233
|
+
onProgress?.(done, tasks.length, task.name);
|
|
234
|
+
if (!task.resolvedUrl) {
|
|
235
|
+
failures.push({
|
|
236
|
+
playlist: task.playlist,
|
|
237
|
+
name: task.name,
|
|
238
|
+
reason: "could not resolve media URL"
|
|
239
|
+
});
|
|
240
|
+
} else {
|
|
241
|
+
const result = await verifyMediaAssetReachable(task.resolvedUrl);
|
|
242
|
+
if (!result.ok) {
|
|
243
|
+
failures.push({
|
|
244
|
+
playlist: task.playlist,
|
|
245
|
+
name: task.name,
|
|
246
|
+
reason: result.reason || "unreachable"
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
done += 1;
|
|
251
|
+
onProgress?.(done, tasks.length, task.name);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return { ok: failures.length === 0, failures, total: tasks.length };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
async function refreshPlaylistsForPublish() {
|
|
258
|
+
try {
|
|
259
|
+
const res = await fetch("/playlists");
|
|
260
|
+
let data = {};
|
|
261
|
+
try {
|
|
262
|
+
data = await res.json();
|
|
263
|
+
} catch {
|
|
264
|
+
data = {};
|
|
265
|
+
}
|
|
266
|
+
if (!res.ok || !Array.isArray(data.playlists)) return false;
|
|
267
|
+
playlistsCatalog = data.playlists;
|
|
268
|
+
renderPlaylistCatalog();
|
|
269
|
+
return true;
|
|
270
|
+
} catch {
|
|
271
|
+
return false;
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
function resetPublishModalStatus() {
|
|
276
|
+
publishInProgress = false;
|
|
277
|
+
updatePublishStatusUi({ hidden: true });
|
|
278
|
+
const confirmBtn = document.getElementById("publishConfirmBtn");
|
|
279
|
+
if (confirmBtn) {
|
|
280
|
+
confirmBtn.disabled = false;
|
|
281
|
+
confirmBtn.textContent = "Publish selected";
|
|
282
|
+
}
|
|
283
|
+
const modal = document.getElementById("publishModal");
|
|
284
|
+
modal?.querySelectorAll("[data-close-modal]").forEach((el) => {
|
|
285
|
+
if (el instanceof HTMLButtonElement) el.disabled = false;
|
|
286
|
+
});
|
|
287
|
+
document
|
|
288
|
+
.querySelectorAll("#publishChecklist input[type=checkbox]")
|
|
289
|
+
.forEach((el) => {
|
|
290
|
+
if (el instanceof HTMLInputElement) el.disabled = false;
|
|
291
|
+
});
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function setPublishModalBusy(busy) {
|
|
295
|
+
publishInProgress = busy;
|
|
296
|
+
const confirmBtn = document.getElementById("publishConfirmBtn");
|
|
297
|
+
if (confirmBtn) {
|
|
298
|
+
confirmBtn.disabled = busy;
|
|
299
|
+
confirmBtn.textContent = busy ? "Publishing..." : "Publish selected";
|
|
300
|
+
}
|
|
301
|
+
document
|
|
302
|
+
.querySelectorAll("#publishChecklist input[type=checkbox]")
|
|
303
|
+
.forEach((el) => {
|
|
304
|
+
if (el instanceof HTMLInputElement) el.disabled = busy;
|
|
305
|
+
});
|
|
306
|
+
document.querySelectorAll("#publishModal [data-close-modal]").forEach((el) => {
|
|
307
|
+
if (el instanceof HTMLButtonElement) el.disabled = busy;
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function updatePublishStatusUi(status) {
|
|
312
|
+
const shell = document.getElementById("publishStatusShell");
|
|
313
|
+
const text = document.getElementById("publishStatusText");
|
|
314
|
+
const bar = document.getElementById("publishProgressBar");
|
|
315
|
+
if (!shell || !text || !bar) return;
|
|
316
|
+
|
|
317
|
+
if (!status || status.hidden) {
|
|
318
|
+
shell.classList.add("hidden");
|
|
319
|
+
shell.classList.remove("publish-status--error", "publish-status--success");
|
|
320
|
+
bar.style.width = "0%";
|
|
321
|
+
text.textContent = "";
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
shell.classList.remove("hidden");
|
|
326
|
+
shell.classList.toggle("publish-status--error", !!status.isError);
|
|
327
|
+
shell.classList.toggle("publish-status--success", !!status.isSuccess);
|
|
328
|
+
text.textContent = status.text || "Working...";
|
|
329
|
+
const percent = Math.max(0, Math.min(100, Number(status.percent) || 0));
|
|
330
|
+
bar.style.width = `${percent}%`;
|
|
156
331
|
}
|
|
157
332
|
|
|
158
333
|
/** Resolve media URLs in the editor (save / thumbnails). */
|
|
@@ -664,6 +839,8 @@ function renderDeviceCards() {
|
|
|
664
839
|
|
|
665
840
|
const header = document.createElement("div");
|
|
666
841
|
header.className = "device-card-header";
|
|
842
|
+
const headerMain = document.createElement("div");
|
|
843
|
+
headerMain.className = "device-card-header-main";
|
|
667
844
|
const led = document.createElement("span");
|
|
668
845
|
led.className = `status-led ${device.connected ? "status-led--online" : "status-led--offline"}`;
|
|
669
846
|
const title = document.createElement("h3");
|
|
@@ -672,9 +849,9 @@ function renderDeviceCards() {
|
|
|
672
849
|
if (isEditingName) {
|
|
673
850
|
const input = document.createElement("input");
|
|
674
851
|
input.type = "text";
|
|
852
|
+
input.className = "device-card-title-input";
|
|
675
853
|
input.value = editingDeviceNameValue || device.deviceName || "Screen";
|
|
676
854
|
input.placeholder = "Device name";
|
|
677
|
-
input.style.minWidth = "12rem";
|
|
678
855
|
input.addEventListener("input", (ev) => {
|
|
679
856
|
editingDeviceNameValue = String(ev.target.value || "");
|
|
680
857
|
});
|
|
@@ -691,12 +868,42 @@ function renderDeviceCards() {
|
|
|
691
868
|
} else {
|
|
692
869
|
title.textContent = device.deviceName || "Screen";
|
|
693
870
|
}
|
|
694
|
-
|
|
695
|
-
|
|
871
|
+
headerMain.appendChild(led);
|
|
872
|
+
headerMain.appendChild(title);
|
|
873
|
+
|
|
874
|
+
const headerActions = document.createElement("div");
|
|
875
|
+
headerActions.className = "device-card-header-actions";
|
|
876
|
+
if (isEditingName) {
|
|
877
|
+
const saveBtn = document.createElement("button");
|
|
878
|
+
saveBtn.type = "button";
|
|
879
|
+
saveBtn.className = "primary";
|
|
880
|
+
saveBtn.textContent = "Save";
|
|
881
|
+
saveBtn.addEventListener("click", () =>
|
|
882
|
+
void submitInlineDeviceRename(device.deviceId)
|
|
883
|
+
);
|
|
884
|
+
const cancelBtn = document.createElement("button");
|
|
885
|
+
cancelBtn.type = "button";
|
|
886
|
+
cancelBtn.textContent = "Cancel";
|
|
887
|
+
cancelBtn.addEventListener("click", cancelInlineDeviceRename);
|
|
888
|
+
headerActions.appendChild(saveBtn);
|
|
889
|
+
headerActions.appendChild(cancelBtn);
|
|
890
|
+
} else {
|
|
891
|
+
const renameBtn = document.createElement("button");
|
|
892
|
+
renameBtn.type = "button";
|
|
893
|
+
renameBtn.textContent = "Rename";
|
|
894
|
+
renameBtn.addEventListener("click", () =>
|
|
895
|
+
startInlineDeviceRename(device.deviceId, device.deviceName)
|
|
896
|
+
);
|
|
897
|
+
headerActions.appendChild(renameBtn);
|
|
898
|
+
}
|
|
899
|
+
header.appendChild(headerMain);
|
|
900
|
+
header.appendChild(headerActions);
|
|
696
901
|
|
|
697
902
|
const published = document.createElement("ul");
|
|
698
903
|
published.className = "device-published-list";
|
|
699
904
|
const pubs = Array.isArray(device.publishedPlaylists) ? device.publishedPlaylists : [];
|
|
905
|
+
const activePlaylistId = typeof device.currentPlaylistId === "string" ? device.currentPlaylistId : "";
|
|
906
|
+
const activePlaylistName = typeof device.currentPlaylistName === "string" ? device.currentPlaylistName : "";
|
|
700
907
|
if (pubs.length === 0) {
|
|
701
908
|
const li = document.createElement("li");
|
|
702
909
|
li.textContent = "No playlists published";
|
|
@@ -705,7 +912,17 @@ function renderDeviceCards() {
|
|
|
705
912
|
for (const p of pubs) {
|
|
706
913
|
const li = document.createElement("li");
|
|
707
914
|
const label = document.createElement("span");
|
|
915
|
+
label.className = "device-published-label";
|
|
708
916
|
label.textContent = p.name;
|
|
917
|
+
const isPlaying =
|
|
918
|
+
(activePlaylistId && p.playlistId === activePlaylistId) ||
|
|
919
|
+
(!activePlaylistId && activePlaylistName && p.name === activePlaylistName);
|
|
920
|
+
if (isPlaying) {
|
|
921
|
+
const playingLight = document.createElement("span");
|
|
922
|
+
playingLight.className = "playlist-playing-light";
|
|
923
|
+
playingLight.title = "Currently playing";
|
|
924
|
+
label.appendChild(playingLight);
|
|
925
|
+
}
|
|
709
926
|
const rm = document.createElement("button");
|
|
710
927
|
rm.type = "button";
|
|
711
928
|
rm.textContent = "Remove";
|
|
@@ -786,26 +1003,6 @@ function renderDeviceCards() {
|
|
|
786
1003
|
latestScreenshotBtn.textContent = "Last screen";
|
|
787
1004
|
latestScreenshotBtn.addEventListener("click", () => viewLatestScreenshot(device.deviceId));
|
|
788
1005
|
|
|
789
|
-
const renameBtn = document.createElement("button");
|
|
790
|
-
renameBtn.type = "button";
|
|
791
|
-
if (isEditingName) {
|
|
792
|
-
renameBtn.className = "primary";
|
|
793
|
-
renameBtn.textContent = "Save";
|
|
794
|
-
renameBtn.addEventListener("click", () =>
|
|
795
|
-
void submitInlineDeviceRename(device.deviceId)
|
|
796
|
-
);
|
|
797
|
-
} else {
|
|
798
|
-
renameBtn.textContent = "Rename";
|
|
799
|
-
renameBtn.addEventListener("click", () =>
|
|
800
|
-
startInlineDeviceRename(device.deviceId, device.deviceName)
|
|
801
|
-
);
|
|
802
|
-
}
|
|
803
|
-
|
|
804
|
-
const cancelRenameBtn = document.createElement("button");
|
|
805
|
-
cancelRenameBtn.type = "button";
|
|
806
|
-
cancelRenameBtn.textContent = "Cancel";
|
|
807
|
-
cancelRenameBtn.addEventListener("click", cancelInlineDeviceRename);
|
|
808
|
-
|
|
809
1006
|
const unpairBtn = document.createElement("button");
|
|
810
1007
|
unpairBtn.type = "button";
|
|
811
1008
|
unpairBtn.className = "danger";
|
|
@@ -820,8 +1017,6 @@ function renderDeviceCards() {
|
|
|
820
1017
|
actions.appendChild(logsBtn);
|
|
821
1018
|
actions.appendChild(screenshotBtn);
|
|
822
1019
|
actions.appendChild(latestScreenshotBtn);
|
|
823
|
-
actions.appendChild(renameBtn);
|
|
824
|
-
if (isEditingName) actions.appendChild(cancelRenameBtn);
|
|
825
1020
|
actions.appendChild(unpairBtn);
|
|
826
1021
|
|
|
827
1022
|
card.appendChild(header);
|
|
@@ -868,15 +1063,24 @@ function openPublishModal(deviceId) {
|
|
|
868
1063
|
}
|
|
869
1064
|
|
|
870
1065
|
modal.classList.remove("hidden");
|
|
1066
|
+
resetPublishModalStatus();
|
|
871
1067
|
}
|
|
872
1068
|
|
|
873
1069
|
function closePublishModal() {
|
|
1070
|
+
if (publishInProgress) return;
|
|
874
1071
|
publishModalDeviceId = null;
|
|
1072
|
+
resetPublishModalStatus();
|
|
875
1073
|
document.getElementById("publishModal")?.classList.add("hidden");
|
|
876
1074
|
}
|
|
877
1075
|
|
|
878
1076
|
async function confirmPublishModal() {
|
|
879
|
-
if (!publishModalDeviceId) return;
|
|
1077
|
+
if (!publishModalDeviceId || publishInProgress) return;
|
|
1078
|
+
|
|
1079
|
+
if (uploadInProgress || uploadQueue.length > 0) {
|
|
1080
|
+
alert("Wait for asset uploads to finish before publishing.");
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
|
|
880
1084
|
const ids = [
|
|
881
1085
|
...document.querySelectorAll("#publishChecklist input[type=checkbox]:checked")
|
|
882
1086
|
].map((el) => el.value);
|
|
@@ -886,48 +1090,121 @@ async function confirmPublishModal() {
|
|
|
886
1090
|
return;
|
|
887
1091
|
}
|
|
888
1092
|
|
|
889
|
-
const
|
|
890
|
-
|
|
891
|
-
|
|
1093
|
+
const deviceId = publishModalDeviceId;
|
|
1094
|
+
setPublishModalBusy(true);
|
|
1095
|
+
updatePublishStatusUi({ hidden: false, text: "Refreshing playlists...", percent: 8 });
|
|
892
1096
|
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1097
|
+
try {
|
|
1098
|
+
const refreshed = await refreshPlaylistsForPublish();
|
|
1099
|
+
if (!refreshed) {
|
|
1100
|
+
updatePublishStatusUi({
|
|
1101
|
+
text: "Could not load playlists from the server.",
|
|
1102
|
+
percent: 0,
|
|
1103
|
+
isError: true
|
|
1104
|
+
});
|
|
1105
|
+
setPublishModalBusy(false);
|
|
1106
|
+
alert("Could not load playlists from the server. Try again.");
|
|
896
1107
|
return;
|
|
897
1108
|
}
|
|
898
|
-
}
|
|
899
1109
|
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
|
|
903
|
-
|
|
1110
|
+
const selectedPlaylists = ids
|
|
1111
|
+
.map((id) => playlistsCatalog.find((p) => p.id === id))
|
|
1112
|
+
.filter(Boolean);
|
|
1113
|
+
|
|
1114
|
+
if (selectedPlaylists.length !== ids.length) {
|
|
1115
|
+
updatePublishStatusUi({
|
|
1116
|
+
text: "A selected playlist is no longer available.",
|
|
1117
|
+
percent: 0,
|
|
1118
|
+
isError: true
|
|
1119
|
+
});
|
|
1120
|
+
setPublishModalBusy(false);
|
|
1121
|
+
alert("A selected playlist was removed or changed. Close this dialog and open Publish again.");
|
|
1122
|
+
return;
|
|
1123
|
+
}
|
|
1124
|
+
|
|
1125
|
+
for (const pl of selectedPlaylists) {
|
|
1126
|
+
if (!(pl.items || []).length) {
|
|
1127
|
+
updatePublishStatusUi({
|
|
1128
|
+
text: `Playlist "${pl.name || pl.id}" has no assets.`,
|
|
1129
|
+
percent: 0,
|
|
1130
|
+
isError: true
|
|
1131
|
+
});
|
|
1132
|
+
setPublishModalBusy(false);
|
|
1133
|
+
alert(`Playlist "${pl.name || pl.id}" has no assets. Save the playlist first.`);
|
|
1134
|
+
return;
|
|
1135
|
+
}
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
const mediaBaseUrl = getPublishMediaBaseUrl(selectedPlaylists);
|
|
1139
|
+
|
|
1140
|
+
const verification = await verifyPlaylistsAssetsReady(
|
|
1141
|
+
selectedPlaylists,
|
|
1142
|
+
mediaBaseUrl,
|
|
1143
|
+
(done, total, assetName) => {
|
|
1144
|
+
const pct = 12 + Math.round((done / Math.max(total, 1)) * 68);
|
|
1145
|
+
updatePublishStatusUi({
|
|
1146
|
+
text: `Verifying assets (${done}/${total}): ${assetName}`,
|
|
1147
|
+
percent: pct
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
904
1150
|
);
|
|
905
|
-
return;
|
|
906
|
-
}
|
|
907
1151
|
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
1152
|
+
if (!verification.ok) {
|
|
1153
|
+
const lines = verification.failures
|
|
1154
|
+
.map((f) => `• ${f.playlist} — ${f.name}: ${f.reason}`)
|
|
1155
|
+
.join("\n");
|
|
1156
|
+
updatePublishStatusUi({
|
|
1157
|
+
text: `Could not load ${verification.failures.length} asset(s). Publishing blocked.`,
|
|
1158
|
+
percent: 0,
|
|
1159
|
+
isError: true
|
|
1160
|
+
});
|
|
1161
|
+
setPublishModalBusy(false);
|
|
1162
|
+
alert(`Cannot publish until all playlist assets are reachable:\n\n${lines}`);
|
|
1163
|
+
return;
|
|
1164
|
+
}
|
|
911
1165
|
|
|
912
|
-
|
|
913
|
-
if (mediaBaseUrl) publishBody.mediaBaseUrl = mediaBaseUrl;
|
|
1166
|
+
updatePublishStatusUi({ text: "Publishing to device...", percent: 88 });
|
|
914
1167
|
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
1168
|
+
const device = devicesCache.find((d) => d.deviceId === deviceId);
|
|
1169
|
+
const alreadyOnDevice = (device?.publishedPlaylists || []).map((p) => p.playlistId);
|
|
1170
|
+
const playlistIds = [...new Set([...alreadyOnDevice, ...ids])];
|
|
1171
|
+
|
|
1172
|
+
const publishBody = { playlistIds };
|
|
1173
|
+
if (mediaBaseUrl) publishBody.mediaBaseUrl = mediaBaseUrl;
|
|
1174
|
+
|
|
1175
|
+
const res = await fetch(`/device/${encodeURIComponent(deviceId)}/assignments`, {
|
|
918
1176
|
method: "POST",
|
|
919
1177
|
headers: { "Content-Type": "application/json" },
|
|
920
1178
|
body: JSON.stringify(publishBody)
|
|
1179
|
+
});
|
|
1180
|
+
const data = await res.json();
|
|
1181
|
+
showResult({ deviceId, publish: data });
|
|
1182
|
+
|
|
1183
|
+
if (!res.ok) {
|
|
1184
|
+
updatePublishStatusUi({
|
|
1185
|
+
text: data.error || "Publish failed",
|
|
1186
|
+
percent: 0,
|
|
1187
|
+
isError: true
|
|
1188
|
+
});
|
|
1189
|
+
setPublishModalBusy(false);
|
|
1190
|
+
alert(data.error || "Publish failed");
|
|
1191
|
+
return;
|
|
921
1192
|
}
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
1193
|
+
|
|
1194
|
+
updatePublishStatusUi({ text: "Published successfully!", percent: 100, isSuccess: true });
|
|
1195
|
+
await fetchDevices();
|
|
1196
|
+
await new Promise((resolve) => setTimeout(resolve, 900));
|
|
1197
|
+
publishInProgress = false;
|
|
1198
|
+
closePublishModal();
|
|
1199
|
+
} catch (err) {
|
|
1200
|
+
updatePublishStatusUi({
|
|
1201
|
+
text: err?.message || "Publish failed",
|
|
1202
|
+
percent: 0,
|
|
1203
|
+
isError: true
|
|
1204
|
+
});
|
|
1205
|
+
setPublishModalBusy(false);
|
|
1206
|
+
alert(err?.message || "Publish failed");
|
|
928
1207
|
}
|
|
929
|
-
closePublishModal();
|
|
930
|
-
await fetchDevices();
|
|
931
1208
|
}
|
|
932
1209
|
|
|
933
1210
|
async function removePlaylistFromDevice(deviceId, playlistId) {
|
|
@@ -141,15 +141,45 @@ body {
|
|
|
141
141
|
.device-card-header {
|
|
142
142
|
display: flex;
|
|
143
143
|
align-items: center;
|
|
144
|
-
|
|
144
|
+
justify-content: space-between;
|
|
145
|
+
gap: 0.35rem;
|
|
145
146
|
margin-bottom: 0.65rem;
|
|
146
147
|
}
|
|
147
148
|
|
|
149
|
+
.device-card-header-main {
|
|
150
|
+
display: flex;
|
|
151
|
+
align-items: center;
|
|
152
|
+
gap: 0.5rem;
|
|
153
|
+
min-width: 0;
|
|
154
|
+
flex: 1;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.device-card-header-actions {
|
|
158
|
+
display: flex;
|
|
159
|
+
align-items: center;
|
|
160
|
+
gap: 0.35rem;
|
|
161
|
+
flex-shrink: 0;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
.device-card-header-actions button {
|
|
165
|
+
font-size: 0.78rem;
|
|
166
|
+
padding: 0.35rem 0.6rem;
|
|
167
|
+
}
|
|
168
|
+
|
|
148
169
|
.device-card-title {
|
|
149
170
|
margin: 0;
|
|
150
171
|
font-size: 0.95rem;
|
|
151
172
|
font-weight: 600;
|
|
152
173
|
line-height: 1.2;
|
|
174
|
+
min-width: 0;
|
|
175
|
+
flex: 1;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
.device-card-title-input {
|
|
179
|
+
width: 100%;
|
|
180
|
+
min-width: 0;
|
|
181
|
+
font-size: 0.95rem;
|
|
182
|
+
font-weight: 600;
|
|
153
183
|
}
|
|
154
184
|
|
|
155
185
|
.status-led {
|
|
@@ -455,6 +485,21 @@ button.danger {
|
|
|
455
485
|
margin-bottom: 0.25rem;
|
|
456
486
|
}
|
|
457
487
|
|
|
488
|
+
.device-published-label {
|
|
489
|
+
display: inline-flex;
|
|
490
|
+
align-items: center;
|
|
491
|
+
gap: 0.35rem;
|
|
492
|
+
}
|
|
493
|
+
|
|
494
|
+
.playlist-playing-light {
|
|
495
|
+
width: 9px;
|
|
496
|
+
height: 9px;
|
|
497
|
+
border-radius: 50%;
|
|
498
|
+
background: #12b76a;
|
|
499
|
+
box-shadow: 0 0 7px rgba(18, 183, 106, 0.65);
|
|
500
|
+
flex-shrink: 0;
|
|
501
|
+
}
|
|
502
|
+
|
|
458
503
|
.modal {
|
|
459
504
|
position: fixed;
|
|
460
505
|
inset: 0;
|
|
@@ -501,6 +546,48 @@ button.danger {
|
|
|
501
546
|
font-size: 0.9rem;
|
|
502
547
|
}
|
|
503
548
|
|
|
549
|
+
.publish-status {
|
|
550
|
+
margin-top: 0.85rem;
|
|
551
|
+
padding: 0.55rem 0.6rem;
|
|
552
|
+
border: 1px solid #e4e1dc;
|
|
553
|
+
border-radius: 8px;
|
|
554
|
+
background: #fafaf9;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
.publish-status-text {
|
|
558
|
+
font-size: 0.82rem;
|
|
559
|
+
font-weight: 600;
|
|
560
|
+
color: #333;
|
|
561
|
+
margin-bottom: 0.35rem;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
.publish-status--error {
|
|
565
|
+
border-color: #fecdca;
|
|
566
|
+
background: #fff5f5;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
.publish-status--error .publish-status-text {
|
|
570
|
+
color: #b42318;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
.publish-status--success {
|
|
574
|
+
border-color: #abefc6;
|
|
575
|
+
background: #f3fcf6;
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
.publish-status--success .publish-status-text {
|
|
579
|
+
color: #067647;
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
.publish-status--success .upload-progress-bar {
|
|
583
|
+
background: #12b76a;
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
.modal-actions button:disabled {
|
|
587
|
+
opacity: 0.55;
|
|
588
|
+
cursor: not-allowed;
|
|
589
|
+
}
|
|
590
|
+
|
|
504
591
|
.modal-actions {
|
|
505
592
|
display: flex;
|
|
506
593
|
gap: 0.5rem;
|