@tomorrowos/sdk 0.9.2 → 0.9.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.
package/REPLIT_SETUP.md CHANGED
@@ -232,7 +232,7 @@ Allowed extras (optional): `"build-player": "tomorrowos build --platform tizen"`
232
232
  "node": ">=20"
233
233
  },
234
234
  "dependencies": {
235
- "@tomorrowos/sdk": "^0.9.2",
235
+ "@tomorrowos/sdk": "^0.9.4",
236
236
  "dotenv": "^17.2.3",
237
237
  "tsx": "^4.19.0"
238
238
  },
@@ -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;AAED,MAAM,WAAW,kBAAkB;IACjC,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA8CD,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;IA8B/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;IA0CvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAO7B,mFAAmF;IAC7E,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAKjF;;;OAGG;IACG,+BAA+B,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC;IA4BrD,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
+ {"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;AAyDD,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;IA8B/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;IAiEvB,wBAAwB,CAC5B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,iBAAiB,CAAC;IAO7B,mFAAmF;IAC7E,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IAKjF;;;OAGG;IACG,+BAA+B,CACnC,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,OAAO,GAAE,kBAAuB,GAC/B,OAAO,CAAC;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,KAAK,CAAC,EAAE,iBAAiB,CAAA;KAAE,CAAC;IA4BrD,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"}
@@ -40,6 +40,15 @@ function cloneSnapshot(playlist, mediaBaseUrl) {
40
40
  items: absolutizePlaylistItems(playlist.items, mediaBaseUrl)
41
41
  };
42
42
  }
43
+ /** True when the playlist has no start/end date or time (always-on / default content). */
44
+ function isUnscheduledPlaylist(schedule) {
45
+ if (!schedule)
46
+ return true;
47
+ return !(schedule.startDate ||
48
+ schedule.endDate ||
49
+ schedule.start ||
50
+ schedule.end);
51
+ }
43
52
  export class PlaylistCatalog {
44
53
  store;
45
54
  constructor(store) {
@@ -106,25 +115,40 @@ export class PlaylistCatalog {
106
115
  }
107
116
  const existing = await this.store.getDeviceAssignments(deviceId);
108
117
  const existingById = new Map(existing.map((a) => [a.playlistId, a]));
118
+ // Resolve incoming playlists first so we know whether this publish includes
119
+ // an always-on (no start/end) playlist that should override prior always-on ones.
120
+ const incomingSnapshots = new Map();
121
+ for (const playlistId of incoming) {
122
+ const playlist = await this.store.getPlaylist(playlistId);
123
+ if (!playlist || playlist.retired) {
124
+ throw Object.assign(new Error(`Playlist not available: ${playlistId}`), {
125
+ code: "PLAYLIST_NOT_FOUND"
126
+ });
127
+ }
128
+ incomingSnapshots.set(playlistId, cloneSnapshot(playlist, options.mediaBaseUrl));
129
+ }
130
+ const incomingHasUnscheduled = [...incomingSnapshots.values()].some((snap) => isUnscheduledPlaylist(snap.schedule));
109
131
  const allIds = [...new Set([...existing.map((a) => a.playlistId), ...incoming])];
132
+ const publishedAt = new Date().toISOString();
110
133
  const assignments = [];
111
134
  for (const playlistId of allIds) {
112
135
  if (!incoming.includes(playlistId)) {
113
136
  const kept = existingById.get(playlistId);
114
- if (kept)
115
- assignments.push(kept);
137
+ if (!kept)
138
+ continue;
139
+ // Later publish of an unscheduled playlist replaces earlier unscheduled ones.
140
+ // Scheduled (dated) assignments are preserved.
141
+ if (incomingHasUnscheduled &&
142
+ isUnscheduledPlaylist(kept.snapshot.schedule)) {
143
+ continue;
144
+ }
145
+ assignments.push(kept);
116
146
  continue;
117
147
  }
118
- const playlist = await this.store.getPlaylist(playlistId);
119
- if (!playlist || playlist.retired) {
120
- throw Object.assign(new Error(`Playlist not available: ${playlistId}`), {
121
- code: "PLAYLIST_NOT_FOUND"
122
- });
123
- }
124
148
  assignments.push({
125
149
  playlistId,
126
- publishedAt: new Date().toISOString(),
127
- snapshot: cloneSnapshot(playlist, options.mediaBaseUrl)
150
+ publishedAt,
151
+ snapshot: incomingSnapshots.get(playlistId)
128
152
  });
129
153
  }
130
154
  await this.store.setDeviceAssignments(deviceId, assignments);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.9.2",
3
+ "version": "0.9.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.9.2",
3
+ "version": "0.9.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",
@@ -13,7 +13,7 @@
13
13
  "build-player": "tomorrowos build --platform tizen"
14
14
  },
15
15
  "dependencies": {
16
- "@tomorrowos/sdk": "^0.9.2",
16
+ "@tomorrowos/sdk": "^0.9.4",
17
17
  "dotenv": "^17.2.3",
18
18
  "tsx": "^4.19.0"
19
19
  },
@@ -566,21 +566,32 @@ function parseScheduleDateTimeMs(dateStr, timeStr, defaultTime) {
566
566
  return Number.isNaN(ms) ? null : ms;
567
567
  }
568
568
 
569
+ /** True when the playlist has no start/end date or time (always-on / default content). */
570
+ function isUnscheduledPublishedPlaylist(playlist) {
571
+ const schedule = playlist?.schedule;
572
+ if (!schedule) return true;
573
+ return !(
574
+ schedule.startDate ||
575
+ schedule.endDate ||
576
+ schedule.start ||
577
+ schedule.end
578
+ );
579
+ }
580
+
569
581
  function getPublishedPlaylistStartMs(playlist) {
570
582
  const schedule = playlist?.schedule;
571
- if (!schedule) return null;
583
+ if (isUnscheduledPublishedPlaylist(playlist) || !schedule) return null;
572
584
  return parseScheduleDateTimeMs(schedule.startDate, schedule.start, "00:00");
573
585
  }
574
586
 
575
587
  function getPublishedPlaylistEndMs(playlist) {
576
588
  const schedule = playlist?.schedule;
577
- if (!schedule) return null;
589
+ if (isUnscheduledPublishedPlaylist(playlist) || !schedule) return null;
578
590
  return parseScheduleDateTimeMs(schedule.endDate, schedule.end, "23:59");
579
591
  }
580
592
 
581
593
  function isPublishedPlaylistActiveNow(playlist, now = new Date()) {
582
- const schedule = playlist?.schedule;
583
- if (!schedule) return true;
594
+ if (isUnscheduledPublishedPlaylist(playlist)) return true;
584
595
  const nowMs = now.getTime();
585
596
  const startMs = getPublishedPlaylistStartMs(playlist);
586
597
  const endMs = getPublishedPlaylistEndMs(playlist);
@@ -589,24 +600,39 @@ function isPublishedPlaylistActiveNow(playlist, now = new Date()) {
589
600
  return true;
590
601
  }
591
602
 
603
+ /**
604
+ * Green-light target for the device card.
605
+ * - Prefer an in-window scheduled playlist (latest start wins).
606
+ * - Else among always-on (no schedule) playlists, prefer the latest publishedAt
607
+ * so a later publish overrides earlier ones in the activity indicator.
608
+ */
592
609
  function pickScheduledPlaylistForIndicator(playlists, now = new Date()) {
593
- const active = (Array.isArray(playlists) ? playlists : []).filter((playlist) =>
594
- isPublishedPlaylistActiveNow(playlist, now)
595
- );
610
+ const list = Array.isArray(playlists) ? playlists : [];
611
+ const active = list
612
+ .map((playlist, index) => ({ playlist, index }))
613
+ .filter(({ playlist }) => isPublishedPlaylistActiveNow(playlist, now));
596
614
  if (!active.length) return null;
597
615
 
598
- active.sort((a, b) => {
599
- const aStart = getPublishedPlaylistStartMs(a);
600
- const bStart = getPublishedPlaylistStartMs(b);
601
- const aScore = aStart === null ? -Infinity : aStart;
602
- const bScore = bStart === null ? -Infinity : bStart;
603
- if (aScore !== bScore) return bScore - aScore;
604
- const aPublished = new Date(a?.publishedAt || 0).getTime() || 0;
605
- const bPublished = new Date(b?.publishedAt || 0).getTime() || 0;
606
- return bPublished - aPublished;
616
+ const scheduled = active.filter(
617
+ ({ playlist }) => !isUnscheduledPublishedPlaylist(playlist)
618
+ );
619
+ // Match player: scheduled takeovers beat always-on; always-on uses latest publish.
620
+ const pool = scheduled.length ? scheduled : active;
621
+
622
+ pool.sort((a, b) => {
623
+ if (scheduled.length) {
624
+ const aStart = getPublishedPlaylistStartMs(a.playlist) ?? 0;
625
+ const bStart = getPublishedPlaylistStartMs(b.playlist) ?? 0;
626
+ if (aStart !== bStart) return bStart - aStart;
627
+ }
628
+ const aPublished = new Date(a.playlist?.publishedAt || 0).getTime() || 0;
629
+ const bPublished = new Date(b.playlist?.publishedAt || 0).getTime() || 0;
630
+ if (aPublished !== bPublished) return bPublished - aPublished;
631
+ // Same publish time → later entry in the assignments list (last published) wins.
632
+ return b.index - a.index;
607
633
  });
608
634
 
609
- return active[0] || null;
635
+ return pool[0]?.playlist || null;
610
636
  }
611
637
 
612
638
  function getSelectedPlaylist() {
@@ -1151,7 +1177,7 @@ function renderDeviceCards() {
1151
1177
  if (isPlaying) {
1152
1178
  const playingLight = document.createElement("span");
1153
1179
  playingLight.className = "playlist-playing-light";
1154
- playingLight.title = "Active now by schedule";
1180
+ playingLight.title = "Active now";
1155
1181
  label.appendChild(playingLight);
1156
1182
  }
1157
1183
  const rm = document.createElement("button");
@@ -1292,7 +1318,9 @@ function openPublishModal(deviceId) {
1292
1318
  return;
1293
1319
  }
1294
1320
 
1295
- hint.textContent = `Device ${deviceId} — add playlists not yet on this device (snapshot at publish time).`;
1321
+ hint.textContent =
1322
+ `Device ${deviceId} — add playlists not yet on this device (snapshot at publish time). ` +
1323
+ `Publishing a playlist with no start/end date/time replaces any previously published unscheduled playlist on this device.`;
1296
1324
  checklist.innerHTML = "";
1297
1325
 
1298
1326
  for (const pl of unpublished) {