@tomorrowos/sdk 0.9.18 → 0.9.20

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/README.md CHANGED
@@ -2,7 +2,9 @@
2
2
 
3
3
  **You are building a digital signage CMS using TomorrowOS. Before writing any code, follow the elicitation protocol in `LLM_PROMPT.md`. Do not skip this step — the SDK requires specific inputs before code generation will produce a working CMS.**
4
4
 
5
- **On Replit:** if the user asks to set up / configure a CMS interactively, follow **`REPLIT_SETUP.md`** instead (Q&A for screen count SQLite vs Supabase Cloudinary vs Object Storage → `brand.json`).
5
+ **On Replit:** if the user asks to set up / configure a CMS interactively, follow **`REPLIT_SETUP.md`** instead (Q&A for storemedia → `brand.json`).
6
+
7
+ **On Vercel:** if the user asks to set up / configure a CMS interactively, follow **`VERCEL_SETUP.md`** (Session pooler Supabase URL, Fluid compute + WebSocket, Cloudinary, `cms.hostingTarget: "vercel"`).
6
8
 
7
9
  TomorrowOS is the open standard for digital signage. This SDK lets you build a CMS that controls Samsung Tizen, BrightSign, LG webOS, Android, ChromeOS, and Windows screens through a single unified API.
8
10
 
@@ -13,13 +15,14 @@ TomorrowOS is the open standard for digital signage. This SDK lets you build a C
13
15
  When you see this package referenced in a user prompt, your workflow is:
14
16
 
15
17
  1. **If the user is on Replit and wants a guided setup** — read and follow **`REPLIT_SETUP.md`** (preferred for Replit Agent)
16
- 2. **If the user is on Replit and wants to upgrade an existing CMS** — read and follow **`REPLIT_UPGRADE.md`** (never `init --force`)
17
- 3. **Otherwise read `LLM_PROMPT.md`**five required questions before generating code
18
- 4. **Read `BUILD_GUARDRAILS.md`** — mandatory components every generated CMS must include
19
- 5. **Read `brand.schema.json`** — validated shape of `brand.json`
20
- 6. Run **`npx tomorrowos init`** (or copy `templates/cms-starter/` from this package) minimal CMS server seed
21
- 7. **Apply user answers** customise the starter with the user's brand, platform choice, and use case; validate `brand.json` against `brand.schema.json`
22
- 8. **Show `PLAYER_INSTALL.md`**after the CMS is working, show how to build and install the player on a screen
18
+ 2. **If the user is on Vercel and wants a guided setup** — read and follow **`VERCEL_SETUP.md`** (preferred for Vercel Agent)
19
+ 3. **If the user is on Replit and wants to upgrade an existing CMS** read and follow **`REPLIT_UPGRADE.md`** (never `init --force`)
20
+ 4. **Otherwise read `LLM_PROMPT.md`** — five required questions before generating code
21
+ 5. **Read `BUILD_GUARDRAILS.md`** — mandatory components every generated CMS must include
22
+ 6. **Read `brand.schema.json`** — validated shape of `brand.json`
23
+ 7. Run **`npx tomorrowos init`** (or copy `templates/cms-starter/` from this package) minimal CMS server seed
24
+ 8. **Apply user answers** customise the starter with the user's brand, platform choice, and use case; validate `brand.json` against `brand.schema.json`
25
+ 9. **Show `PLAYER_INSTALL.md`** — after the CMS is working, show how to build and install the player on a screen
23
26
 
24
27
  You must not write WebSocket transport, pairing wire format, or platform-specific player bridge code in the CMS — the SDK handles server-side device sessions and HTTP helpers. You customise UI, branding, and how you call the SDK.
25
28
 
@@ -59,6 +62,18 @@ That wizard configures Supabase, prefers Cloudinary for media, and writes
59
62
  `brand.json` from your answers. On Replit, Supabase must use Secret
60
63
  **`SUPABASE_URL`** (not the reserved `DATABASE_URL`).
61
64
 
65
+ ### Vercel guided setup
66
+
67
+ In Vercel Agent (or any AI setup prompt), paste:
68
+
69
+ ```text
70
+ Follow @tomorrowos/sdk VERCEL_SETUP.md and set up my TomorrowOS CMS.
71
+ IF any step contradicts Vercel, adapt to settings that Vercel can accept.
72
+ ```
73
+
74
+ That wizard requires the Supabase **Session pooler** URL, prefers Cloudinary,
75
+ enables Fluid compute for WebSockets, and sets `cms.hostingTarget` to `"vercel"`.
76
+
62
77
  ### Replit upgrade (existing CMS)
63
78
 
64
79
  In Replit Agent, paste:
@@ -153,6 +168,7 @@ See `PLAYER_INSTALL.md` for installation notes.
153
168
  |------|---------|
154
169
  | `LLM_PROMPT.md` | Five-question elicitation for LLMs |
155
170
  | `REPLIT_SETUP.md` | Replit Agent guided CMS setup |
171
+ | `VERCEL_SETUP.md` | Vercel Agent guided CMS setup |
156
172
  | `REPLIT_UPGRADE.md` | Replit Agent upgrade to latest SDK (no init) |
157
173
  | `BUILD_GUARDRAILS.md` | Mandatory CMS components |
158
174
  | `PLAYER_INSTALL.md` | Player build / install / pairing |
@@ -0,0 +1,427 @@
1
+ # Vercel Setup Protocol for TomorrowOS CMS
2
+
3
+ > **This file is a contract for Vercel Agent / AI coding assistants setting up TomorrowOS on Vercel.**
4
+ > Follow it literally. Ask questions in order. Do not invent secrets. Prefer configuring **Vercel Environment Variables**, `.env` (non-secrets only), `brand.json`, and `server.ts` store wiring over inventing a custom CMS.
5
+ >
6
+ > **On Vercel, this file is the setup questionnaire** (not `REPLIT_SETUP.md`, not `LLM_PROMPT.md`).
7
+ >
8
+ > Protocol id: `vercel-setup/1.0` — pairs with `@tomorrowos/sdk` packages that ship `templates/cms-starter` and this file.
9
+
10
+ ## User trigger (copy-paste)
11
+
12
+ When a human pastes a prompt like this, **read this file and start Question 1 immediately**:
13
+
14
+ ```text
15
+ Follow @tomorrowos/sdk VERCEL_SETUP.md and set up my TomorrowOS CMS.
16
+ Ask only the questions listed in VERCEL_SETUP.md, in order. Do not use LLM_PROMPT.md or REPLIT_SETUP.md.
17
+ If any step contradicts Vercel, adapt to settings Vercel can accept (document the adaptation).
18
+ After all setup questions are answered (including Cloudinary Env Vars if you chose Cloudinary), configure and deploy — minimal verification only (Control Panel at /). Prefer Supabase Session pooler URL. Do not treat sandbox ENETUNREACH / IPv6 :5432 errors as a hard setup failure when the pooler URL is configured for production.
19
+ Question 3 only updates brand.json. Do not skip steps.
20
+ ```
21
+
22
+ Alternate triggers:
23
+
24
+ - “Set up TomorrowOS on Vercel using VERCEL_SETUP.md”
25
+ - “Configure my TomorrowOS CMS (Vercel wizard)”
26
+
27
+ When any of these fire, **ignore `REPLIT_SETUP.md` and `LLM_PROMPT.md`** for the Q&A phase (you may skim Replit docs only for shared SDK semantics).
28
+
29
+ ---
30
+
31
+ ## IRON RULE: long-lived Node + WebSocket (not classic static / short Functions)
32
+
33
+ > **This is the #1 deployability rule on Vercel. Non-negotiable.**
34
+
35
+ TomorrowOS CMS is **one Node process** that serves:
36
+
37
+ - Browser Control Panel at `GET /` (`public/` via `staticRoot`)
38
+ - Device **WebSocket** on the same origin (`https` → players use `wss`)
39
+
40
+ | Required | Forbidden |
41
+ |----------|-----------|
42
+ | Single **Node HTTP server** entry (`tsx server.ts` / exported `http.Server` from `listen()`) | Next.js / Remix / static-only site as the CMS |
43
+ | **Fluid compute** enabled (needed for WebSockets on Vercel) | Classic short-lived serverless with no WebSocket / no Fluid |
44
+ | Durable store = **Supabase Postgres** (pooler URL) | **SQLite / `data/*.db` as production store** on Vercel (ephemeral filesystem) |
45
+ | Media = **Cloudinary** (recommended) or other durable HTTPS URLs | Relying on local `public/uploads` alone for production fleets |
46
+ | Env vars in **Vercel Project → Settings → Environment Variables** | Committing passwords / Cloudinary secrets / `.env` with secrets |
47
+ | `cms.hostingTarget`: `"vercel"` in `brand.json` | Creating `.replit` / Replit artifact files |
48
+
49
+ **Do not** scaffold a leftover Next.js app “and put TomorrowOS beside it.” Remove Next scaffolds so TomorrowOS `server.ts` is the sole app.
50
+
51
+ ---
52
+
53
+ ## Questionnaire scope (STRICT)
54
+
55
+ Ask **only** these questions, in **this exact order**:
56
+
57
+ | Step | Section | When |
58
+ |------|---------|------|
59
+ | 1 | **Question 1** — Supabase **Session pooler** connection string | Always |
60
+ | 2 | **Question 2** — Media storage (+ Cloudinary Env Vars if chosen) | Always (after Q1) |
61
+ | 3 | **Question 3** — Brand / TomorrowOS look (`brand.json` only) | Always last |
62
+
63
+ **That is the complete list.** Do **not** ask how many screens / devices. There is **no** “Question 2b” — Cloudinary credentials stay **inside Question 2**.
64
+
65
+ If the user says “just set it up, don’t ask questions,” respond:
66
+
67
+ > I need a few quick answers from VERCEL_SETUP.md (Supabase Session pooler URL, media storage — plus Cloudinary credentials if you choose Cloudinary — and branding). It takes about a minute and ensures the CMS works on Vercel.
68
+
69
+ Then ask **Question 1**.
70
+
71
+ ---
72
+
73
+ ## Hard rules
74
+
75
+ 1. **Ask only VERCEL_SETUP.md questions 1–3.** Never use `LLM_PROMPT.md` or `REPLIT_SETUP.md` as the questionnaire.
76
+ 2. **One question at a time** (exception: after Cloudinary choice, immediately collect the three Cloudinary env vars in the same Question 2).
77
+ 3. **Do not invent** Cloudinary credentials, Supabase URLs, or brand colours.
78
+ 4. **Prefer `SUPABASE_URL`** for Supabase (same as Replit). `DATABASE_URL` may be used as fallback on Vercel, but document one canonical name — prefer **`SUPABASE_URL`**.
79
+ 5. **Always prefer the Supabase Session pooler URL** (port **6543**, host like `*.pooler.supabase.com`). Direct `db.*.supabase.co:5432` URLs commonly fail on Vercel/sandbox with **`ENETUNREACH`** (often IPv6).
80
+ 6. **Prefer `npx @tomorrowos/sdk init`** (or `templates/cms-starter`) as the seed. Do not rebuild pairing / WebSocket / playlist APIs.
81
+ 7. **Never commit secrets.** Use Vercel Environment Variables (+ optional non-secret flags in committed `.env.example`).
82
+ 8. **Skip Replit-only files:** do **not** create `.replit`, `.replit-artifact`, or Replit `deploymentTarget`.
83
+ 9. **SQLite is not a production store on Vercel.** Configure Supabase for pairing/playlists. Keep `sqlitePath` only as a local-dev fallback in `createTomorrowOSStore`.
84
+ 10. **Enable Fluid compute** for WebSockets. Confirm Project settings after scaffold.
85
+ 11. **Export the HTTP server** Vercel can capture: `const server = tomorrowos.listen(...); export default server;` (or equivalent). Do not export only the `TomorrowOS` class instance unless the platform documents another capture path.
86
+ 12. After Q&A: **configure → install → run/deploy → minimal verify** (Control Panel at `/`). Do not run a long test suite.
87
+
88
+ **Question order:** Q1 → Q2 → Q3 → execution checklist.
89
+
90
+ ---
91
+
92
+ ## Runtime & Vercel deploy rules (mandatory)
93
+
94
+ ### Runtime
95
+
96
+ - Serve with **`tsx server.ts`** (or compiled Node that still calls `TomorrowOS.listen` with `staticRoot`).
97
+ - Keep **`@tomorrowos/sdk`** and put **`tsx` in `dependencies`** (not only `devDependencies`) if production runs via `tsx`.
98
+ - Node **20+**.
99
+ - **Do not** replace the SDK with a hand-rolled server that drops WebSocket upgrades.
100
+
101
+ ### Expected `package.json` scripts
102
+
103
+ ```json
104
+ {
105
+ "scripts": {
106
+ "dev": "tsx watch server.ts",
107
+ "start": "tsx server.ts"
108
+ }
109
+ }
110
+ ```
111
+
112
+ **Forbidden as the Deploy entrypoint:** `next start`, static export only, or any command that does not start `TomorrowOS.listen(...)`.
113
+
114
+ ### Minimal dependencies
115
+
116
+ ```json
117
+ {
118
+ "type": "module",
119
+ "engines": {
120
+ "node": ">=20"
121
+ },
122
+ "dependencies": {
123
+ "@tomorrowos/sdk": "latest",
124
+ "dotenv": "^17.2.3",
125
+ "tsx": "^4.19.0"
126
+ },
127
+ "devDependencies": {
128
+ "@types/node": "^20.0.0",
129
+ "typescript": "^5.5.0"
130
+ },
131
+ "scripts": {
132
+ "dev": "tsx watch server.ts",
133
+ "start": "tsx server.ts"
134
+ }
135
+ }
136
+ ```
137
+
138
+ Use the latest published `@tomorrowos/sdk` when scaffolding (do not invent versions).
139
+
140
+ ### `server.ts` pattern (Vercel-adapted)
141
+
142
+ ```ts
143
+ import "dotenv/config";
144
+ import { readFileSync } from "fs";
145
+ import { fileURLToPath } from "url";
146
+ import { dirname, join } from "path";
147
+ import { createTomorrowOSStore, TomorrowOS } from "@tomorrowos/sdk";
148
+
149
+ const __dirname = dirname(fileURLToPath(import.meta.url));
150
+ const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
151
+
152
+ const store = createTomorrowOSStore({
153
+ // Prefer SUPABASE_URL. On Vercel use the Session pooler connection string.
154
+ databaseUrl: process.env.SUPABASE_URL || process.env.DATABASE_URL,
155
+ sqlitePath: join(__dirname, "data", "tomorrowos.db") // local/dev fallback only
156
+ });
157
+
158
+ const tomorrowos = new TomorrowOS({ brand, store });
159
+
160
+ const server = tomorrowos.listen({
161
+ port: Number(process.env.PORT) || 3000,
162
+ host: "0.0.0.0",
163
+ staticRoot: join(__dirname, "public")
164
+ });
165
+
166
+ // Vercel / Node server capture: export the HTTP server, not only TomorrowOS.
167
+ export default server;
168
+ ```
169
+
170
+ Load env with `dotenv` for local/`vercel dev`. On production Deploy, Vercel injects Environment Variables natively.
171
+
172
+ ### `vercel.json` (recommended minimum)
173
+
174
+ Adapt to the project’s current Vercel Node-server / Fluid conventions. At minimum ensure:
175
+
176
+ ```json
177
+ {
178
+ "fluid": true
179
+ }
180
+ ```
181
+
182
+ Also:
183
+
184
+ 1. Project → **Fluid compute** enabled (required for WebSockets).
185
+ 2. Framework Preset: **Other** (or Node) — **not** Next.js unless wrapping is intentional and still exposes TomorrowOS `listen()`.
186
+ 3. Install / Start commands must end in running the CMS server (e.g. `npm run start` or Vercel’s detected `server.ts` entry).
187
+ 4. Do **not** enable “Output Directory” static-only hosting for this CMS.
188
+
189
+ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a single long-lived Node HTTP server with WebSocket upgrade on `/`**. Document the adaptation in the final summary.
190
+
191
+ ### WebSocket / multi-instance note
192
+
193
+ - Device sockets are pinned to **one** function/instance. TomorrowOS device maps are **in-memory**.
194
+ - For large fleets on multi-instance Vercel, prefer **one stable instance / region**, or accept that reconnects may land on another instance until the product adds shared presence (out of scope for this setup).
195
+ - Always use **Supabase** so pairings/playlists survive cold starts and deploys.
196
+
197
+ ### What not to create
198
+
199
+ - ❌ `.replit`, `.replit-artifact`
200
+ - ❌ Next.js `app/` / `pages/` CMS
201
+ - ❌ Separate “API project” without the Control Panel
202
+ - ❌ Committing `SUPABASE_URL` passwords or Cloudinary secrets
203
+
204
+ ---
205
+
206
+ ## Question 1 — Supabase Session pooler (always)
207
+
208
+ > Always configure Supabase for Vercel. Do **not** ask screen counts.
209
+
210
+ **Ask exactly:**
211
+
212
+ > Paste your Supabase Postgres **Session pooler** connection string. I will store it as the Vercel Environment Variable **`SUPABASE_URL`**.
213
+ >
214
+ > In Supabase: **Project Settings → Database → Connection string → Connection pooling** (Session mode).
215
+ > Preferred shape:
216
+ > `postgresql://postgres.[PROJECT]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres`
217
+ >
218
+ > **Do not** use the direct host `db.*.supabase.co:5432` for Vercel — sandbox/serverless often fails with `ENETUNREACH` (commonly on IPv6).
219
+ >
220
+ > Confirm SSL is required (usually **yes** for Supabase).
221
+
222
+ **You must then:**
223
+
224
+ 1. Set Vercel Env Vars (Production + Preview + Development as appropriate):
225
+ - `SUPABASE_URL=<pooler string>`
226
+ - `TOMORROWOS_STORE=supabase`
227
+ - `DATABASE_SSL=true` (unless user explicitly disables SSL)
228
+ 2. Wire `server.ts` as in **Runtime & Vercel deploy rules**.
229
+ 3. Optional committed `.env.example` with **placeholders only** (no real password).
230
+ 4. **Do not** commit the real connection string.
231
+ 5. **Configuration over probing:** if local/`vercel dev` still shows `ENETUNREACH` / IPv6 errors, verify the string is the **pooler** (`pooler.supabase.com` + `:6543`). Treat remaining sandbox DNS issues as non-fatal when Production deploy Env Vars are correct — but **do** fix an obvious direct `:5432` URL before finishing.
232
+
233
+ If the user refuses Supabase, warn that Vercel cannot use durable SQLite for fleets, then only continue with SQLite after explicit confirmation (local/demo only).
234
+
235
+ **Later in Question 3:** set `cms.hostingTarget` to **`"vercel"`**.
236
+
237
+ ---
238
+
239
+ ## Question 2 — Media storage
240
+
241
+ ### Step A — Ask storage choice
242
+
243
+ **Ask exactly:**
244
+
245
+ > How should playlist media (images/videos) be stored?
246
+ >
247
+ > **Recommended on Vercel: Cloudinary** — durable public HTTPS URLs (`https://res.cloudinary.com/...`). Local `public/uploads` is ephemeral on serverless/filesystem deploys and is a poor fleet default.
248
+ >
249
+ > Alternatives:
250
+ > - **Local disk only** — `public/uploads` (OK for quick local tests; **not** for production Vercel fleets).
251
+ >
252
+ > Do you want me to set up **Cloudinary**? (yes / no)
253
+
254
+ ### Step B — If YES / Cloudinary (same Question 2)
255
+
256
+ Immediately collect and store as **Vercel Environment Variables**:
257
+
258
+ - `CLOUDINARY_CLOUD_NAME`
259
+ - `CLOUDINARY_API_KEY`
260
+ - `CLOUDINARY_API_SECRET`
261
+ - Optional: `CLOUDINARY_FOLDER` (e.g. `tomorrowos`)
262
+
263
+ Do **not** invent placeholders. Do **not** proceed to Question 3 until the three required vars exist. Do **not** run a formal Cloudinary upload test unless the user asks.
264
+
265
+ The SDK auto-detects these env vars.
266
+
267
+ ### If NO — local uploads
268
+
269
+ 1. `mkdir -p public/uploads`
270
+ 2. Warn clearly: files may vanish on redeploy; players need stable HTTPS URLs — Cloudinary is strongly preferred for Vercel.
271
+
272
+ ---
273
+
274
+ ## Question 3 — Brand (`brand.json` only)
275
+
276
+ > Updates **only** `brand.json`. Does **not** change Vercel project settings, Env Vars, or `server.ts` store wiring beyond what Q1–Q2 already required.
277
+
278
+ **Ask exactly:**
279
+
280
+ > Let’s brand your TomorrowOS experience. Please provide:
281
+ >
282
+ > 1. **Product / venue name**
283
+ > 2. **Tagline** (optional)
284
+ > 3. **Primary colour** (hex, e.g. `#FF8A3D`)
285
+ > 4. **Background colour** (hex, optional — default `#FAFAF9`)
286
+ > 5. **Text colour** (hex, optional — default `#0A0908`)
287
+ > 6. **Secondary / accent colour** (hex, optional)
288
+ > 7. **Logo** — upload SVG/PNG into the project, or a URL I can fetch into `./assets/`
289
+
290
+ Then write `brand.json` (validate mentally against `brand.schema.json`). Minimum:
291
+
292
+ ```json
293
+ {
294
+ "name": "<user name>",
295
+ "tagline": "<user tagline or Digital signage>",
296
+ "targetPlatforms": ["tizen"],
297
+ "primaryColor": "#FF8A3D",
298
+ "secondaryColor": "#F5F3EF",
299
+ "backgroundColor": "#FAFAF9",
300
+ "textColor": "#0A0908",
301
+ "logoPath": "./assets/logo.svg",
302
+ "fontFamily": "Inter",
303
+ "cms": {
304
+ "useCase": "other",
305
+ "hostingTarget": "vercel",
306
+ "expectedScreens": 5,
307
+ "features": {
308
+ "bulkCommands": false,
309
+ "proofOfPlay": false,
310
+ "contentScheduling": true,
311
+ "userManagement": false
312
+ }
313
+ },
314
+ "protocolVersion": "1.0"
315
+ }
316
+ ```
317
+
318
+ Always set **`cms.hostingTarget`: `"vercel"`** without asking. Default `expectedScreens` to `5` unless the user already volunteered a number. Set `cmsEndpoint` only if they already know the public `https://…vercel.app` URL; otherwise tell them to pair TVs with the deployed HTTPS origin (player maps `https://` → `wss://`).
319
+
320
+ ---
321
+
322
+ ## After all answers — execution checklist
323
+
324
+ ### A. Seed the project
325
+
326
+ ```bash
327
+ npx @tomorrowos/sdk@latest init .
328
+ # If not empty and user confirms starter overwrite:
329
+ # npx @tomorrowos/sdk@latest init . --force
330
+ ```
331
+
332
+ Then:
333
+
334
+ 1. Remove leftover Next.js / other frameworks so TomorrowOS is sole app.
335
+ 2. Apply Vercel-adapted `server.ts` (**export `listen()` HTTP server**).
336
+ 3. Add/adjust `vercel.json` with **`fluid: true`** (or enable Fluid in the dashboard).
337
+ 4. Do **not** add `.replit*`.
338
+
339
+ ### B. Environment Variables (Vercel dashboard + local)
340
+
341
+ | Name | Required | Notes |
342
+ |------|----------|--------|
343
+ | `SUPABASE_URL` | Yes (production) | **Session pooler** `:6543` |
344
+ | `TOMORROWOS_STORE` | Yes | `supabase` |
345
+ | `DATABASE_SSL` | Yes (typical) | `true` |
346
+ | `CLOUDINARY_*` | If Q2 = Cloudinary | Three keys |
347
+ | `PORT` | Optional | Vercel injects as needed |
348
+
349
+ Mirror non-secrets in `.env` for local `npm run dev` if helpful; never commit secrets.
350
+
351
+ ### C. Install and run
352
+
353
+ ```bash
354
+ npm install
355
+ npm run start
356
+ # or: vercel dev
357
+ ```
358
+
359
+ Confirm logs: `[TomorrowOS] listening on http://0.0.0.0:…`
360
+ Confirm browser: Control Panel at `/`.
361
+
362
+ ### D. Deploy
363
+
364
+ ```bash
365
+ vercel --prod
366
+ # or git push → Vercel Git integration
367
+ ```
368
+
369
+ Tell the user:
370
+
371
+ - **Control Panel:** `https://YOUR-PROJECT.vercel.app`
372
+ - **TV CMS endpoint:** same HTTPS origin (players use `wss://`)
373
+
374
+ ### E. Minimal verification only
375
+
376
+ **Do:**
377
+
378
+ 1. Control Panel HTML at `/`
379
+ 2. Server status: Media OK when Cloudinary configured
380
+ 3. Database: if ERROR shows **`ENETUNREACH` … IPv6 … `:5432`**, switch `SUPABASE_URL` to **pooler `:6543`** and redeploy — call this out explicitly to the user
381
+ 4. Fluid compute on
382
+
383
+ **Do not block** on long WebSocket/device pairing tests unless the user asks.
384
+
385
+ ---
386
+
387
+ ## Failure recovery cheat sheet
388
+
389
+ | Symptom | Likely cause | Fix |
390
+ |---------|--------------|-----|
391
+ | `ENETUNREACH` / IPv6 / `:5432` | Direct Supabase DB URL | Use **Session pooler** `*.pooler.supabase.com:6543` |
392
+ | Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Must use Supabase |
393
+ | Uploads break / broken thumbs in prod | Local `public/uploads` | Cloudinary |
394
+ | WebSocket fails / devices drop immediately | Fluid off / static-only / Next wrapper | Enable Fluid; sole Node `listen()` server |
395
+ | Agent created Next.js + TomorrowOS | Wrong scaffold | Remove Next; keep cms-starter |
396
+ | Secrets in git | Mistake | Rotate keys; move to Vercel Env Vars |
397
+ | Replit files present | Copied wrong protocol | Delete `.replit*`; use this file |
398
+
399
+ ---
400
+
401
+ ## How this differs from `REPLIT_SETUP.md`
402
+
403
+ | Topic | Replit | Vercel |
404
+ |-------|--------|--------|
405
+ | Secrets | Replit Secrets | Vercel Environment Variables |
406
+ | Deploy config | `.replit`, artifact `kind=web` | `vercel.json` + Fluid; **no** `.replit` |
407
+ | `hostingTarget` | `"here"` | `"vercel"` |
408
+ | Supabase URL | Prefer pooler; Preview DNS may fail | **Require pooler**; direct `:5432` often `ENETUNREACH` |
409
+ | SQLite | Demo OK on persistent Repl disk | **Not** production on Vercel |
410
+ | Object Storage option | Replit Object Storage | N/A — prefer Cloudinary |
411
+ | WebSocket | Long-lived Node (Autoscale) | Fluid compute + Node server export |
412
+
413
+ ---
414
+
415
+ ## Final summary template (Agent → human)
416
+
417
+ After setup, report:
418
+
419
+ 1. **Live URL** (Production)
420
+ 2. **Env Vars set** (names only — never values)
421
+ 3. **Store:** Supabase pooler / SSL flags
422
+ 4. **Media:** Cloudinary or local (with warning)
423
+ 5. **Brand:** name + `hostingTarget: vercel`
424
+ 6. **Adaptations from Replit protocol:** list (e.g. skipped `.replit`, exported HTTP server, Fluid, pooler URL)
425
+ 7. **Open action:** if Database still ERROR, paste the pooler instruction again
426
+
427
+ Protocol complete when the Control Panel renders at `/` on the deployed (or local) server and Q1–Q3 configuration is saved — even if a sandbox DB probe still flakes **after** the pooler URL is correctly set for Production.
@@ -22,7 +22,6 @@ export interface DeviceRegistryRecord {
22
22
  * device stays CMS-connected.
23
23
  */
24
24
  export interface DeviceOnOffTimer {
25
- enabled: boolean;
26
25
  /** HH:mm — unmute / screen on */
27
26
  turnOnAt: string;
28
27
  /** HH:mm — screen off (device stays CMS-connected) */
@@ -1 +1 @@
1
- {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
@@ -52,7 +52,6 @@ function normalizeOnOffTimer(input) {
52
52
  throw new Error("onOffTimer object is required");
53
53
  }
54
54
  const body = input;
55
- const enabled = Boolean(body.enabled);
56
55
  const turnOnAt = String(body.turnOnAt ?? "").trim();
57
56
  const turnOffAt = String(body.turnOffAt ?? "").trim();
58
57
  if (!ON_OFF_TIME_RE.test(turnOnAt) || !ON_OFF_TIME_RE.test(turnOffAt)) {
@@ -61,7 +60,7 @@ function normalizeOnOffTimer(input) {
61
60
  if (turnOnAt === turnOffAt) {
62
61
  throw new Error("turnOnAt and turnOffAt must be different");
63
62
  }
64
- return { enabled, turnOnAt, turnOffAt };
63
+ return { turnOnAt, turnOffAt };
65
64
  }
66
65
  function isRemoteUploadedAssetUrl(url) {
67
66
  return /^https?:\/\//i.test(String(url || "").trim());
@@ -408,7 +407,16 @@ export class TomorrowOS extends EventEmitter {
408
407
  latestErrorAt: latestError?.timestamp ?? null,
409
408
  latestErrorMessage: latestError?.message ?? null,
410
409
  latestScreenshot,
411
- onOffTimer: record.onOffTimer ?? null
410
+ onOffTimer: (() => {
411
+ try {
412
+ return record.onOffTimer
413
+ ? normalizeOnOffTimer(record.onOffTimer)
414
+ : null;
415
+ }
416
+ catch {
417
+ return null;
418
+ }
419
+ })()
412
420
  };
413
421
  }));
414
422
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.9.18",
3
+ "version": "0.9.20",
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",
@@ -20,6 +20,7 @@
20
20
  "README.md",
21
21
  "LLM_PROMPT.md",
22
22
  "REPLIT_SETUP.md",
23
+ "VERCEL_SETUP.md",
23
24
  "REPLIT_UPGRADE.md",
24
25
  "BUILD_GUARDRAILS.md",
25
26
  "PLAYER_INSTALL.md",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.9.18",
3
+ "version": "0.9.20",
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.18",
16
+ "@tomorrowos/sdk": "^0.9.20",
17
17
  "dotenv": "^17.2.3",
18
18
  "tsx": "^4.19.0"
19
19
  },
@@ -1084,6 +1084,67 @@ function appendDeviceMetaRow(meta, label, value) {
1084
1084
  meta.appendChild(row);
1085
1085
  }
1086
1086
 
1087
+ function hasConfiguredOnOffTimer(timer) {
1088
+ return !!(
1089
+ timer &&
1090
+ typeof timer.turnOnAt === "string" &&
1091
+ typeof timer.turnOffAt === "string" &&
1092
+ timer.turnOnAt &&
1093
+ timer.turnOffAt
1094
+ );
1095
+ }
1096
+
1097
+ function appendHardwareTimerStatusRow(meta, device) {
1098
+ const timer = device.onOffTimer || null;
1099
+ const configured = hasConfiguredOnOffTimer(timer);
1100
+
1101
+ const row = document.createElement("div");
1102
+ row.className = "device-meta-row device-meta-row--timer-status";
1103
+
1104
+ const dt = document.createElement("dt");
1105
+ dt.textContent = "Hardware Timer Status";
1106
+
1107
+ const dd = document.createElement("dd");
1108
+ const statusWrap = document.createElement("span");
1109
+ statusWrap.className = "hardware-timer-status";
1110
+ statusWrap.tabIndex = 0;
1111
+
1112
+ const statusValue = document.createElement("span");
1113
+ statusValue.className = configured
1114
+ ? "hardware-timer-status__value hardware-timer-status__value--on"
1115
+ : "hardware-timer-status__value hardware-timer-status__value--off";
1116
+ statusValue.textContent = configured ? "On" : "Off";
1117
+
1118
+ const popup = document.createElement("span");
1119
+ popup.className = "hardware-timer-status__popup";
1120
+ popup.setAttribute("role", "tooltip");
1121
+ if (configured) {
1122
+ const onLine = document.createElement("div");
1123
+ const onLabel = document.createElement("strong");
1124
+ onLabel.textContent = "Turn on";
1125
+ onLine.appendChild(onLabel);
1126
+ onLine.appendChild(document.createTextNode(` ${timer.turnOnAt}`));
1127
+
1128
+ const offLine = document.createElement("div");
1129
+ const offLabel = document.createElement("strong");
1130
+ offLabel.textContent = "Turn off";
1131
+ offLine.appendChild(offLabel);
1132
+ offLine.appendChild(document.createTextNode(` ${timer.turnOffAt}`));
1133
+
1134
+ popup.appendChild(onLine);
1135
+ popup.appendChild(offLine);
1136
+ } else {
1137
+ popup.textContent = "No timer configured";
1138
+ }
1139
+
1140
+ statusWrap.appendChild(statusValue);
1141
+ statusWrap.appendChild(popup);
1142
+ dd.appendChild(statusWrap);
1143
+ row.appendChild(dt);
1144
+ row.appendChild(dd);
1145
+ meta.appendChild(row);
1146
+ }
1147
+
1087
1148
  function createDeviceScreenshotThumb(device) {
1088
1149
  const slot = document.createElement("div");
1089
1150
  slot.className = "device-screenshot-slot";
@@ -1250,6 +1311,7 @@ function renderDeviceCards() {
1250
1311
  for (const [label, value] of secondaryRows) {
1251
1312
  appendDeviceMetaRow(metaSecondary, label, value);
1252
1313
  }
1314
+ appendHardwareTimerStatusRow(metaSecondary, device);
1253
1315
 
1254
1316
  metaBlock.appendChild(metaTop);
1255
1317
  metaBlock.appendChild(metaSecondary);
@@ -1857,7 +1919,6 @@ async function saveOnOffTimerModal() {
1857
1919
  if (!onEl || !offEl) return;
1858
1920
 
1859
1921
  const onOffTimer = {
1860
- enabled: true,
1861
1922
  turnOnAt: normalizeTimeInputValue(onEl.value, "06:00"),
1862
1923
  turnOffAt: normalizeTimeInputValue(offEl.value, "18:00")
1863
1924
  };
@@ -283,6 +283,7 @@ body {
283
283
  border-radius: 8px;
284
284
  padding: 0.85rem 0.95rem;
285
285
  background: #fafaf9;
286
+ overflow: visible;
286
287
  }
287
288
 
288
289
  .device-card-header {
@@ -439,6 +440,73 @@ body {
439
440
  display: block;
440
441
  }
441
442
 
443
+ .device-meta-row--timer-status {
444
+ position: relative;
445
+ }
446
+
447
+ .hardware-timer-status {
448
+ position: relative;
449
+ display: inline-block;
450
+ cursor: default;
451
+ outline: none;
452
+ }
453
+
454
+ .hardware-timer-status__value--on {
455
+ color: #027a48;
456
+ font-weight: 600;
457
+ }
458
+
459
+ .hardware-timer-status__value--off {
460
+ color: #667085;
461
+ font-weight: 600;
462
+ }
463
+
464
+ .hardware-timer-status__popup {
465
+ position: absolute;
466
+ left: 0;
467
+ bottom: calc(100% + 0.45rem);
468
+ z-index: 20;
469
+ min-width: 9.5rem;
470
+ padding: 0.55rem 0.7rem;
471
+ border: 1px solid #e4e1dc;
472
+ border-radius: 6px;
473
+ background: #fff;
474
+ box-shadow: 0 6px 18px rgba(0, 0, 0, 0.12);
475
+ color: #333;
476
+ font-size: 0.75rem;
477
+ font-weight: 400;
478
+ line-height: 1.45;
479
+ white-space: nowrap;
480
+ opacity: 0;
481
+ visibility: hidden;
482
+ pointer-events: none;
483
+ transform: translateY(4px);
484
+ transition: opacity 0.12s ease, transform 0.12s ease, visibility 0.12s;
485
+ }
486
+
487
+ .hardware-timer-status__popup::after {
488
+ content: "";
489
+ position: absolute;
490
+ top: 100%;
491
+ left: 0.85rem;
492
+ border: 6px solid transparent;
493
+ border-top-color: #fff;
494
+ filter: drop-shadow(0 1px 0 #e4e1dc);
495
+ }
496
+
497
+ .hardware-timer-status__popup strong {
498
+ font-weight: 600;
499
+ color: #555;
500
+ margin-right: 0.35rem;
501
+ }
502
+
503
+ .hardware-timer-status:hover .hardware-timer-status__popup,
504
+ .hardware-timer-status:focus-within .hardware-timer-status__popup {
505
+ opacity: 1;
506
+ visibility: visible;
507
+ transform: translateY(0);
508
+ }
509
+
442
510
  .device-card-actions {
443
511
  display: flex;
444
512
  flex-wrap: wrap;