@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.
- package/BUILD_GUARDRAILS.md +350 -350
- package/README.md +111 -111
- package/dist/cli.js +12 -12
- package/dist/tomorrowos.d.ts +1 -1
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +5 -5
- package/package.json +43 -43
- package/templates/cms-starter/package.json +20 -20
- package/templates/cms-starter/policy.example.json +30 -30
- package/templates/cms-starter/public/index.html +5 -3
- package/templates/cms-starter/public/methods.js +9 -24
- package/templates/cms-starter/public/panel.css +329 -329
package/BUILD_GUARDRAILS.md
CHANGED
|
@@ -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.
|