@tomorrowos/sdk 0.2.2 → 0.2.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.
@@ -1,350 +1,350 @@
1
- # Build Guardrails for TomorrowOS CMS Projects
2
-
3
- > **These are not suggestions. Every CMS generated from `@tomorrowos/sdk` must include each component in this document. The SDK assumes they exist. If any are missing, pairing will fail, content will not deploy, and the LLM that generated the CMS has produced broken code.**
4
-
5
- This document exists because an LLM given a blank canvas will produce a CMS that looks right and doesn't work. This file is the fence that stops that. Read each section. Include each component. Do not skip any.
6
-
7
- ---
8
-
9
- ## 1. Server-side WebSocket host
10
-
11
- **File:** `server.ts` (or `server.js`)
12
-
13
- **Must contain:**
14
-
15
- ```typescript
16
- import { TomorrowOS } from '@tomorrowos/sdk';
17
- import brand from './brand.json';
18
-
19
- const tomorrowos = new TomorrowOS({ brand });
20
-
21
- tomorrowos.listen({
22
- port: Number(process.env.PORT) || 3000,
23
- host: '0.0.0.0',
24
- });
25
- ```
26
-
27
- **Why this matters:**
28
-
29
- - `TomorrowOS` is a class from the SDK that wraps WebSocket handling, pairing, command envelopes, lifecycle tracking, and event publishing. Do not reimplement any of this.
30
- - `tomorrowos.listen()` binds a WebSocket server that every player connects to. Without this call, no screen can ever pair.
31
- - `host: '0.0.0.0'` is required for Replit / Claude Code / containerised hosting. Do not use `localhost` or `127.0.0.1` in the starter.
32
-
33
- **Must not contain:**
34
-
35
- - Raw `ws` or `socket.io` imports — the SDK handles WebSocket transport
36
- - Manual pairing code generation — the SDK provides `tomorrowos.pairing.createCode()`
37
- - Manual JWT signing for device tokens — the SDK mints and verifies these
38
-
39
- ---
40
-
41
- ## 2. Pairing page
42
-
43
- **Route:** `/pair`
44
-
45
- **Must include:**
46
-
47
- - An input field accepting a 6-digit numeric code (input pattern `\d{6}`)
48
- - A submit button that calls `tomorrowos.pairing.verify(code)` via the SDK
49
- - Success state showing the newly-paired device's name and ID
50
- - Error state for expired, invalid, or already-paired codes
51
- - A link back to the main screens list
52
-
53
- **Example flow:**
54
-
55
- ```typescript
56
- import { useTomorrowOS } from '@tomorrowos/sdk/react';
57
-
58
- function PairPage() {
59
- const { pairing } = useTomorrowOS();
60
- const [code, setCode] = useState('');
61
- const [result, setResult] = useState(null);
62
-
63
- async function handleSubmit() {
64
- try {
65
- const device = await pairing.verify(code);
66
- setResult({ success: true, device });
67
- } catch (err) {
68
- setResult({ success: false, error: err.message });
69
- }
70
- }
71
-
72
- return (/* UI here */);
73
- }
74
- ```
75
-
76
- **Why mandatory:**
77
-
78
- Without a pairing page, a device showing an activation code on its screen has no way to tell the CMS "I am device X, please accept me." The CMS will never gain a paired device. Every CMS needs this page regardless of use case.
79
-
80
- ---
81
-
82
- ## 3. Screens list page
83
-
84
- **Route:** `/` or `/screens`
85
-
86
- **Must include:**
87
-
88
- - Live list of paired devices from `tomorrowos.devices.list()`
89
- - Online / offline indicator per device (driven by `device.heartbeat` events)
90
- - Last-seen timestamp per device (from heartbeat stream)
91
- - Device name, model, and platform per row
92
- - Click-through to `/screens/[deviceId]`
93
-
94
- **Real-time updates:**
95
-
96
- The SDK emits `device.online`, `device.offline`, and `device.heartbeat` events. Subscribe to these via `tomorrowos.on('device.online', handler)` to keep the list live without polling.
97
-
98
- **Why mandatory:**
99
-
100
- This is the operator's primary workspace. Without it, the CMS has paired devices it cannot show.
101
-
102
- ---
103
-
104
- ## 4. Per-device control panel
105
-
106
- **Route:** `/screens/[deviceId]`
107
-
108
- **Must include the following controls, wired to SDK methods:**
109
-
110
- ### Device info panel (top of page)
111
-
112
- Display output of `tomorrowos.device(deviceId).info.get()`:
113
-
114
- - Device name
115
- - Platform and platform version
116
- - Hardware model and serial
117
- - Current resolution
118
- - Paired since date
119
-
120
- ### Control buttons
121
-
122
- Each button calls the corresponding SDK method via `tomorrowos.device(deviceId).sendCommand()`:
123
-
124
- | Label | SDK call | Notes |
125
- |------------------|--------------------------------------------------|------------------------------------------------|
126
- | Reboot | `device.power.reboot()` | Confirm dialog before sending |
127
- | Deploy content | Opens URL input → `device.content.setPolicy()` | Wrap URL as single-item policy; see below |
128
- | Clear content | `device.content.clear()` | Confirm dialog |
129
- | Screenshot | `device.display.screenshot()` | Only show button if capability supports it |
130
-
131
- ### Current content indicator
132
-
133
- Poll `tomorrowos.device(deviceId).content.current()` every 10 seconds, or subscribe to `content.started` / `content.finished` events for push updates.
134
-
135
- ### Deploy content — correct shape
136
-
137
- When the user enters a URL to deploy, the SDK call must wrap it as a content policy:
138
-
139
- ```typescript
140
- await device.sendCommand('device.content.setPolicy', {
141
- policy: {
142
- playlists: [{
143
- id: 'default',
144
- schedule: {
145
- startDate: '2026-05-01', // YYYY-MM-DD, optional
146
- endDate: '2026-05-31',
147
- daysOfWeek: [1, 2, 3, 4, 5], // 0=Sun … 6=Sat, optional
148
- start: '09:00', // HH:MM daily window, optional
149
- end: '17:00',
150
- },
151
- items: [{
152
- url: userProvidedUrl,
153
- type: 'image',
154
- durationMs: 30000,
155
- }],
156
- }],
157
- fallback: { type: 'brand' },
158
- },
159
- });
160
- ```
161
-
162
- **Do not call** `device.content.deploy(url)` — this method does not exist in V1. Content is declarative policy, not imperative deploy.
163
-
164
- **Why mandatory:**
165
-
166
- Without the control panel, the CMS is a pretty dashboard with no function. Every TomorrowOS demo, test, and user showcase depends on this page working.
167
-
168
- ---
169
-
170
- ## 5. Brand application via `brand.json`
171
-
172
- **Brand data MUST flow through `brand.json`**, never hard-coded in components.
173
-
174
- ### Read brand at startup
175
-
176
- ```typescript
177
- import brand from './brand.json';
178
- ```
179
-
180
- ### Apply brand via CSS custom properties
181
-
182
- Generate a `brand.css` file (or equivalent) on server start that derives CSS variables from `brand.json`:
183
-
184
- ```css
185
- :root {
186
- --color-primary: #FF8A3D; /* from brand.primaryColor */
187
- --color-background: #FAFAF9; /* from brand.backgroundColor */
188
- --color-text: #0A0908; /* from brand.textColor */
189
- --font-sans: 'Inter', sans-serif; /* from brand.fontFamily */
190
- }
191
- ```
192
-
193
- ### Use the `useBrand()` hook in React components
194
-
195
- ```typescript
196
- import { useBrand } from '@tomorrowos/sdk/react';
197
-
198
- function Header() {
199
- const brand = useBrand();
200
- return (
201
- <header>
202
- <img src={brand.logoPath} alt={brand.name} />
203
- <h1>{brand.name} Signage</h1>
204
- </header>
205
- );
206
- }
207
- ```
208
-
209
- **Must not:**
210
-
211
- - Hard-code the brand name, logo path, or colours in any component
212
- - Use inline `style={{ color: '#FF8A3D' }}` — always use the CSS variable
213
- - Reference the TomorrowOS amber or the starter's placeholder colour in production components
214
-
215
- **Why mandatory:**
216
-
217
- Hard-coded branding means the user has to edit every file to change a colour. `brand.json`-driven branding means one file change propagates everywhere — including into the device's activation screen (baked into the `.wgt` at build time by `tomorrowos build`).
218
-
219
- ---
220
-
221
- ## 6. Command envelope
222
-
223
- **Every mutating command must go through `sdk.sendCommand(method, params)`.**
224
-
225
- The SDK wraps your call with the standard command envelope:
226
-
227
- ```json
228
- {
229
- "commandId": "uuid-generated",
230
- "method": "device.power.reboot",
231
- "params": {},
232
- "issuedAt": "2026-05-14T10:30:00Z",
233
- "ttlSec": 300,
234
- "idempotencyKey": "uuid-generated",
235
- "requiresAck": true
236
- }
237
- ```
238
-
239
- **Must not:**
240
-
241
- - Write to the WebSocket directly. If you find yourself typing `ws.send()` or `socket.emit()` in your CMS code, stop — you are bypassing the SDK and your commands will be rejected by the player.
242
- - Construct your own commandId or idempotency logic. The SDK does this.
243
- - Assume a command is complete when `sendCommand` resolves. `sendCommand` returns when the command is `accepted`. Listen for `command.verified` or `command.failed` events to know the final outcome.
244
-
245
- ---
246
-
247
- ## 7. No platform-specific branching in CMS code
248
-
249
- The CMS must be platform-agnostic. The SDK + capability declarations abstract every platform difference.
250
-
251
- **Do not write:**
252
-
253
- ```typescript
254
- // WRONG
255
- if (device.platform === 'tizen') {
256
- await device.sendCommand('device.power.reboot');
257
- } else if (device.platform === 'brightsign') {
258
- await device.sendCommand('device.power.restart');
259
- }
260
- ```
261
-
262
- **Write:**
263
-
264
- ```typescript
265
- // CORRECT
266
- await device.sendCommand('device.power.reboot');
267
- ```
268
-
269
- If the device doesn't support reboot, the SDK returns a `CAPABILITY_NOT_SUPPORTED` error. Handle that error — do not pre-branch on platform.
270
-
271
- **Do not:**
272
-
273
- - Check `device.platform` or `device.class` to change which methods you call
274
- - Call raw native APIs (there are none exposed to the CMS — only SDK methods)
275
- - Import any `@tomorrowos/player-*` packages into the CMS — those are player-side, not CMS-side
276
-
277
- ---
278
-
279
- ## 8. Event subscription for real-time UI
280
-
281
- Subscribe to events for live dashboards. Do not poll unless explicitly required.
282
-
283
- ```typescript
284
- tomorrowos.on('device.online', (event) => {
285
- // update UI
286
- });
287
-
288
- tomorrowos.on('device.heartbeat', (event) => {
289
- // update last-seen
290
- });
291
-
292
- tomorrowos.on('command.verified', (event) => {
293
- // mark command as successful in UI
294
- });
295
-
296
- tomorrowos.on('command.failed', (event) => {
297
- // show error to operator
298
- });
299
-
300
- tomorrowos.on('content.error', (event) => {
301
- // alert operator of content playback failure
302
- });
303
- ```
304
-
305
- ---
306
-
307
- ## Optional but strongly recommended
308
-
309
- The following are not required but improve the CMS substantially. Include them if the user is at expectedScreens > 10 or if the use case warrants it.
310
-
311
- ### Build player button
312
-
313
- A button in the CMS admin area that runs `npx tomorrowos build --platform tizen` (or other) and links the user to the generated `.wgt` file in `dist/`. Saves the user a terminal step.
314
-
315
- ### Settings page for `brand.json`
316
-
317
- A `/settings` page that lets the user edit brand fields in-UI and writes back to `brand.json`. On save, the CMS reloads the brand.
318
-
319
- ### Fleet broadcast
320
-
321
- A bulk-command UI that uses `tomorrowos.fleet.broadcast(method, params, targets)` to send the same command to many devices at once. Essential at > 50 screens.
322
-
323
- ### Proof-of-play log
324
-
325
- A `/playback` page showing which content played on which screen at what time. Reads from `tomorrowos.device(id).proofOfPlay.getLog()`.
326
-
327
- ---
328
-
329
- ## Summary — the "did I build this right?" checklist
330
-
331
- Before shipping a generated CMS, verify:
332
-
333
- - `server.ts` imports `TomorrowOS` from `@tomorrowos/sdk` and calls `listen()` — ☐
334
- - `/pair` page exists and calls `pairing.verify(code)` — ☐
335
- - `/screens` page shows live device list with online/offline status — ☐
336
- - `/screens/[id]` page has reboot, deploy, clear, current-content at minimum — ☐
337
- - Deploy uses `device.content.setPolicy` with a wrapped policy object, not `content.deploy` — ☐
338
- - `brand.json` exists at project root and drives all UI branding via CSS variables — ☐
339
- - No hard-coded colours, logos, or brand names in components — ☐
340
- - All mutating commands use `sdk.sendCommand()` — no raw WebSocket writes — ☐
341
- - No platform branching in CMS code — ☐
342
- - Event subscriptions for `device.online`, `device.offline`, `device.heartbeat` active — ☐
343
-
344
- If every box is checked, the CMS will work on first connection. If any box is unchecked, pairing or control will fail in ways the user cannot diagnose.
345
-
346
- ---
347
-
348
- ## Protocol compliance
349
-
350
- This guardrails document is version `1.0`, aligned with TomorrowOS wire protocol `1.0`. Future SDK versions may add or strengthen guardrails; they will not relax them without a major-version bump.
1
+ # Build Guardrails for TomorrowOS CMS Projects
2
+
3
+ > **These are not suggestions. Every CMS generated from `@tomorrowos/sdk` must include each component in this document. The SDK assumes they exist. If any are missing, pairing will fail, content will not deploy, and the LLM that generated the CMS has produced broken code.**
4
+
5
+ This document exists because an LLM given a blank canvas will produce a CMS that looks right and doesn't work. This file is the fence that stops that. Read each section. Include each component. Do not skip any.
6
+
7
+ ---
8
+
9
+ ## 1. Server-side WebSocket host
10
+
11
+ **File:** `server.ts` (or `server.js`)
12
+
13
+ **Must contain:**
14
+
15
+ ```typescript
16
+ import { TomorrowOS } from '@tomorrowos/sdk';
17
+ import brand from './brand.json';
18
+
19
+ const tomorrowos = new TomorrowOS({ brand });
20
+
21
+ tomorrowos.listen({
22
+ port: Number(process.env.PORT) || 3000,
23
+ host: '0.0.0.0',
24
+ });
25
+ ```
26
+
27
+ **Why this matters:**
28
+
29
+ - `TomorrowOS` is a class from the SDK that wraps WebSocket handling, pairing, command envelopes, lifecycle tracking, and event publishing. Do not reimplement any of this.
30
+ - `tomorrowos.listen()` binds a WebSocket server that every player connects to. Without this call, no screen can ever pair.
31
+ - `host: '0.0.0.0'` is required for Replit / Claude Code / containerised hosting. Do not use `localhost` or `127.0.0.1` in the starter.
32
+
33
+ **Must not contain:**
34
+
35
+ - Raw `ws` or `socket.io` imports — the SDK handles WebSocket transport
36
+ - Manual pairing code generation — the SDK provides `tomorrowos.pairing.createCode()`
37
+ - Manual JWT signing for device tokens — the SDK mints and verifies these
38
+
39
+ ---
40
+
41
+ ## 2. Pairing page
42
+
43
+ **Route:** `/pair`
44
+
45
+ **Must include:**
46
+
47
+ - An input field accepting a 6-digit numeric code (input pattern `\d{6}`)
48
+ - A submit button that calls `tomorrowos.pairing.verify(code)` via the SDK
49
+ - Success state showing the newly-paired device's name and ID
50
+ - Error state for expired, invalid, or already-paired codes
51
+ - A link back to the main screens list
52
+
53
+ **Example flow:**
54
+
55
+ ```typescript
56
+ import { useTomorrowOS } from '@tomorrowos/sdk/react';
57
+
58
+ function PairPage() {
59
+ const { pairing } = useTomorrowOS();
60
+ const [code, setCode] = useState('');
61
+ const [result, setResult] = useState(null);
62
+
63
+ async function handleSubmit() {
64
+ try {
65
+ const device = await pairing.verify(code);
66
+ setResult({ success: true, device });
67
+ } catch (err) {
68
+ setResult({ success: false, error: err.message });
69
+ }
70
+ }
71
+
72
+ return (/* UI here */);
73
+ }
74
+ ```
75
+
76
+ **Why mandatory:**
77
+
78
+ Without a pairing page, a device showing an activation code on its screen has no way to tell the CMS "I am device X, please accept me." The CMS will never gain a paired device. Every CMS needs this page regardless of use case.
79
+
80
+ ---
81
+
82
+ ## 3. Screens list page
83
+
84
+ **Route:** `/` or `/screens`
85
+
86
+ **Must include:**
87
+
88
+ - Live list of paired devices from `tomorrowos.devices.list()`
89
+ - Online / offline indicator per device (driven by `device.heartbeat` events)
90
+ - Last-seen timestamp per device (from heartbeat stream)
91
+ - Device name, model, and platform per row
92
+ - Click-through to `/screens/[deviceId]`
93
+
94
+ **Real-time updates:**
95
+
96
+ The SDK emits `device.online`, `device.offline`, and `device.heartbeat` events. Subscribe to these via `tomorrowos.on('device.online', handler)` to keep the list live without polling.
97
+
98
+ **Why mandatory:**
99
+
100
+ This is the operator's primary workspace. Without it, the CMS has paired devices it cannot show.
101
+
102
+ ---
103
+
104
+ ## 4. Per-device control panel
105
+
106
+ **Route:** `/screens/[deviceId]`
107
+
108
+ **Must include the following controls, wired to SDK methods:**
109
+
110
+ ### Device info panel (top of page)
111
+
112
+ Display output of `tomorrowos.device(deviceId).info.get()`:
113
+
114
+ - Device name
115
+ - Platform and platform version
116
+ - Hardware model and serial
117
+ - Current resolution
118
+ - Paired since date
119
+
120
+ ### Control buttons
121
+
122
+ Each button calls the corresponding SDK method via `tomorrowos.device(deviceId).sendCommand()`:
123
+
124
+ | Label | SDK call | Notes |
125
+ |------------------|--------------------------------------------------|------------------------------------------------|
126
+ | Reboot | `device.power.reboot()` | Confirm dialog before sending |
127
+ | Deploy content | Opens URL input → `device.content.setPolicy()` | Wrap URL as single-item policy; see below |
128
+ | Clear content | `device.content.clear()` | Confirm dialog |
129
+ | Screenshot | `device.display.screenshot()` | Only show button if capability supports it |
130
+
131
+ ### Current content indicator
132
+
133
+ Poll `tomorrowos.device(deviceId).content.current()` every 10 seconds, or subscribe to `content.started` / `content.finished` events for push updates.
134
+
135
+ ### Deploy content — correct shape
136
+
137
+ When the user enters a URL to deploy, the SDK call must wrap it as a content policy:
138
+
139
+ ```typescript
140
+ await device.sendCommand('device.content.setPolicy', {
141
+ policy: {
142
+ playlists: [{
143
+ id: 'default',
144
+ schedule: {
145
+ startDate: '2026-05-01', // YYYY-MM-DD, optional
146
+ endDate: '2026-05-31',
147
+ daysOfWeek: [1, 2, 3, 4, 5], // 0=Sun … 6=Sat, optional
148
+ start: '09:00', // HH:MM daily window, optional
149
+ end: '17:00',
150
+ },
151
+ items: [{
152
+ url: userProvidedUrl,
153
+ type: 'image',
154
+ durationMs: 30000,
155
+ }],
156
+ }],
157
+ fallback: { type: 'brand' },
158
+ },
159
+ });
160
+ ```
161
+
162
+ **Do not call** `device.content.deploy(url)` — this method does not exist in V1. Content is declarative policy, not imperative deploy.
163
+
164
+ **Why mandatory:**
165
+
166
+ Without the control panel, the CMS is a pretty dashboard with no function. Every TomorrowOS demo, test, and user showcase depends on this page working.
167
+
168
+ ---
169
+
170
+ ## 5. Brand application via `brand.json`
171
+
172
+ **Brand data MUST flow through `brand.json`**, never hard-coded in components.
173
+
174
+ ### Read brand at startup
175
+
176
+ ```typescript
177
+ import brand from './brand.json';
178
+ ```
179
+
180
+ ### Apply brand via CSS custom properties
181
+
182
+ Generate a `brand.css` file (or equivalent) on server start that derives CSS variables from `brand.json`:
183
+
184
+ ```css
185
+ :root {
186
+ --color-primary: #FF8A3D; /* from brand.primaryColor */
187
+ --color-background: #FAFAF9; /* from brand.backgroundColor */
188
+ --color-text: #0A0908; /* from brand.textColor */
189
+ --font-sans: 'Inter', sans-serif; /* from brand.fontFamily */
190
+ }
191
+ ```
192
+
193
+ ### Use the `useBrand()` hook in React components
194
+
195
+ ```typescript
196
+ import { useBrand } from '@tomorrowos/sdk/react';
197
+
198
+ function Header() {
199
+ const brand = useBrand();
200
+ return (
201
+ <header>
202
+ <img src={brand.logoPath} alt={brand.name} />
203
+ <h1>{brand.name} Signage</h1>
204
+ </header>
205
+ );
206
+ }
207
+ ```
208
+
209
+ **Must not:**
210
+
211
+ - Hard-code the brand name, logo path, or colours in any component
212
+ - Use inline `style={{ color: '#FF8A3D' }}` — always use the CSS variable
213
+ - Reference the TomorrowOS amber or the starter's placeholder colour in production components
214
+
215
+ **Why mandatory:**
216
+
217
+ Hard-coded branding means the user has to edit every file to change a colour. `brand.json`-driven branding means one file change propagates everywhere — including into the device's activation screen (baked into the `.wgt` at build time by `tomorrowos build`).
218
+
219
+ ---
220
+
221
+ ## 6. Command envelope
222
+
223
+ **Every mutating command must go through `sdk.sendCommand(method, params)`.**
224
+
225
+ The SDK wraps your call with the standard command envelope:
226
+
227
+ ```json
228
+ {
229
+ "commandId": "uuid-generated",
230
+ "method": "device.power.reboot",
231
+ "params": {},
232
+ "issuedAt": "2026-05-14T10:30:00Z",
233
+ "ttlSec": 300,
234
+ "idempotencyKey": "uuid-generated",
235
+ "requiresAck": true
236
+ }
237
+ ```
238
+
239
+ **Must not:**
240
+
241
+ - Write to the WebSocket directly. If you find yourself typing `ws.send()` or `socket.emit()` in your CMS code, stop — you are bypassing the SDK and your commands will be rejected by the player.
242
+ - Construct your own commandId or idempotency logic. The SDK does this.
243
+ - Assume a command is complete when `sendCommand` resolves. `sendCommand` returns when the command is `accepted`. Listen for `command.verified` or `command.failed` events to know the final outcome.
244
+
245
+ ---
246
+
247
+ ## 7. No platform-specific branching in CMS code
248
+
249
+ The CMS must be platform-agnostic. The SDK + capability declarations abstract every platform difference.
250
+
251
+ **Do not write:**
252
+
253
+ ```typescript
254
+ // WRONG
255
+ if (device.platform === 'tizen') {
256
+ await device.sendCommand('device.power.reboot');
257
+ } else if (device.platform === 'brightsign') {
258
+ await device.sendCommand('device.power.restart');
259
+ }
260
+ ```
261
+
262
+ **Write:**
263
+
264
+ ```typescript
265
+ // CORRECT
266
+ await device.sendCommand('device.power.reboot');
267
+ ```
268
+
269
+ If the device doesn't support reboot, the SDK returns a `CAPABILITY_NOT_SUPPORTED` error. Handle that error — do not pre-branch on platform.
270
+
271
+ **Do not:**
272
+
273
+ - Check `device.platform` or `device.class` to change which methods you call
274
+ - Call raw native APIs (there are none exposed to the CMS — only SDK methods)
275
+ - Import any `@tomorrowos/player-*` packages into the CMS — those are player-side, not CMS-side
276
+
277
+ ---
278
+
279
+ ## 8. Event subscription for real-time UI
280
+
281
+ Subscribe to events for live dashboards. Do not poll unless explicitly required.
282
+
283
+ ```typescript
284
+ tomorrowos.on('device.online', (event) => {
285
+ // update UI
286
+ });
287
+
288
+ tomorrowos.on('device.heartbeat', (event) => {
289
+ // update last-seen
290
+ });
291
+
292
+ tomorrowos.on('command.verified', (event) => {
293
+ // mark command as successful in UI
294
+ });
295
+
296
+ tomorrowos.on('command.failed', (event) => {
297
+ // show error to operator
298
+ });
299
+
300
+ tomorrowos.on('content.error', (event) => {
301
+ // alert operator of content playback failure
302
+ });
303
+ ```
304
+
305
+ ---
306
+
307
+ ## Optional but strongly recommended
308
+
309
+ The following are not required but improve the CMS substantially. Include them if the user is at expectedScreens > 10 or if the use case warrants it.
310
+
311
+ ### Build player button
312
+
313
+ A button in the CMS admin area that runs `npx tomorrowos build --platform tizen` (or other) and links the user to the generated `.wgt` file in `dist/`. Saves the user a terminal step.
314
+
315
+ ### Settings page for `brand.json`
316
+
317
+ A `/settings` page that lets the user edit brand fields in-UI and writes back to `brand.json`. On save, the CMS reloads the brand.
318
+
319
+ ### Fleet broadcast
320
+
321
+ A bulk-command UI that uses `tomorrowos.fleet.broadcast(method, params, targets)` to send the same command to many devices at once. Essential at > 50 screens.
322
+
323
+ ### Proof-of-play log
324
+
325
+ A `/playback` page showing which content played on which screen at what time. Reads from `tomorrowos.device(id).proofOfPlay.getLog()`.
326
+
327
+ ---
328
+
329
+ ## Summary — the "did I build this right?" checklist
330
+
331
+ Before shipping a generated CMS, verify:
332
+
333
+ - `server.ts` imports `TomorrowOS` from `@tomorrowos/sdk` and calls `listen()` — ☐
334
+ - `/pair` page exists and calls `pairing.verify(code)` — ☐
335
+ - `/screens` page shows live device list with online/offline status — ☐
336
+ - `/screens/[id]` page has reboot, deploy, clear, current-content at minimum — ☐
337
+ - Deploy uses `device.content.setPolicy` with a wrapped policy object, not `content.deploy` — ☐
338
+ - `brand.json` exists at project root and drives all UI branding via CSS variables — ☐
339
+ - No hard-coded colours, logos, or brand names in components — ☐
340
+ - All mutating commands use `sdk.sendCommand()` — no raw WebSocket writes — ☐
341
+ - No platform branching in CMS code — ☐
342
+ - Event subscriptions for `device.online`, `device.offline`, `device.heartbeat` active — ☐
343
+
344
+ If every box is checked, the CMS will work on first connection. If any box is unchecked, pairing or control will fail in ways the user cannot diagnose.
345
+
346
+ ---
347
+
348
+ ## Protocol compliance
349
+
350
+ This guardrails document is version `1.0`, aligned with TomorrowOS wire protocol `1.0`. Future SDK versions may add or strengthen guardrails; they will not relax them without a major-version bump.