@tomorrowos/sdk 1.1.5 → 1.1.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.
Files changed (37) hide show
  1. package/dist/index.d.ts +5 -1
  2. package/dist/index.d.ts.map +1 -1
  3. package/dist/index.js +2 -0
  4. package/dist/playlist-catalog.d.ts +5 -0
  5. package/dist/playlist-catalog.d.ts.map +1 -1
  6. package/dist/playlist-catalog.js +22 -1
  7. package/dist/screen-references.d.ts +37 -0
  8. package/dist/screen-references.d.ts.map +1 -0
  9. package/dist/screen-references.js +228 -0
  10. package/dist/screen-tags.d.ts +95 -0
  11. package/dist/screen-tags.d.ts.map +1 -0
  12. package/dist/screen-tags.js +835 -0
  13. package/dist/store/memory-store.d.ts +2 -0
  14. package/dist/store/memory-store.d.ts.map +1 -1
  15. package/dist/store/memory-store.js +11 -0
  16. package/dist/store/postgres-store.d.ts +2 -0
  17. package/dist/store/postgres-store.d.ts.map +1 -1
  18. package/dist/store/postgres-store.js +9 -0
  19. package/dist/store/sqlite-store.d.ts +2 -0
  20. package/dist/store/sqlite-store.d.ts.map +1 -1
  21. package/dist/store/sqlite-store.js +12 -0
  22. package/dist/store/types.d.ts +20 -0
  23. package/dist/store/types.d.ts.map +1 -1
  24. package/dist/tomorrowos.d.ts +7 -0
  25. package/dist/tomorrowos.d.ts.map +1 -1
  26. package/dist/tomorrowos.js +56 -1
  27. package/package.json +1 -1
  28. package/templates/cms-starter/package-lock.json +3 -3
  29. package/templates/cms-starter/package.json +2 -2
  30. package/templates/cms-starter/public/index.html +86 -1
  31. package/templates/cms-starter/public/methods.js +782 -11
  32. package/templates/cms-starter/public/panel.css +356 -0
  33. package/templates/cms-starter-v0/cms-panel/index.html +86 -1
  34. package/templates/cms-starter-v0/cms-panel/methods.js +782 -11
  35. package/templates/cms-starter-v0/cms-panel/panel.css +356 -0
  36. package/templates/cms-starter-v0/package-lock.json +3 -3
  37. package/templates/cms-starter-v0/package.json +2 -2
package/dist/index.d.ts CHANGED
@@ -3,8 +3,12 @@ export type { DeviceListItem, DeviceScreenshotInfo, ListenOptions, TomorrowOSBra
3
3
  export type { ConnectorState, ConnectorStatus, ServerStatusReport, StatusBlocker } from "./server-status.js";
4
4
  export { buildServerStatus } from "./server-status.js";
5
5
  export { getSdkPackageVersion } from "./sdk-version.js";
6
- export type { DeviceOnOffTimer, DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, DeviceRegistryEntry, PlaylistItemRecord, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSDataSnapshot, TomorrowOSMigratableStore, TomorrowOSStore } from "./store/types.js";
6
+ export type { DeviceOnOffTimer, DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, DeviceRegistryEntry, PlaylistItemRecord, PlaylistItemSchedule, PlaylistItemTagRule, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSDataSnapshot, TomorrowOSMigratableStore, TomorrowOSStore } from "./store/types.js";
7
7
  export { PlaylistCatalog } from "./playlist-catalog.js";
8
+ export { filterPolicyForScreen, itemMatchesScreen, itemScheduleActive, parseItemSchedule, parseTagRule, ScreenTagStore } from "./screen-tags.js";
9
+ export { parseScreenReferences, policyWithScreenReferences, ScreenReferenceStore } from "./screen-references.js";
10
+ export type { ScreenReferenceHost, ScreenReferences } from "./screen-references.js";
11
+ export type { ItemSchedule, ScreenTagHost, ScreenTagHttpDeps, TagRule, TagRuleOp } from "./screen-tags.js";
8
12
  export type { BuiltDevicePolicy, SavePlaylistInput } from "./playlist-catalog.js";
9
13
  export { DEFAULT_PAIRING_CODE_SECRET, generateDeterministicPairingCode, generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH, resolvePairingCodeSecret } from "./pairing-code.js";
10
14
  export { MemoryStore } from "./store/memory-store.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,YAAY,EACV,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,2BAA2B,EAC3B,gCAAgC,EAChC,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AACvD,OAAO,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAExD,YAAY,EACV,gBAAgB,EAChB,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,oBAAoB,EACpB,mBAAmB,EACnB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,YAAY,EACZ,cAAc,EACf,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,qBAAqB,EACrB,0BAA0B,EAC1B,oBAAoB,EACrB,MAAM,wBAAwB,CAAC;AAChC,YAAY,EAAE,mBAAmB,EAAE,gBAAgB,EAAE,MAAM,wBAAwB,CAAC;AACpF,YAAY,EACV,YAAY,EACZ,aAAa,EACb,iBAAiB,EACjB,OAAO,EACP,SAAS,EACV,MAAM,kBAAkB,CAAC;AAC1B,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,2BAA2B,EAC3B,gCAAgC,EAChC,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -2,6 +2,8 @@ export { TomorrowOS } from "./tomorrowos.js";
2
2
  export { buildServerStatus } from "./server-status.js";
3
3
  export { getSdkPackageVersion } from "./sdk-version.js";
4
4
  export { PlaylistCatalog } from "./playlist-catalog.js";
5
+ export { filterPolicyForScreen, itemMatchesScreen, itemScheduleActive, parseItemSchedule, parseTagRule, ScreenTagStore } from "./screen-tags.js";
6
+ export { parseScreenReferences, policyWithScreenReferences, ScreenReferenceStore } from "./screen-references.js";
5
7
  export { DEFAULT_PAIRING_CODE_SECRET, generateDeterministicPairingCode, generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH, resolvePairingCodeSecret } from "./pairing-code.js";
6
8
  export { MemoryStore } from "./store/memory-store.js";
7
9
  export { PostgresStore } from "./store/postgres-store.js";
@@ -29,6 +29,11 @@ export declare class PlaylistCatalog {
29
29
  removePlaylistFromDevice(deviceId: string, playlistId: string): Promise<BuiltDevicePolicy>;
30
30
  /** Remove every playlist assignment from a device (CMS catalog + empty policy). */
31
31
  clearAllAssignmentsFromDevice(deviceId: string): Promise<BuiltDevicePolicy>;
32
+ /**
33
+ * A retired playlist stays only while some device still has its published snapshot.
34
+ * Clear (or removing it from the last screen) drops that snapshot, so the row goes too.
35
+ */
36
+ private deleteRetiredPlaylistsWithoutAssignments;
32
37
  /**
33
38
  * If the device has this playlist assigned, refresh its snapshot to latest
34
39
  * and return a rebuilt snapshot policy for immediate push.
@@ -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,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;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,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;IAkBnD,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;IAQ7B,mFAAmF;IAC7E,6BAA6B,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,CAAC;IASjF;;;OAGG;YACW,wCAAwC;IAStD;;;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;IA+BxC,6BAA6B,CAAC,UAAU,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;CAGpE"}
@@ -100,7 +100,11 @@ export class PlaylistCatalog {
100
100
  retired: true,
101
101
  retiredAt: new Date().toISOString()
102
102
  };
103
- await this.store.setPlaylist(record);
103
+ if (await this.store.isPlaylistAssigned(id)) {
104
+ await this.store.setPlaylist(record);
105
+ return record;
106
+ }
107
+ await this.store.deletePlaylist(id);
104
108
  return record;
105
109
  }
106
110
  async getDeviceAssignments(deviceId) {
@@ -161,13 +165,30 @@ export class PlaylistCatalog {
161
165
  const assignments = await this.store.getDeviceAssignments(deviceId);
162
166
  const next = assignments.filter((a) => a.playlistId !== playlistId);
163
167
  await this.store.setDeviceAssignments(deviceId, next);
168
+ await this.deleteRetiredPlaylistsWithoutAssignments([playlistId]);
164
169
  return this.buildPolicyFromAssignments(next, { useLatest: false });
165
170
  }
166
171
  /** Remove every playlist assignment from a device (CMS catalog + empty policy). */
167
172
  async clearAllAssignmentsFromDevice(deviceId) {
173
+ const removedIds = (await this.store.getDeviceAssignments(deviceId)).map((assignment) => assignment.playlistId);
168
174
  await this.store.setDeviceAssignments(deviceId, []);
175
+ await this.deleteRetiredPlaylistsWithoutAssignments(removedIds);
169
176
  return this.buildPolicyFromAssignments([], { useLatest: false });
170
177
  }
178
+ /**
179
+ * A retired playlist stays only while some device still has its published snapshot.
180
+ * Clear (or removing it from the last screen) drops that snapshot, so the row goes too.
181
+ */
182
+ async deleteRetiredPlaylistsWithoutAssignments(playlistIds) {
183
+ for (const playlistId of new Set(playlistIds)) {
184
+ const playlist = await this.store.getPlaylist(playlistId);
185
+ if (!playlist?.retired)
186
+ continue;
187
+ if (await this.store.isPlaylistAssigned(playlistId))
188
+ continue;
189
+ await this.store.deletePlaylist(playlistId);
190
+ }
191
+ }
171
192
  /**
172
193
  * If the device has this playlist assigned, refresh its snapshot to latest
173
194
  * and return a rebuilt snapshot policy for immediate push.
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Per-screen key/value references.
3
+ *
4
+ * Stored in screen-references.json beside the database. Each setPolicy for a
5
+ * screen includes that screen's references. The shared playlist is unchanged.
6
+ * The default CMS panel does not edit these; a product UI calls the HTTP routes.
7
+ */
8
+ import type { IncomingMessage, ServerResponse } from "http";
9
+ export type ScreenReferences = Record<string, string>;
10
+ export interface ScreenReferenceHost {
11
+ pushLatestPolicyToDevice(deviceId: string): Promise<{
12
+ pushed: boolean;
13
+ }>;
14
+ }
15
+ export declare function parseScreenReferences(raw: unknown): ScreenReferences;
16
+ /** Copy a policy and add this screen's references. Does not mutate the input. */
17
+ export declare function policyWithScreenReferences(policy: unknown, references: ScreenReferences): unknown;
18
+ export declare class ScreenReferenceStore {
19
+ private readonly filePath;
20
+ constructor(filePath: string);
21
+ get(deviceId: string): Promise<ScreenReferences>;
22
+ all(): Promise<Record<string, ScreenReferences>>;
23
+ replace(deviceId: string, references: ScreenReferences): Promise<ScreenReferences>;
24
+ removeKey(deviceId: string, key: string): Promise<ScreenReferences>;
25
+ deleteDevice(deviceId: string): Promise<void>;
26
+ private read;
27
+ private write;
28
+ }
29
+ export interface ScreenReferenceHttpDeps {
30
+ host: ScreenReferenceHost;
31
+ store: {
32
+ getPairedDevice(deviceId: string): Promise<unknown>;
33
+ };
34
+ references: ScreenReferenceStore;
35
+ }
36
+ export declare function handleScreenReferenceHttp(req: IncomingMessage, res: ServerResponse, deps: ScreenReferenceHttpDeps): Promise<boolean>;
37
+ //# sourceMappingURL=screen-references.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screen-references.d.ts","sourceRoot":"","sources":["../src/screen-references.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAGH,OAAO,KAAK,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,MAAM,CAAC;AAO5D,MAAM,MAAM,gBAAgB,GAAG,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;AAMtD,MAAM,WAAW,mBAAmB;IAClC,wBAAwB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC;QAAE,MAAM,EAAE,OAAO,CAAA;KAAE,CAAC,CAAC;CAC1E;AAED,wBAAgB,qBAAqB,CAAC,GAAG,EAAE,OAAO,GAAG,gBAAgB,CA0BpE;AAED,iFAAiF;AACjF,wBAAgB,0BAA0B,CACxC,MAAM,EAAE,OAAO,EACf,UAAU,EAAE,gBAAgB,GAC3B,OAAO,CAMT;AAED,qBAAa,oBAAoB;IACnB,OAAO,CAAC,QAAQ,CAAC,QAAQ;gBAAR,QAAQ,EAAE,MAAM;IAEvC,GAAG,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAKhD,GAAG,IAAI,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,gBAAgB,CAAC,CAAC;IAShD,OAAO,CAAC,QAAQ,EAAE,MAAM,EAAE,UAAU,EAAE,gBAAgB,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAQlF,SAAS,CAAC,QAAQ,EAAE,MAAM,EAAE,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,gBAAgB,CAAC;IAUnE,YAAY,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;YAOrC,IAAI;YAuBJ,KAAK;CAMpB;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,mBAAmB,CAAC;IAC1B,KAAK,EAAE;QAAE,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC,CAAA;KAAE,CAAC;IAC/D,UAAU,EAAE,oBAAoB,CAAC;CAClC;AAyDD,wBAAsB,yBAAyB,CAC7C,GAAG,EAAE,eAAe,EACpB,GAAG,EAAE,cAAc,EACnB,IAAI,EAAE,uBAAuB,GAC5B,OAAO,CAAC,OAAO,CAAC,CA4DlB"}
@@ -0,0 +1,228 @@
1
+ /**
2
+ * Per-screen key/value references.
3
+ *
4
+ * Stored in screen-references.json beside the database. Each setPolicy for a
5
+ * screen includes that screen's references. The shared playlist is unchanged.
6
+ * The default CMS panel does not edit these; a product UI calls the HTTP routes.
7
+ */
8
+ import { mkdir, readFile, rename, writeFile } from "fs/promises";
9
+ import { dirname } from "path";
10
+ const KEY_PATTERN = /^[A-Za-z][A-Za-z0-9_]{0,63}$/;
11
+ const MAX_KEYS = 32;
12
+ const MAX_VALUE_LENGTH = 500;
13
+ export function parseScreenReferences(raw) {
14
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
15
+ throw new Error("references must be an object of string values");
16
+ }
17
+ const entries = Object.entries(raw);
18
+ if (entries.length > MAX_KEYS) {
19
+ throw new Error(`At most ${MAX_KEYS} references`);
20
+ }
21
+ const out = {};
22
+ for (const [key, value] of entries) {
23
+ const name = key.trim();
24
+ if (!KEY_PATTERN.test(name)) {
25
+ throw new Error(`Invalid reference key “${key}”. Use a letter, then letters, numbers, or underscores.`);
26
+ }
27
+ if (typeof value !== "string") {
28
+ throw new Error(`Reference “${name}” must be a string`);
29
+ }
30
+ const text = value.trim();
31
+ if (!text || text.length > MAX_VALUE_LENGTH) {
32
+ throw new Error(`Reference “${name}” must be 1–${MAX_VALUE_LENGTH} characters`);
33
+ }
34
+ out[name] = text;
35
+ }
36
+ return out;
37
+ }
38
+ /** Copy a policy and add this screen's references. Does not mutate the input. */
39
+ export function policyWithScreenReferences(policy, references) {
40
+ if (!policy || typeof policy !== "object" || Array.isArray(policy))
41
+ return policy;
42
+ return {
43
+ ...policy,
44
+ references: { ...references }
45
+ };
46
+ }
47
+ export class ScreenReferenceStore {
48
+ filePath;
49
+ constructor(filePath) {
50
+ this.filePath = filePath;
51
+ }
52
+ async get(deviceId) {
53
+ const file = await this.read();
54
+ return { ...(file.devices[deviceId] ?? {}) };
55
+ }
56
+ async all() {
57
+ const file = await this.read();
58
+ const out = {};
59
+ for (const [deviceId, references] of Object.entries(file.devices)) {
60
+ out[deviceId] = { ...references };
61
+ }
62
+ return out;
63
+ }
64
+ async replace(deviceId, references) {
65
+ const file = await this.read();
66
+ if (Object.keys(references).length === 0)
67
+ delete file.devices[deviceId];
68
+ else
69
+ file.devices[deviceId] = references;
70
+ await this.write(file);
71
+ return { ...references };
72
+ }
73
+ async removeKey(deviceId, key) {
74
+ const file = await this.read();
75
+ const current = { ...(file.devices[deviceId] ?? {}) };
76
+ delete current[key];
77
+ if (Object.keys(current).length === 0)
78
+ delete file.devices[deviceId];
79
+ else
80
+ file.devices[deviceId] = current;
81
+ await this.write(file);
82
+ return current;
83
+ }
84
+ async deleteDevice(deviceId) {
85
+ const file = await this.read();
86
+ if (!file.devices[deviceId])
87
+ return;
88
+ delete file.devices[deviceId];
89
+ await this.write(file);
90
+ }
91
+ async read() {
92
+ try {
93
+ const raw = JSON.parse(await readFile(this.filePath, "utf8"));
94
+ const devices = {};
95
+ if (raw?.devices && typeof raw.devices === "object") {
96
+ for (const [deviceId, references] of Object.entries(raw.devices)) {
97
+ try {
98
+ devices[deviceId] = parseScreenReferences(references);
99
+ }
100
+ catch {
101
+ devices[deviceId] = {};
102
+ }
103
+ }
104
+ }
105
+ return { devices };
106
+ }
107
+ catch (err) {
108
+ const code = err.code;
109
+ if (code === "ENOENT")
110
+ return { devices: {} };
111
+ throw err;
112
+ }
113
+ }
114
+ async write(file) {
115
+ await mkdir(dirname(this.filePath), { recursive: true });
116
+ const tmp = `${this.filePath}.${process.pid}.tmp`;
117
+ await writeFile(tmp, JSON.stringify(file, null, 2), "utf8");
118
+ await rename(tmp, this.filePath);
119
+ }
120
+ }
121
+ function sendJson(res, status, body) {
122
+ res.writeHead(status, {
123
+ "Content-Type": "application/json; charset=utf-8",
124
+ "Cache-Control": "no-store"
125
+ });
126
+ res.end(JSON.stringify(body));
127
+ }
128
+ function pathnameOf(req) {
129
+ const url = new URL(req.url || "/", "http://localhost");
130
+ let pathname = url.pathname || "/";
131
+ if (pathname.length > 1 && pathname.endsWith("/"))
132
+ pathname = pathname.slice(0, -1);
133
+ return pathname;
134
+ }
135
+ async function readJson(req) {
136
+ const chunks = [];
137
+ let size = 0;
138
+ for await (const chunk of req) {
139
+ const buf = Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk);
140
+ size += buf.length;
141
+ if (size > 1_000_000)
142
+ throw new Error("Body too large");
143
+ chunks.push(buf);
144
+ }
145
+ if (chunks.length === 0)
146
+ return {};
147
+ const parsed = JSON.parse(Buffer.concat(chunks).toString("utf8"));
148
+ if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
149
+ throw new Error("JSON object required");
150
+ }
151
+ return parsed;
152
+ }
153
+ async function requirePaired(deps, deviceId, res) {
154
+ const paired = await deps.store.getPairedDevice(deviceId);
155
+ if (paired)
156
+ return true;
157
+ sendJson(res, 404, { status: "failed", error: "Device not found" });
158
+ return false;
159
+ }
160
+ async function pushReferences(deps, deviceId) {
161
+ try {
162
+ const result = await deps.host.pushLatestPolicyToDevice(deviceId);
163
+ return result.pushed;
164
+ }
165
+ catch {
166
+ return false;
167
+ }
168
+ }
169
+ export async function handleScreenReferenceHttp(req, res, deps) {
170
+ const method = String(req.method || "GET").toUpperCase();
171
+ const pathname = pathnameOf(req);
172
+ if (method === "GET" && pathname === "/screen-references") {
173
+ sendJson(res, 200, {
174
+ status: "success",
175
+ referencesByDevice: await deps.references.all()
176
+ });
177
+ return true;
178
+ }
179
+ const collection = /^\/device\/([^/]+)\/references$/.exec(pathname);
180
+ if (collection && method === "GET") {
181
+ const deviceId = decodeURIComponent(collection[1]);
182
+ if (!(await requirePaired(deps, deviceId, res)))
183
+ return true;
184
+ sendJson(res, 200, {
185
+ status: "success",
186
+ deviceId,
187
+ references: await deps.references.get(deviceId)
188
+ });
189
+ return true;
190
+ }
191
+ if (collection && method === "PUT") {
192
+ const deviceId = decodeURIComponent(collection[1]);
193
+ if (!(await requirePaired(deps, deviceId, res)))
194
+ return true;
195
+ let references;
196
+ try {
197
+ const body = await readJson(req);
198
+ references = parseScreenReferences(body.references);
199
+ }
200
+ catch (err) {
201
+ sendJson(res, 400, {
202
+ status: "failed",
203
+ error: err instanceof Error ? err.message : "Invalid references"
204
+ });
205
+ return true;
206
+ }
207
+ const saved = await deps.references.replace(deviceId, references);
208
+ const pushed = await pushReferences(deps, deviceId);
209
+ sendJson(res, 200, { status: "success", deviceId, references: saved, pushed });
210
+ return true;
211
+ }
212
+ const oneKey = /^\/device\/([^/]+)\/references\/([^/]+)$/.exec(pathname);
213
+ if (oneKey && method === "DELETE") {
214
+ const deviceId = decodeURIComponent(oneKey[1]);
215
+ const key = decodeURIComponent(oneKey[2]);
216
+ if (!KEY_PATTERN.test(key)) {
217
+ sendJson(res, 400, { status: "failed", error: "Invalid reference key" });
218
+ return true;
219
+ }
220
+ if (!(await requirePaired(deps, deviceId, res)))
221
+ return true;
222
+ const references = await deps.references.removeKey(deviceId, key);
223
+ const pushed = await pushReferences(deps, deviceId);
224
+ sendJson(res, 200, { status: "success", deviceId, references, pushed });
225
+ return true;
226
+ }
227
+ return false;
228
+ }
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Screen tags and per-item schedules.
3
+ *
4
+ * Screens store tags. Each playlist item may store a tagRule and a schedule.
5
+ * When a policy is pushed, items that do not match that screen right now are
6
+ * removed. Items with no rule play on every screen. Items with no schedule
7
+ * play all the time. tagRule and schedule are stripped before the player sees
8
+ * the policy.
9
+ *
10
+ * Screen tags live in screen-tags.json beside the SQLite database (or
11
+ * data/screen-tags.json). Playlist item rules ride along in items JSON.
12
+ */
13
+ import type { IncomingMessage, ServerResponse } from "http";
14
+ import type { TomorrowOSStore } from "./store/types.js";
15
+ export interface ScreenTagHost {
16
+ listDevices(): Promise<Array<{
17
+ deviceId: string;
18
+ deviceName?: string | null;
19
+ connected: boolean;
20
+ }>>;
21
+ pushLatestPolicyToDevice(deviceId: string): Promise<{
22
+ pushed: boolean;
23
+ }>;
24
+ playlists: {
25
+ buildPolicyForDevice(deviceId: string, options: {
26
+ useLatest?: boolean;
27
+ }): Promise<{
28
+ policy: unknown;
29
+ }>;
30
+ };
31
+ }
32
+ export type TagRuleOp = "any" | "all" | "notAny" | "notAll";
33
+ export interface TagRule {
34
+ op: TagRuleOp;
35
+ tags: string[];
36
+ }
37
+ export declare function normalizeTag(raw: unknown): string | null;
38
+ export declare function normalizeTagList(raw: unknown): string[];
39
+ /**
40
+ * Monday = 0 … Sunday = 6.
41
+ * [true, true, true, true, true, false, false] is Mon–Fri and does not play Sat or Sun.
42
+ * [false, false, false, false, false, true, true] is Sat and Sun.
43
+ */
44
+ export declare function weekdayIndexMondayFirst(date: Date): number;
45
+ export interface ItemSchedule {
46
+ startDate?: string;
47
+ endDate?: string;
48
+ startTime?: string;
49
+ endTime?: string;
50
+ days?: boolean[];
51
+ }
52
+ /** Missing or empty schedule means the item plays all the time. */
53
+ export declare function parseItemSchedule(raw: unknown): ItemSchedule | null;
54
+ /**
55
+ * `now` is interpreted in its local timezone. End times are exclusive.
56
+ * A window whose end is earlier than its start continues past midnight,
57
+ * and that tail still belongs to the day the window started.
58
+ */
59
+ export declare function itemScheduleActive(raw: unknown, now?: Date): boolean;
60
+ /** Missing, empty, or invalid rules mean the item plays on every screen. */
61
+ export declare function parseTagRule(raw: unknown): TagRule | null;
62
+ export declare function itemMatchesScreen(screenTags: readonly string[], item: unknown, now?: Date): boolean;
63
+ /** Returns a new policy. Does not mutate the SDK snapshot objects. */
64
+ export declare function filterPolicyForScreen(policy: unknown, screenTags: readonly string[], now?: Date): unknown;
65
+ /**
66
+ * Re-push a screen when a schedule window opens or closes.
67
+ * Current players ignore item schedules, so the CMS removes inactive items
68
+ * before the policy is sent. The check uses this computer's local clock.
69
+ */
70
+ export declare function startItemScheduleRefresher(deps: ScreenTagHttpDeps): void;
71
+ export declare class ScreenTagStore {
72
+ private readonly filePath;
73
+ private chain;
74
+ constructor(filePath: string);
75
+ get(deviceId: string): Promise<string[]>;
76
+ all(): Promise<Record<string, string[]>>;
77
+ catalog(): Promise<string[]>;
78
+ remember(tag: string): Promise<string[]>;
79
+ rename(from: string, to: string): Promise<string[]>;
80
+ forget(tag: string): Promise<void>;
81
+ add(deviceId: string, tag: string): Promise<string[]>;
82
+ remove(deviceId: string, tag: string): Promise<string[]>;
83
+ deleteDevice(deviceId: string): Promise<void>;
84
+ private rememberOn;
85
+ private enqueue;
86
+ private read;
87
+ private write;
88
+ }
89
+ export interface ScreenTagHttpDeps {
90
+ host: ScreenTagHost;
91
+ store: TomorrowOSStore;
92
+ tags: ScreenTagStore;
93
+ }
94
+ export declare function handleScreenTagHttp(req: IncomingMessage, res: ServerResponse, deps: ScreenTagHttpDeps): Promise<boolean>;
95
+ //# sourceMappingURL=screen-tags.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"screen-tags.d.ts","sourceRoot":"","sources":["../src/screen-tags.ts"],"names":[],"mappings":"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"}