@tomorrowos/sdk 0.3.2 → 0.3.4

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.
@@ -15,6 +15,10 @@ export interface BuiltDevicePolicy {
15
15
  syncMode?: "latest" | "snapshot";
16
16
  };
17
17
  }
18
+ export interface PolicyBuildOptions {
19
+ useLatest?: boolean;
20
+ mediaBaseUrl?: string;
21
+ }
18
22
  export declare class PlaylistCatalog {
19
23
  private readonly store;
20
24
  constructor(store: TomorrowOSStore);
@@ -24,11 +28,9 @@ export declare class PlaylistCatalog {
24
28
  savePlaylist(input: SavePlaylistInput): Promise<StoredPlaylist>;
25
29
  retirePlaylist(id: string): Promise<StoredPlaylist>;
26
30
  getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
27
- publishPlaylistsToDevice(deviceId: string, playlistIds: string[]): Promise<BuiltDevicePolicy>;
31
+ publishPlaylistsToDevice(deviceId: string, playlistIds: string[], options?: PolicyBuildOptions): Promise<BuiltDevicePolicy>;
28
32
  removePlaylistFromDevice(deviceId: string, playlistId: string): Promise<BuiltDevicePolicy>;
29
- buildPolicyForDevice(deviceId: string, options?: {
30
- useLatest?: boolean;
31
- }): Promise<BuiltDevicePolicy>;
33
+ buildPolicyForDevice(deviceId: string, options?: PolicyBuildOptions): Promise<BuiltDevicePolicy>;
32
34
  private buildPolicyFromAssignments;
33
35
  canPublishPlaylistToNewDevice(playlistId: string): Promise<boolean>;
34
36
  }
@@ -1 +1 @@
1
- {"version":3,"file":"playlist-catalog.d.ts","sourceRoot":"","sources":["../src/playlist-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QACN,SAAS,EAAE,yBAAyB,EAAE,CAAC;QACvC,QAAQ,EAAE;YAAE,IAAI,EAAE,OAAO,CAAA;SAAE,CAAC;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;KAClC,CAAC;CACH;AAYD,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,eAAe;IAE7C,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAK1C,6BAA6B,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAgC/D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAcnD,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAAE,GACpB,OAAO,CAAC,iBAAiB,CAAC;IA8BvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAOvB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE;QAAE,SAAS,CAAC,EAAE,OAAO,CAAA;KAAO,GACpC,OAAO,CAAC,iBAAiB,CAAC;YAKf,0BAA0B;IA+BxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
1
+ {"version":3,"file":"playlist-catalog.d.ts","sourceRoot":"","sources":["../src/playlist-catalog.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,wBAAwB,EACxB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAE1B,MAAM,WAAW,iBAAiB;IAChC,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,gBAAgB,CAAC;IAC5B,KAAK,EAAE,kBAAkB,EAAE,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC,MAAM,EAAE;QACN,SAAS,EAAE,yBAAyB,EAAE,CAAC;QACvC,QAAQ,EAAE;YAAE,IAAI,EAAE,OAAO,CAAA;SAAE,CAAC;QAC5B,QAAQ,CAAC,EAAE,MAAM,CAAC;QAClB,QAAQ,CAAC,EAAE,QAAQ,GAAG,UAAU,CAAC;KAClC,CAAC;CACH;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA+CD,qBAAa,eAAe;IACd,OAAO,CAAC,QAAQ,CAAC,KAAK;gBAAL,KAAK,EAAE,eAAe;IAE7C,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAK1C,6BAA6B,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1D,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,YAAY,CAAC,KAAK,EAAE,iBAAiB,GAAG,OAAO,CAAC,cAAc,CAAC;IAgC/D,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,CAAC;IAcnD,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,MAAM,EAAE,EACrB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;IA2CvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAOvB,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC,iBAAiB,CAAC;YAKf,0BAA0B;IAgCxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
@@ -1,11 +1,44 @@
1
1
  import { randomUUID } from "crypto";
2
- function cloneSnapshot(playlist) {
2
+ function normalizeMediaBaseUrl(raw) {
3
+ let s = String(raw || "").trim();
4
+ if (!s)
5
+ return "";
6
+ if (!/^https?:\/\//i.test(s))
7
+ s = `http://${s}`;
8
+ try {
9
+ return new URL(s).origin;
10
+ }
11
+ catch {
12
+ return "";
13
+ }
14
+ }
15
+ function absolutizeMediaUrl(url, mediaBaseUrl) {
16
+ const trimmed = String(url || "").trim();
17
+ if (!trimmed)
18
+ return trimmed;
19
+ if (/^https?:\/\//i.test(trimmed))
20
+ return trimmed;
21
+ const base = normalizeMediaBaseUrl(mediaBaseUrl);
22
+ if (!base)
23
+ return trimmed;
24
+ return `${base}${trimmed.startsWith("/") ? trimmed : `/${trimmed}`}`;
25
+ }
26
+ function absolutizePlaylistItems(items, mediaBaseUrl) {
27
+ const base = normalizeMediaBaseUrl(mediaBaseUrl);
28
+ if (!base)
29
+ return items.map((item) => ({ ...item }));
30
+ return items.map((item) => ({
31
+ ...item,
32
+ url: absolutizeMediaUrl(item.url, base)
33
+ }));
34
+ }
35
+ function cloneSnapshot(playlist, mediaBaseUrl) {
3
36
  return {
4
37
  id: playlist.id,
5
38
  name: playlist.name,
6
39
  version: playlist.version,
7
40
  schedule: playlist.schedule ? { ...playlist.schedule } : undefined,
8
- items: playlist.items.map((item) => ({ ...item }))
41
+ items: absolutizePlaylistItems(playlist.items, mediaBaseUrl)
9
42
  };
10
43
  }
11
44
  export class PlaylistCatalog {
@@ -67,15 +100,24 @@ export class PlaylistCatalog {
67
100
  async getDeviceAssignments(deviceId) {
68
101
  return this.store.getDeviceAssignments(deviceId);
69
102
  }
70
- async publishPlaylistsToDevice(deviceId, playlistIds) {
71
- const ids = [...new Set(playlistIds.map((x) => String(x).trim()).filter(Boolean))];
72
- if (ids.length === 0) {
103
+ async publishPlaylistsToDevice(deviceId, playlistIds, options = {}) {
104
+ const incoming = [...new Set(playlistIds.map((x) => String(x).trim()).filter(Boolean))];
105
+ if (incoming.length === 0) {
73
106
  throw Object.assign(new Error("Select at least one playlist"), {
74
107
  code: "PLAYLIST_INVALID"
75
108
  });
76
109
  }
110
+ const existing = await this.store.getDeviceAssignments(deviceId);
111
+ const existingById = new Map(existing.map((a) => [a.playlistId, a]));
112
+ const allIds = [...new Set([...existing.map((a) => a.playlistId), ...incoming])];
77
113
  const assignments = [];
78
- for (const playlistId of ids) {
114
+ for (const playlistId of allIds) {
115
+ if (!incoming.includes(playlistId)) {
116
+ const kept = existingById.get(playlistId);
117
+ if (kept)
118
+ assignments.push(kept);
119
+ continue;
120
+ }
79
121
  const playlist = await this.store.getPlaylist(playlistId);
80
122
  if (!playlist || playlist.retired) {
81
123
  throw Object.assign(new Error(`Playlist not available: ${playlistId}`), {
@@ -86,11 +128,14 @@ export class PlaylistCatalog {
86
128
  playlistId,
87
129
  publishedVersion: playlist.version,
88
130
  publishedAt: new Date().toISOString(),
89
- snapshot: cloneSnapshot(playlist)
131
+ snapshot: cloneSnapshot(playlist, options.mediaBaseUrl)
90
132
  });
91
133
  }
92
134
  await this.store.setDeviceAssignments(deviceId, assignments);
93
- return this.buildPolicyFromAssignments(assignments, { useLatest: false });
135
+ return this.buildPolicyFromAssignments(assignments, {
136
+ useLatest: false,
137
+ mediaBaseUrl: options.mediaBaseUrl
138
+ });
94
139
  }
95
140
  async removePlaylistFromDevice(deviceId, playlistId) {
96
141
  const assignments = await this.store.getDeviceAssignments(deviceId);
@@ -104,18 +149,19 @@ export class PlaylistCatalog {
104
149
  }
105
150
  async buildPolicyFromAssignments(assignments, options) {
106
151
  const useLatest = options.useLatest === true;
152
+ const mediaBaseUrl = options.mediaBaseUrl;
107
153
  const playlists = [];
108
154
  for (const assignment of assignments) {
109
155
  if (useLatest) {
110
156
  const current = await this.store.getPlaylist(assignment.playlistId);
111
157
  if (current && !current.retired) {
112
- playlists.push(cloneSnapshot(current));
158
+ playlists.push(cloneSnapshot(current, mediaBaseUrl));
113
159
  continue;
114
160
  }
115
161
  }
116
162
  playlists.push({
117
163
  ...assignment.snapshot,
118
- items: assignment.snapshot.items.map((item) => ({ ...item }))
164
+ items: absolutizePlaylistItems(assignment.snapshot.items, mediaBaseUrl)
119
165
  });
120
166
  }
121
167
  return {
@@ -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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"}
@@ -639,14 +639,18 @@ export class TomorrowOS extends EventEmitter {
639
639
  }
640
640
  try {
641
641
  let built;
642
+ const mediaBaseUrl = typeof body.mediaBaseUrl === "string" ? body.mediaBaseUrl : undefined;
642
643
  if (body.useLatest === true) {
643
644
  built = await this.playlists.buildPolicyForDevice(deviceId, {
644
- useLatest: true
645
+ useLatest: true,
646
+ mediaBaseUrl
645
647
  });
646
648
  }
647
649
  else {
648
650
  const ids = Array.isArray(body.playlistIds) ? body.playlistIds : [];
649
- built = await this.playlists.publishPlaylistsToDevice(deviceId, ids);
651
+ built = await this.playlists.publishPlaylistsToDevice(deviceId, ids, {
652
+ mediaBaseUrl
653
+ });
650
654
  }
651
655
  const result = await this.sendDeviceCommand(deviceId, "device.content.setPolicy", {
652
656
  policy: built.policy
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.3.2",
3
+ "version": "0.3.4",
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.3.2",
3
+ "version": "0.3.4",
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.3.2"
13
+ "@tomorrowos/sdk": "^0.3.4"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@types/node": "^20.0.0",
@@ -41,7 +41,8 @@
41
41
  <section class="card" id="cmsUrlSection">
42
42
  <h2>CMS URL for screens</h2>
43
43
  <p class="hint">
44
- <strong>Local development only.</strong> LAN URL for TVs (not localhost).
44
+ <strong>Local development only.</strong> Optional LAN override so TVs reach your PC (not localhost).
45
+ Hosted CMS (e.g. Replit) uses its public URL automatically — leave this empty there.
45
46
  </p>
46
47
  <div class="row">
47
48
  <input
@@ -63,7 +64,10 @@
63
64
  </label>
64
65
 
65
66
  <h3 class="subheading">When this playlist plays</h3>
66
- <p class="hint">Leave blank for always on (device local time).</p>
67
+ <p class="hint">
68
+ Leave blank for always on. Uses the <strong>TV/device local clock</strong> (not CMS server time).
69
+ “Until” includes that minute (e.g. Until 18:00 plays through 18:00).
70
+ </p>
67
71
  <div class="schedule-grid">
68
72
  <label>
69
73
  Start date
@@ -123,7 +127,7 @@
123
127
  <div class="modal-backdrop" data-close-modal="1"></div>
124
128
  <div class="modal-card">
125
129
  <h2>Publish to device</h2>
126
- <p class="hint" id="publishModalHint">Select playlists to deploy. Updates apply on publish; reboot pulls latest saved versions.</p>
130
+ <p class="hint" id="publishModalHint">Select playlists to add to this device. Already-published playlists are not listed here.</p>
127
131
  <div id="publishChecklist" class="publish-checklist"></div>
128
132
  <div class="modal-actions">
129
133
  <button type="button" id="publishConfirmBtn" class="primary">Publish selected</button>
@@ -79,15 +79,44 @@ function normalizeMediaBaseUrl(raw) {
79
79
  }
80
80
  }
81
81
 
82
+ /** User-saved LAN override (local dev only). Not used on hosted CMS unless explicitly set. */
83
+ function getExplicitLanMediaBase() {
84
+ return normalizeMediaBaseUrl(localStorage.getItem(PANEL_MEDIA_BASE_KEY) || "");
85
+ }
86
+
87
+ function playlistHasRelativeMediaUrls(playlist) {
88
+ return (playlist?.items || []).some((item) => {
89
+ const url = String(item?.url || "").trim();
90
+ return url && !/^https?:\/\//i.test(url);
91
+ });
92
+ }
93
+
94
+ /**
95
+ * Base URL sent on publish only when needed.
96
+ * - Hosted (e.g. Replit): use public origin only if items are still relative (/uploads/...).
97
+ * - Local: use saved LAN override only when set; never default to rewriting otherwise.
98
+ * - Returns null to omit mediaBaseUrl (playlist already has absolute https URLs).
99
+ */
100
+ function getPublishMediaBaseUrl(selectedPlaylists) {
101
+ const explicitLan = getExplicitLanMediaBase();
102
+ if (explicitLan) return explicitLan;
103
+
104
+ const needsRewrite = selectedPlaylists.some(playlistHasRelativeMediaUrls);
105
+ if (!needsRewrite) return null;
106
+
107
+ if (!isLocalPanelHost(window.location.hostname)) {
108
+ return window.location.origin;
109
+ }
110
+ return "";
111
+ }
112
+
113
+ /** Resolve media URLs in the editor (save / thumbnails). */
82
114
  function getMediaBaseOrigin() {
83
- const fromInput = normalizeMediaBaseUrl(
84
- document.getElementById("cmsDeviceBaseUrl")?.value ||
85
- localStorage.getItem(PANEL_MEDIA_BASE_KEY) ||
86
- ""
87
- );
88
- if (fromInput) return fromInput;
115
+ const explicitLan = getExplicitLanMediaBase();
116
+ if (explicitLan) return explicitLan;
89
117
  if (!isLocalPanelHost(window.location.hostname)) return window.location.origin;
90
- return "";
118
+ const draft = normalizeMediaBaseUrl(document.getElementById("cmsDeviceBaseUrl")?.value);
119
+ return draft;
91
120
  }
92
121
 
93
122
  function saveCmsDeviceBaseUrl() {
@@ -188,7 +217,7 @@ async function fetchPlaylists() {
188
217
  showResult({
189
218
  status: "failed",
190
219
  error:
191
- "CMS server is missing /playlists. Restart CMS with @tomorrowos/sdk 0.3.2 or newer."
220
+ "CMS server is missing /playlists. Restart CMS with @tomorrowos/sdk 0.3.4 or newer."
192
221
  });
193
222
  }
194
223
  return;
@@ -547,6 +576,13 @@ function renderDeviceCards() {
547
576
  infoBtn.textContent = "Info";
548
577
  infoBtn.addEventListener("click", () => deviceAction(device.deviceId, "get-info"));
549
578
 
579
+ const capBtn = document.createElement("button");
580
+ capBtn.type = "button";
581
+ capBtn.textContent = "Get capabilities";
582
+ capBtn.addEventListener("click", () =>
583
+ deviceAction(device.deviceId, "get-capabilities")
584
+ );
585
+
550
586
  const rebootBtn = document.createElement("button");
551
587
  rebootBtn.type = "button";
552
588
  rebootBtn.textContent = "Reboot";
@@ -565,6 +601,7 @@ function renderDeviceCards() {
565
601
 
566
602
  actions.appendChild(publishBtn);
567
603
  actions.appendChild(infoBtn);
604
+ actions.appendChild(capBtn);
568
605
  actions.appendChild(rebootBtn);
569
606
  actions.appendChild(clearBtn);
570
607
  actions.appendChild(unpairBtn);
@@ -589,17 +626,24 @@ function openPublishModal(deviceId) {
589
626
  return;
590
627
  }
591
628
 
592
- hint.textContent = `Device ${deviceId} select playlists to publish (snapshot at publish time).`;
629
+ const pubs = devicesCache.find((d) => d.deviceId === deviceId)?.publishedPlaylists || [];
630
+ const publishedIds = new Set(pubs.map((p) => p.playlistId));
631
+ const unpublished = playlistsCatalog.filter((pl) => !publishedIds.has(pl.id));
632
+
633
+ if (unpublished.length === 0) {
634
+ alert("All playlists are already published to this device. Use Remove on the card to unpublish one first.");
635
+ return;
636
+ }
637
+
638
+ hint.textContent = `Device ${deviceId} — add playlists not yet on this device (snapshot at publish time).`;
593
639
  checklist.innerHTML = "";
594
640
 
595
- for (const pl of playlistsCatalog) {
641
+ for (const pl of unpublished) {
596
642
  const label = document.createElement("label");
597
643
  const cb = document.createElement("input");
598
644
  cb.type = "checkbox";
599
645
  cb.value = pl.id;
600
646
  cb.dataset.name = pl.name;
601
- const pubs = devicesCache.find((d) => d.deviceId === deviceId)?.publishedPlaylists || [];
602
- if (pubs.some((p) => p.playlistId === pl.id)) cb.checked = true;
603
647
  label.appendChild(cb);
604
648
  label.appendChild(document.createTextNode(` ${pl.name} (v${pl.version})`));
605
649
  checklist.appendChild(label);
@@ -624,12 +668,38 @@ async function confirmPublishModal() {
624
668
  return;
625
669
  }
626
670
 
671
+ const selectedPlaylists = ids
672
+ .map((id) => playlistsCatalog.find((p) => p.id === id))
673
+ .filter(Boolean);
674
+
675
+ for (const pl of selectedPlaylists) {
676
+ if (!(pl.items || []).length) {
677
+ alert(`Playlist "${pl.name || pl.id}" has no assets. Save the playlist first.`);
678
+ return;
679
+ }
680
+ }
681
+
682
+ const mediaBaseUrl = getPublishMediaBaseUrl(selectedPlaylists);
683
+ if (mediaBaseUrl === "") {
684
+ alert(
685
+ "Local CMS: save a LAN URL under “CMS URL for screens” (e.g. http://192.168.1.105:3000) so TVs can load /uploads paths. On Replit/hosted CMS this field is not required."
686
+ );
687
+ return;
688
+ }
689
+
690
+ const device = devicesCache.find((d) => d.deviceId === publishModalDeviceId);
691
+ const alreadyOnDevice = (device?.publishedPlaylists || []).map((p) => p.playlistId);
692
+ const playlistIds = [...new Set([...alreadyOnDevice, ...ids])];
693
+
694
+ const publishBody = { playlistIds };
695
+ if (mediaBaseUrl) publishBody.mediaBaseUrl = mediaBaseUrl;
696
+
627
697
  const res = await fetch(
628
698
  `/device/${encodeURIComponent(publishModalDeviceId)}/assignments`,
629
699
  {
630
700
  method: "POST",
631
701
  headers: { "Content-Type": "application/json" },
632
- body: JSON.stringify({ playlistIds: ids })
702
+ body: JSON.stringify(publishBody)
633
703
  }
634
704
  );
635
705
  const data = await res.json();
@@ -743,12 +813,14 @@ function startDevicePolling() {
743
813
  }
744
814
 
745
815
  document.addEventListener("DOMContentLoaded", () => {
816
+ const cmsUrlSection = document.getElementById("cmsUrlSection");
817
+ if (cmsUrlSection && !isLocalPanelHost(window.location.hostname)) {
818
+ cmsUrlSection.classList.add("hidden");
819
+ }
820
+
746
821
  const savedMediaBase = localStorage.getItem(PANEL_MEDIA_BASE_KEY);
747
822
  const cmsBaseInput = document.getElementById("cmsDeviceBaseUrl");
748
823
  if (savedMediaBase && cmsBaseInput) cmsBaseInput.value = savedMediaBase;
749
- else if (cmsBaseInput && !isLocalPanelHost(window.location.hostname)) {
750
- cmsBaseInput.value = window.location.origin;
751
- }
752
824
 
753
825
  void fetchPlaylists();
754
826
  startDevicePolling();