@tomorrowos/sdk 0.9.20 → 0.9.22
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 +4 -3
- package/VERCEL_SETUP.md +169 -49
- package/dist/tomorrowos.d.ts +9 -0
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +40 -4
- package/package.json +1 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/public/index.html +2 -0
- package/templates/cms-starter/public/methods.js +49 -1
package/README.md
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
|
|
5
5
|
**On Replit:** if the user asks to set up / configure a CMS interactively, follow **`REPLIT_SETUP.md`** instead (Q&A for store → media → `brand.json`).
|
|
6
6
|
|
|
7
|
-
**On Vercel:** if the user asks to set up / configure a CMS interactively, follow **`VERCEL_SETUP.md`** (
|
|
7
|
+
**On Vercel:** if the user asks to set up / configure a CMS interactively, follow **`VERCEL_SETUP.md`** (Supabase or Neon pooled Postgres, Cloudinary or Vercel Blob media, Fluid compute + WebSocket, `cms.hostingTarget: "vercel"`).
|
|
8
8
|
|
|
9
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.
|
|
10
10
|
|
|
@@ -71,8 +71,9 @@ Follow @tomorrowos/sdk VERCEL_SETUP.md and set up my TomorrowOS CMS.
|
|
|
71
71
|
IF any step contradicts Vercel, adapt to settings that Vercel can accept.
|
|
72
72
|
```
|
|
73
73
|
|
|
74
|
-
That wizard
|
|
75
|
-
enables Fluid compute for WebSockets, and sets
|
|
74
|
+
That wizard asks: database (**Supabase** → **Neon** → SQLite demo), media (**Cloudinary** → **Vercel Blob** → local),
|
|
75
|
+
branding (manual fields or **infer from a website URL**), enables Fluid compute for WebSockets, and sets
|
|
76
|
+
`cms.hostingTarget` to `"vercel"`.
|
|
76
77
|
|
|
77
78
|
### Replit upgrade (existing CMS)
|
|
78
79
|
|
package/VERCEL_SETUP.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
>
|
|
6
6
|
> **On Vercel, this file is the setup questionnaire** (not `REPLIT_SETUP.md`, not `LLM_PROMPT.md`).
|
|
7
7
|
>
|
|
8
|
-
> Protocol id: `vercel-setup/1.
|
|
8
|
+
> Protocol id: `vercel-setup/1.1` — pairs with `@tomorrowos/sdk` packages that ship `templates/cms-starter` and this file.
|
|
9
9
|
|
|
10
10
|
## User trigger (copy-paste)
|
|
11
11
|
|
|
@@ -15,7 +15,7 @@ When a human pastes a prompt like this, **read this file and start Question 1 im
|
|
|
15
15
|
Follow @tomorrowos/sdk VERCEL_SETUP.md and set up my TomorrowOS CMS.
|
|
16
16
|
Ask only the questions listed in VERCEL_SETUP.md, in order. Do not use LLM_PROMPT.md or REPLIT_SETUP.md.
|
|
17
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
|
|
18
|
+
After all setup questions are answered (including Cloudinary or Vercel Blob Env Vars if you chose those), configure and deploy — minimal verification only (Control Panel at /). Prefer Supabase Session pooler URL or Neon pooled URL. Do not treat sandbox ENETUNREACH / IPv6 :5432 errors as a hard setup failure when a pooler URL is configured for production.
|
|
19
19
|
Question 3 only updates brand.json. Do not skip steps.
|
|
20
20
|
```
|
|
21
21
|
|
|
@@ -41,9 +41,9 @@ TomorrowOS CMS is **one Node process** that serves:
|
|
|
41
41
|
|----------|-----------|
|
|
42
42
|
| Single **Node HTTP server** entry (`tsx server.ts` / exported `http.Server` from `listen()`) | Next.js / Remix / static-only site as the CMS |
|
|
43
43
|
| **Fluid compute** enabled (needed for WebSockets on Vercel) | Classic short-lived serverless with no WebSocket / no Fluid |
|
|
44
|
-
| Durable store = **
|
|
45
|
-
| Media = **Cloudinary**
|
|
46
|
-
| Env vars in **Vercel Project → Settings → Environment Variables** | Committing passwords /
|
|
44
|
+
| Durable store = **Postgres** (Supabase or Neon, pooled URL) | **SQLite / `data/*.db` as production store** on Vercel (ephemeral filesystem) |
|
|
45
|
+
| Media = **Cloudinary** or **Vercel Blob** (durable HTTPS URLs) | Relying on local `public/uploads` alone for production fleets |
|
|
46
|
+
| Env vars in **Vercel Project → Settings → Environment Variables** | Committing passwords / API secrets / `.env` with secrets |
|
|
47
47
|
| `cms.hostingTarget`: `"vercel"` in `brand.json` | Creating `.replit` / Replit artifact files |
|
|
48
48
|
|
|
49
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.
|
|
@@ -56,15 +56,15 @@ Ask **only** these questions, in **this exact order**:
|
|
|
56
56
|
|
|
57
57
|
| Step | Section | When |
|
|
58
58
|
|------|---------|------|
|
|
59
|
-
| 1 | **Question 1** — Supabase
|
|
60
|
-
| 2 | **Question 2** — Media storage (
|
|
59
|
+
| 1 | **Question 1** — Database (Supabase → Neon → SQLite) | Always |
|
|
60
|
+
| 2 | **Question 2** — Media storage (Cloudinary → Vercel Blob → local) | Always (after Q1) |
|
|
61
61
|
| 3 | **Question 3** — Brand / TomorrowOS look (`brand.json` only) | Always last |
|
|
62
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**.
|
|
63
|
+
**That is the complete list.** Do **not** ask how many screens / devices. There is **no** “Question 2b” — Cloudinary / Vercel Blob credentials stay **inside Question 2**.
|
|
64
64
|
|
|
65
65
|
If the user says “just set it up, don’t ask questions,” respond:
|
|
66
66
|
|
|
67
|
-
> I need a few quick answers from VERCEL_SETUP.md (
|
|
67
|
+
> I need a few quick answers from VERCEL_SETUP.md (database — Supabase or Neon preferred — media storage, and branding). It takes about a minute and ensures the CMS works on Vercel.
|
|
68
68
|
|
|
69
69
|
Then ask **Question 1**.
|
|
70
70
|
|
|
@@ -73,14 +73,14 @@ Then ask **Question 1**.
|
|
|
73
73
|
## Hard rules
|
|
74
74
|
|
|
75
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
|
|
77
|
-
3. **Do not invent** Cloudinary credentials,
|
|
78
|
-
4. **
|
|
79
|
-
5. **Always prefer
|
|
76
|
+
2. **One question at a time** (exception: after Cloudinary or Vercel Blob choice, immediately collect the required env vars in the same Question 2).
|
|
77
|
+
3. **Do not invent** Cloudinary credentials, database URLs, Vercel Blob tokens, or brand colours.
|
|
78
|
+
4. **Postgres env naming:** prefer **`SUPABASE_URL`** when using Supabase; prefer **`DATABASE_URL`** (or `NEON_DATABASE_URL`) when using Neon. `createTomorrowOSStore` accepts `SUPABASE_URL` then `DATABASE_URL`.
|
|
79
|
+
5. **Always prefer pooled connection strings** (Supabase Session pooler port **6543**; Neon pooled host). Direct `:5432` URLs commonly fail on Vercel/sandbox with **`ENETUNREACH`** (often IPv6).
|
|
80
80
|
6. **Prefer `npx @tomorrowos/sdk init`** (or `templates/cms-starter`) as the seed. Do not rebuild pairing / WebSocket / playlist APIs.
|
|
81
81
|
7. **Never commit secrets.** Use Vercel Environment Variables (+ optional non-secret flags in committed `.env.example`).
|
|
82
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.**
|
|
83
|
+
9. **SQLite is not a production store on Vercel.** Offer it only as option **3** after explicit warning. Keep `sqlitePath` as local-dev fallback in `createTomorrowOSStore`.
|
|
84
84
|
10. **Enable Fluid compute** for WebSockets. Confirm Project settings after scaffold.
|
|
85
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
86
|
12. After Q&A: **configure → install → run/deploy → minimal verify** (Control Panel at `/`). Do not run a long test suite.
|
|
@@ -137,6 +137,8 @@ Then ask **Question 1**.
|
|
|
137
137
|
|
|
138
138
|
Use the latest published `@tomorrowos/sdk` when scaffolding (do not invent versions).
|
|
139
139
|
|
|
140
|
+
Add `@vercel/blob` to `dependencies` only when the user chooses **Vercel Blob** in Question 2.
|
|
141
|
+
|
|
140
142
|
### `server.ts` pattern (Vercel-adapted)
|
|
141
143
|
|
|
142
144
|
```ts
|
|
@@ -150,8 +152,11 @@ const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
150
152
|
const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
|
|
151
153
|
|
|
152
154
|
const store = createTomorrowOSStore({
|
|
153
|
-
//
|
|
154
|
-
databaseUrl:
|
|
155
|
+
// Supabase: SUPABASE_URL (pooler). Neon: DATABASE_URL (pooled).
|
|
156
|
+
databaseUrl:
|
|
157
|
+
process.env.SUPABASE_URL ||
|
|
158
|
+
process.env.NEON_DATABASE_URL ||
|
|
159
|
+
process.env.DATABASE_URL,
|
|
155
160
|
sqlitePath: join(__dirname, "data", "tomorrowos.db") // local/dev fallback only
|
|
156
161
|
});
|
|
157
162
|
|
|
@@ -192,24 +197,40 @@ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a s
|
|
|
192
197
|
|
|
193
198
|
- Device sockets are pinned to **one** function/instance. TomorrowOS device maps are **in-memory**.
|
|
194
199
|
- 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 **
|
|
200
|
+
- Always use **Postgres** (Supabase or Neon) so pairings/playlists survive cold starts and deploys.
|
|
196
201
|
|
|
197
202
|
### What not to create
|
|
198
203
|
|
|
199
204
|
- ❌ `.replit`, `.replit-artifact`
|
|
200
205
|
- ❌ Next.js `app/` / `pages/` CMS
|
|
201
206
|
- ❌ Separate “API project” without the Control Panel
|
|
202
|
-
- ❌ Committing
|
|
207
|
+
- ❌ Committing database passwords, Cloudinary secrets, or Blob tokens
|
|
203
208
|
|
|
204
209
|
---
|
|
205
210
|
|
|
206
|
-
## Question 1 —
|
|
211
|
+
## Question 1 — Database (always)
|
|
212
|
+
|
|
213
|
+
> **Do not** ask screen counts. Present all three options; recommend **Supabase** for Vercel fleets.
|
|
207
214
|
|
|
208
|
-
|
|
215
|
+
### Step A — Ask storage choice
|
|
209
216
|
|
|
210
217
|
**Ask exactly:**
|
|
211
218
|
|
|
212
|
-
>
|
|
219
|
+
> Which database should TomorrowOS use on Vercel?
|
|
220
|
+
>
|
|
221
|
+
> **1. Supabase Postgres (recommended)** — durable pairing/playlists; use the **Session pooler** URL (`*.pooler.supabase.com:6543`).
|
|
222
|
+
>
|
|
223
|
+
> **2. Neon Postgres** — serverless Postgres native to Vercel; use Neon’s **pooled** connection string from the Neon dashboard (not the direct un-pooled host for serverless).
|
|
224
|
+
>
|
|
225
|
+
> **3. SQLite (`data/tomorrowos.db`)** — **demo / local only on Vercel.** The filesystem is ephemeral; pairings and playlists will not survive redeploys. Only choose this if you understand the limitation.
|
|
226
|
+
>
|
|
227
|
+
> Reply with **1**, **2**, or **3** (or “Supabase” / “Neon” / “SQLite”).
|
|
228
|
+
|
|
229
|
+
### Step B — If **1 / Supabase**
|
|
230
|
+
|
|
231
|
+
**Ask exactly:**
|
|
232
|
+
|
|
233
|
+
> Paste your Supabase Postgres connection string. I will store it as the Vercel Environment Variable **`SUPABASE_URL`**.
|
|
213
234
|
>
|
|
214
235
|
> In Supabase: **Project Settings → Database → Connection string → Connection pooling** (Session mode).
|
|
215
236
|
> Preferred shape:
|
|
@@ -230,9 +251,45 @@ If Vercel’s UI and this file disagree on bundling, **prefer whatever keeps a s
|
|
|
230
251
|
4. **Do not** commit the real connection string.
|
|
231
252
|
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
253
|
|
|
233
|
-
|
|
254
|
+
### Step C — If **2 / Neon**
|
|
234
255
|
|
|
235
|
-
**
|
|
256
|
+
**Ask exactly:**
|
|
257
|
+
|
|
258
|
+
> Paste your **Neon pooled** Postgres connection string. I will store it as **`DATABASE_URL`** (or `NEON_DATABASE_URL` if you prefer — wire whichever you set in `server.ts`).
|
|
259
|
+
>
|
|
260
|
+
> In Neon: **Dashboard → Connection details → Pooled connection** (enable pooling).
|
|
261
|
+
> Typical shape:
|
|
262
|
+
> `postgresql://[user]:[password]@[endpoint].pooler.[region].aws.neon.tech/[dbname]?sslmode=require`
|
|
263
|
+
>
|
|
264
|
+
> **Do not** use the direct (non-pooled) connection string for Vercel serverless unless Neon docs explicitly say otherwise for your plan.
|
|
265
|
+
>
|
|
266
|
+
> Confirm SSL (`sslmode=require`) is on (usually **yes**).
|
|
267
|
+
|
|
268
|
+
**You must then:**
|
|
269
|
+
|
|
270
|
+
1. Set Vercel Env Vars:
|
|
271
|
+
- `DATABASE_URL=<pooled Neon string>` (or `NEON_DATABASE_URL` + read it in `server.ts`)
|
|
272
|
+
- `TOMORROWOS_STORE=postgres`
|
|
273
|
+
- `DATABASE_SSL=true` (unless user explicitly disables SSL)
|
|
274
|
+
2. Wire `server.ts` — `createTomorrowOSStore` uses `PostgresStore` for `postgres` / `supabase` drivers.
|
|
275
|
+
3. **Do not** commit the real connection string.
|
|
276
|
+
4. Same pooler guidance as Supabase: if you see `ENETUNREACH` on `:5432`, switch to Neon’s **pooled** URL.
|
|
277
|
+
|
|
278
|
+
### Step D — If **3 / SQLite**
|
|
279
|
+
|
|
280
|
+
**Warn exactly before continuing:**
|
|
281
|
+
|
|
282
|
+
> SQLite on Vercel is **not** suitable for production fleets — data in `data/tomorrowos.db` is lost when the instance is recycled or redeployed. Continue only for a quick demo.
|
|
283
|
+
|
|
284
|
+
Only proceed after the user explicitly confirms.
|
|
285
|
+
|
|
286
|
+
**You must then:**
|
|
287
|
+
|
|
288
|
+
1. Set `TOMORROWOS_STORE=sqlite` (or omit `DATABASE_URL` / `SUPABASE_URL` so the factory defaults to SQLite).
|
|
289
|
+
2. Keep `sqlitePath` in `server.ts` for `data/tomorrowos.db`.
|
|
290
|
+
3. Warn again in the final summary that they should move to Supabase or Neon for real devices.
|
|
291
|
+
|
|
292
|
+
**Later in Question 3:** set `cms.hostingTarget` to **`"vercel"`** (all Q1 branches).
|
|
236
293
|
|
|
237
294
|
---
|
|
238
295
|
|
|
@@ -244,14 +301,15 @@ If the user refuses Supabase, warn that Vercel cannot use durable SQLite for fle
|
|
|
244
301
|
|
|
245
302
|
> How should playlist media (images/videos) be stored?
|
|
246
303
|
>
|
|
247
|
-
> **
|
|
304
|
+
> **1. Cloudinary (recommended)** — durable public HTTPS URLs (`https://res.cloudinary.com/...`). Works out of the box with `@tomorrowos/sdk` auto-detection.
|
|
248
305
|
>
|
|
249
|
-
>
|
|
250
|
-
> - **Local disk only** — `public/uploads` (OK for quick local tests; **not** for production Vercel fleets).
|
|
306
|
+
> **2. Vercel Blob** — Vercel-native object storage; durable `https://*.public.blob.vercel-storage.com/...` URLs. Good when you want media on the same Vercel project without a Cloudinary account.
|
|
251
307
|
>
|
|
252
|
-
>
|
|
308
|
+
> **3. Local disk only** — `public/uploads` (OK for quick local tests; **not** for production Vercel fleets — files are ephemeral).
|
|
309
|
+
>
|
|
310
|
+
> Reply with **1**, **2**, or **3** (or “Cloudinary” / “Vercel Blob” / “local”).
|
|
253
311
|
|
|
254
|
-
### Step B — If
|
|
312
|
+
### Step B — If **1 / Cloudinary** (same Question 2)
|
|
255
313
|
|
|
256
314
|
Immediately collect and store as **Vercel Environment Variables**:
|
|
257
315
|
|
|
@@ -264,10 +322,39 @@ Do **not** invent placeholders. Do **not** proceed to Question 3 until the three
|
|
|
264
322
|
|
|
265
323
|
The SDK auto-detects these env vars.
|
|
266
324
|
|
|
267
|
-
###
|
|
325
|
+
### Step C — If **2 / Vercel Blob** (same Question 2)
|
|
326
|
+
|
|
327
|
+
**Ask exactly:**
|
|
328
|
+
|
|
329
|
+
> I will enable **Vercel Blob** for media uploads.
|
|
330
|
+
>
|
|
331
|
+
> 1. In the Vercel project: **Storage → Create → Blob** (or link an existing Blob store to this project).
|
|
332
|
+
> 2. Confirm **`BLOB_READ_WRITE_TOKEN`** is available (Vercel usually injects it when Blob is linked).
|
|
333
|
+
>
|
|
334
|
+
> Paste the token only if it is not already set in your project Env Vars. Do you already have Blob linked on this Vercel project? (yes / no)
|
|
335
|
+
|
|
336
|
+
**You must then:**
|
|
337
|
+
|
|
338
|
+
1. Set Vercel Env Var: `BLOB_READ_WRITE_TOKEN=<token>` (if not auto-injected).
|
|
339
|
+
2. `npm install @vercel/blob` (add to `dependencies`).
|
|
340
|
+
3. Wire uploads so media returns **absolute HTTPS Blob URLs** stored in `uploaded_assets` (players need stable public URLs).
|
|
341
|
+
- The SDK **natively auto-detects Cloudinary** today. For Vercel Blob, add a thin upload bridge in the CMS project (e.g. custom route or middleware that calls `put()` from `@vercel/blob` and persists the returned `url` the same way Cloudinary URLs are stored).
|
|
342
|
+
- Minimum pattern:
|
|
343
|
+
|
|
344
|
+
```ts
|
|
345
|
+
import { put } from "@vercel/blob";
|
|
346
|
+
|
|
347
|
+
// On upload: const blob = await put(filename, body, { access: "public", token: process.env.BLOB_READ_WRITE_TOKEN });
|
|
348
|
+
// Store blob.url in uploaded_assets — same shape as Cloudinary secure URLs.
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
4. Warn: until the project’s Blob bridge is wired, Control Panel **Media** status may show local/ephemeral — verify uploads return `https://*.blob.vercel-storage.com/...` before calling setup complete.
|
|
352
|
+
5. Do **not** proceed to Question 3 until `BLOB_READ_WRITE_TOKEN` exists (or Blob is linked and token is confirmed in the Vercel dashboard).
|
|
353
|
+
|
|
354
|
+
### Step D — If **3 / local uploads**
|
|
268
355
|
|
|
269
356
|
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.
|
|
357
|
+
2. Warn clearly: files may vanish on redeploy; players need stable HTTPS URLs — Cloudinary or Vercel Blob is strongly preferred for Vercel.
|
|
271
358
|
|
|
272
359
|
---
|
|
273
360
|
|
|
@@ -275,10 +362,17 @@ The SDK auto-detects these env vars.
|
|
|
275
362
|
|
|
276
363
|
> Updates **only** `brand.json`. Does **not** change Vercel project settings, Env Vars, or `server.ts` store wiring beyond what Q1–Q2 already required.
|
|
277
364
|
|
|
365
|
+
### Step A — Ask branding input
|
|
366
|
+
|
|
278
367
|
**Ask exactly:**
|
|
279
368
|
|
|
280
|
-
> Let’s brand your TomorrowOS experience.
|
|
369
|
+
> Let’s brand your TomorrowOS experience. You can answer in either way:
|
|
370
|
+
>
|
|
371
|
+
> **Option A — Website URL only**
|
|
372
|
+
> Paste **one public website URL** (your company / venue site). I will infer colours, fonts, name, tagline, and logo from that page and write `brand.json` to match its look.
|
|
281
373
|
>
|
|
374
|
+
> **Option B — Manual fields**
|
|
375
|
+
> Provide:
|
|
282
376
|
> 1. **Product / venue name**
|
|
283
377
|
> 2. **Tagline** (optional)
|
|
284
378
|
> 3. **Primary colour** (hex, e.g. `#FF8A3D`)
|
|
@@ -287,12 +381,33 @@ The SDK auto-detects these env vars.
|
|
|
287
381
|
> 6. **Secondary / accent colour** (hex, optional)
|
|
288
382
|
> 7. **Logo** — upload SVG/PNG into the project, or a URL I can fetch into `./assets/`
|
|
289
383
|
|
|
290
|
-
|
|
384
|
+
### Step B — If the user gives **only a website URL** (Option A)
|
|
385
|
+
|
|
386
|
+
**You must:**
|
|
387
|
+
|
|
388
|
+
1. **Fetch and inspect** the page (HTTP GET the URL; follow one redirect if needed). Do not invent colours.
|
|
389
|
+
2. **Derive branding** using this priority order:
|
|
390
|
+
- **Name:** `<title>`, `og:site_name`, or prominent header / logo `alt` text (trim to ≤ 60 chars).
|
|
391
|
+
- **Tagline:** `meta[name="description"]`, `og:description`, or first hero subtitle (≤ 120 chars).
|
|
392
|
+
- **Primary colour:** `meta[name="theme-color"]`, CSS `--primary` / `--brand` variables, or dominant accent from linked stylesheets / inline styles (convert to `#RRGGBB`).
|
|
393
|
+
- **Background colour:** `body` / `:root` background (default `#FAFAF9` if light site).
|
|
394
|
+
- **Text colour:** main body text colour (default `#0A0908` if light site).
|
|
395
|
+
- **Secondary colour:** muted border / secondary button colour, or a tint of the primary.
|
|
396
|
+
- **Font:** first `font-family` on `body` (strip quotes; default `Inter` if generic system stack).
|
|
397
|
+
- **Logo:** prefer `og:image`, then `link[rel="icon"]` / apple-touch-icon, then header `<img>` logo. Download into `./assets/logo.png` or `./assets/logo.svg` and set `logoPath` accordingly. If only favicon exists, use it and note size in the summary.
|
|
398
|
+
3. **Contrast check:** if background is dark, set `activationScreen.theme` to `"dark"` and add `logoPathOnDark` when a light logo variant exists.
|
|
399
|
+
4. **Infer `cms.useCase`** from page content when obvious (restaurant / retail / corporate / etc.); otherwise `"other"`.
|
|
400
|
+
5. **Show a one-line summary** to the user (name + primary hex + logo path) before or while writing the file — do not ask a second branding question unless fetch failed.
|
|
401
|
+
6. If the URL is unreachable, blocked, or has no usable styles, say so and fall back to **Option B** manual questions for missing fields only.
|
|
402
|
+
|
|
403
|
+
### Step C — Write `brand.json`
|
|
404
|
+
|
|
405
|
+
Validate mentally against `brand.schema.json`. Minimum:
|
|
291
406
|
|
|
292
407
|
```json
|
|
293
408
|
{
|
|
294
|
-
"name": "<user name>",
|
|
295
|
-
"tagline": "<
|
|
409
|
+
"name": "<user name or site-derived name>",
|
|
410
|
+
"tagline": "<tagline or Digital signage>",
|
|
296
411
|
"targetPlatforms": ["tizen"],
|
|
297
412
|
"primaryColor": "#FF8A3D",
|
|
298
413
|
"secondaryColor": "#F5F3EF",
|
|
@@ -335,15 +450,18 @@ Then:
|
|
|
335
450
|
2. Apply Vercel-adapted `server.ts` (**export `listen()` HTTP server**).
|
|
336
451
|
3. Add/adjust `vercel.json` with **`fluid: true`** (or enable Fluid in the dashboard).
|
|
337
452
|
4. Do **not** add `.replit*`.
|
|
453
|
+
5. If Q2 = Vercel Blob: install `@vercel/blob` and wire the upload bridge.
|
|
338
454
|
|
|
339
455
|
### B. Environment Variables (Vercel dashboard + local)
|
|
340
456
|
|
|
341
457
|
| Name | Required | Notes |
|
|
342
458
|
|------|----------|--------|
|
|
343
|
-
| `SUPABASE_URL` |
|
|
344
|
-
| `
|
|
459
|
+
| `SUPABASE_URL` | If Q1 = Supabase | **Session pooler** `:6543` |
|
|
460
|
+
| `DATABASE_URL` | If Q1 = Neon | **Pooled** Neon string |
|
|
461
|
+
| `TOMORROWOS_STORE` | Yes | `supabase`, `postgres`, or `sqlite` |
|
|
345
462
|
| `DATABASE_SSL` | Yes (typical) | `true` |
|
|
346
463
|
| `CLOUDINARY_*` | If Q2 = Cloudinary | Three keys |
|
|
464
|
+
| `BLOB_READ_WRITE_TOKEN` | If Q2 = Vercel Blob | From Vercel Blob store |
|
|
347
465
|
| `PORT` | Optional | Vercel injects as needed |
|
|
348
466
|
|
|
349
467
|
Mirror non-secrets in `.env` for local `npm run dev` if helpful; never commit secrets.
|
|
@@ -376,8 +494,8 @@ Tell the user:
|
|
|
376
494
|
**Do:**
|
|
377
495
|
|
|
378
496
|
1. Control Panel HTML at `/`
|
|
379
|
-
2. Server status: Media OK when Cloudinary configured
|
|
380
|
-
3. Database: if ERROR shows **`ENETUNREACH` … IPv6 … `:5432`**, switch
|
|
497
|
+
2. Server status: Media OK when Cloudinary or Blob configured
|
|
498
|
+
3. Database: if ERROR shows **`ENETUNREACH` … IPv6 … `:5432`**, switch to **pooled** URL (Supabase `:6543` or Neon pooler) and redeploy — call this out explicitly to the user
|
|
381
499
|
4. Fluid compute on
|
|
382
500
|
|
|
383
501
|
**Do not block** on long WebSocket/device pairing tests unless the user asks.
|
|
@@ -388,13 +506,15 @@ Tell the user:
|
|
|
388
506
|
|
|
389
507
|
| Symptom | Likely cause | Fix |
|
|
390
508
|
|---------|--------------|-----|
|
|
391
|
-
| `ENETUNREACH` / IPv6 / `:5432` | Direct
|
|
392
|
-
| Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk |
|
|
393
|
-
| Uploads break / broken thumbs in prod | Local `public/uploads` | Cloudinary |
|
|
509
|
+
| `ENETUNREACH` / IPv6 / `:5432` | Direct Postgres URL | Use **pooled** URL (Supabase `:6543` or Neon pooler) |
|
|
510
|
+
| Control Panel OK, devices never stay paired after restart | SQLite / ephemeral disk | Use Supabase or Neon |
|
|
511
|
+
| Uploads break / broken thumbs in prod | Local `public/uploads` | Cloudinary or Vercel Blob |
|
|
512
|
+
| Blob uploads 401 / missing token | Blob not linked | Vercel Storage → Blob + `BLOB_READ_WRITE_TOKEN` |
|
|
394
513
|
| WebSocket fails / devices drop immediately | Fluid off / static-only / Next wrapper | Enable Fluid; sole Node `listen()` server |
|
|
395
514
|
| Agent created Next.js + TomorrowOS | Wrong scaffold | Remove Next; keep cms-starter |
|
|
396
515
|
| Secrets in git | Mistake | Rotate keys; move to Vercel Env Vars |
|
|
397
516
|
| Replit files present | Copied wrong protocol | Delete `.replit*`; use this file |
|
|
517
|
+
| Brand colours wrong from URL | SPA / blocked fetch | Manual Option B for colours + logo |
|
|
398
518
|
|
|
399
519
|
---
|
|
400
520
|
|
|
@@ -405,10 +525,10 @@ Tell the user:
|
|
|
405
525
|
| Secrets | Replit Secrets | Vercel Environment Variables |
|
|
406
526
|
| Deploy config | `.replit`, artifact `kind=web` | `vercel.json` + Fluid; **no** `.replit` |
|
|
407
527
|
| `hostingTarget` | `"here"` | `"vercel"` |
|
|
408
|
-
|
|
|
409
|
-
|
|
|
410
|
-
| Object Storage option | Replit Object Storage | N/A — prefer Cloudinary |
|
|
528
|
+
| Database | Supabase (primary) | Supabase **or Neon** (pooled); SQLite demo only |
|
|
529
|
+
| Media | Cloudinary or Replit Object Storage | Cloudinary **or Vercel Blob** |
|
|
411
530
|
| WebSocket | Long-lived Node (Autoscale) | Fluid compute + Node server export |
|
|
531
|
+
| Brand from URL | Not in Replit protocol | **Option A** in Question 3 — infer from website |
|
|
412
532
|
|
|
413
533
|
---
|
|
414
534
|
|
|
@@ -418,10 +538,10 @@ After setup, report:
|
|
|
418
538
|
|
|
419
539
|
1. **Live URL** (Production)
|
|
420
540
|
2. **Env Vars set** (names only — never values)
|
|
421
|
-
3. **Store:** Supabase pooler /
|
|
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)
|
|
541
|
+
3. **Store:** Supabase pooler / Neon pooled / SQLite (with warning)
|
|
542
|
+
4. **Media:** Cloudinary, Vercel Blob, or local (with warning)
|
|
543
|
+
5. **Brand:** name + `hostingTarget: vercel` (+ note if derived from website URL)
|
|
544
|
+
6. **Adaptations from Replit protocol:** list (e.g. skipped `.replit`, exported HTTP server, Fluid, pooler URL, Blob bridge)
|
|
425
545
|
7. **Open action:** if Database still ERROR, paste the pooler instruction again
|
|
426
546
|
|
|
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
|
|
547
|
+
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 pooled URL is correctly set for Production.
|
package/dist/tomorrowos.d.ts
CHANGED
|
@@ -136,6 +136,15 @@ export declare class TomorrowOS extends EventEmitter {
|
|
|
136
136
|
onOffTimer: DeviceOnOffTimer;
|
|
137
137
|
pushed: boolean;
|
|
138
138
|
}>;
|
|
139
|
+
/**
|
|
140
|
+
* Clear the daily on/off timer. The player keeps its current mute / power-save
|
|
141
|
+
* state until a new timer is set.
|
|
142
|
+
*/
|
|
143
|
+
clearDeviceOnOffTimer(deviceId: string): Promise<{
|
|
144
|
+
deviceId: string;
|
|
145
|
+
onOffTimer: null;
|
|
146
|
+
pushed: boolean;
|
|
147
|
+
}>;
|
|
139
148
|
private pushOnOffTimerToDevice;
|
|
140
149
|
setDeviceName(deviceId: string, deviceName: string): Promise<{
|
|
141
150
|
deviceId: string;
|
package/dist/tomorrowos.d.ts.map
CHANGED
|
@@ -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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"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -441,15 +441,33 @@ export class TomorrowOS extends EventEmitter {
|
|
|
441
441
|
const pushed = await this.pushOnOffTimerToDevice(id);
|
|
442
442
|
return { deviceId: id, onOffTimer, pushed };
|
|
443
443
|
}
|
|
444
|
+
/**
|
|
445
|
+
* Clear the daily on/off timer. The player keeps its current mute / power-save
|
|
446
|
+
* state until a new timer is set.
|
|
447
|
+
*/
|
|
448
|
+
async clearDeviceOnOffTimer(deviceId) {
|
|
449
|
+
const id = String(deviceId || "").trim();
|
|
450
|
+
if (!id) {
|
|
451
|
+
throw new Error("deviceId is required");
|
|
452
|
+
}
|
|
453
|
+
const existing = await this.store.getPairedDevice(id);
|
|
454
|
+
if (!existing) {
|
|
455
|
+
throw new Error("Device is not paired");
|
|
456
|
+
}
|
|
457
|
+
const { onOffTimer: _removed, ...rest } = existing;
|
|
458
|
+
await this.store.setPairedDevice(id, rest);
|
|
459
|
+
const pushed = await this.pushOnOffTimerToDevice(id);
|
|
460
|
+
return { deviceId: id, onOffTimer: null, pushed };
|
|
461
|
+
}
|
|
444
462
|
async pushOnOffTimerToDevice(deviceId) {
|
|
445
463
|
const existing = await this.store.getPairedDevice(deviceId);
|
|
446
|
-
if (!existing
|
|
464
|
+
if (!existing)
|
|
447
465
|
return false;
|
|
448
466
|
const ws = this.devices.get(deviceId);
|
|
449
467
|
if (!ws || ws.readyState !== WebSocket.OPEN)
|
|
450
468
|
return false;
|
|
451
469
|
try {
|
|
452
|
-
const result = await this.sendCommandToSocket(ws, deviceId, "device.display.setOnOffTimer", { onOffTimer: existing.onOffTimer }, 15_000);
|
|
470
|
+
const result = await this.sendCommandToSocket(ws, deviceId, "device.display.setOnOffTimer", { onOffTimer: existing.onOffTimer ?? null }, 15_000);
|
|
453
471
|
return result.status === "success" || result.status === "accepted";
|
|
454
472
|
}
|
|
455
473
|
catch (err) {
|
|
@@ -1178,8 +1196,14 @@ export class TomorrowOS extends EventEmitter {
|
|
|
1178
1196
|
const deviceId = decodeURIComponent(deviceOnOffTimer[1]);
|
|
1179
1197
|
const body = (await readJsonBody(req));
|
|
1180
1198
|
try {
|
|
1181
|
-
|
|
1182
|
-
|
|
1199
|
+
if (body.clear === true || body.onOffTimer === null) {
|
|
1200
|
+
const result = await this.clearDeviceOnOffTimer(deviceId);
|
|
1201
|
+
sendJson(res, 200, { status: "success", ...result });
|
|
1202
|
+
}
|
|
1203
|
+
else {
|
|
1204
|
+
const result = await this.setDeviceOnOffTimer(deviceId, body.onOffTimer ?? body);
|
|
1205
|
+
sendJson(res, 200, { status: "success", ...result });
|
|
1206
|
+
}
|
|
1183
1207
|
}
|
|
1184
1208
|
catch (e) {
|
|
1185
1209
|
const msg = e instanceof Error ? e.message : "On/off timer failed";
|
|
@@ -1187,6 +1211,18 @@ export class TomorrowOS extends EventEmitter {
|
|
|
1187
1211
|
}
|
|
1188
1212
|
return;
|
|
1189
1213
|
}
|
|
1214
|
+
if (req.method === "DELETE" && deviceOnOffTimer) {
|
|
1215
|
+
const deviceId = decodeURIComponent(deviceOnOffTimer[1]);
|
|
1216
|
+
try {
|
|
1217
|
+
const result = await this.clearDeviceOnOffTimer(deviceId);
|
|
1218
|
+
sendJson(res, 200, { status: "success", ...result });
|
|
1219
|
+
}
|
|
1220
|
+
catch (e) {
|
|
1221
|
+
const msg = e instanceof Error ? e.message : "On/off timer clear failed";
|
|
1222
|
+
sendJson(res, 400, { status: "failed", error: msg });
|
|
1223
|
+
}
|
|
1224
|
+
return;
|
|
1225
|
+
}
|
|
1190
1226
|
if (req.method === "GET" && pathname === "/playlists") {
|
|
1191
1227
|
const playlists = await this.playlists.listPlaylists();
|
|
1192
1228
|
sendJson(res, 200, { status: "success", playlists });
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.22",
|
|
4
4
|
"description": "TomorrowOS CMS server SDK - WebSocket transport, pairing, device commands, optional static CMS UI. Includes CLI (tomorrowos init / build) and starter templates.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "my-cms",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.22",
|
|
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.
|
|
16
|
+
"@tomorrowos/sdk": "^0.9.22",
|
|
17
17
|
"dotenv": "^17.2.3",
|
|
18
18
|
"tsx": "^4.19.0"
|
|
19
19
|
},
|
|
@@ -187,6 +187,7 @@
|
|
|
187
187
|
<h2>On / off timer</h2>
|
|
188
188
|
<p class="hint" id="onOffTimerModalHint">
|
|
189
189
|
Daily on/off times follow the device clock. Off blacks the screen; the player stays powered and connected to the CMS.
|
|
190
|
+
Removing the timer stops the schedule and leaves the screen in its current on or off state until you set a timer again.
|
|
190
191
|
</p>
|
|
191
192
|
<div class="on-off-timer-grid">
|
|
192
193
|
<label class="field-label">
|
|
@@ -200,6 +201,7 @@
|
|
|
200
201
|
</div>
|
|
201
202
|
<div class="modal-actions">
|
|
202
203
|
<button type="button" id="onOffTimerSaveBtn" class="primary">Save</button>
|
|
204
|
+
<button type="button" id="onOffTimerRemoveBtn" class="danger hidden">Remove timer</button>
|
|
203
205
|
<button type="button" data-close-on-off-timer-modal="1">Cancel</button>
|
|
204
206
|
</div>
|
|
205
207
|
</div>
|
|
@@ -1102,7 +1102,7 @@ function appendHardwareTimerStatusRow(meta, device) {
|
|
|
1102
1102
|
row.className = "device-meta-row device-meta-row--timer-status";
|
|
1103
1103
|
|
|
1104
1104
|
const dt = document.createElement("dt");
|
|
1105
|
-
dt.textContent = "Hardware
|
|
1105
|
+
dt.textContent = "Hardware timer status";
|
|
1106
1106
|
|
|
1107
1107
|
const dd = document.createElement("dd");
|
|
1108
1108
|
const statusWrap = document.createElement("span");
|
|
@@ -1895,11 +1895,17 @@ function openOnOffTimerModal(deviceId) {
|
|
|
1895
1895
|
const modal = document.getElementById("onOffTimerModal");
|
|
1896
1896
|
const onEl = document.getElementById("onOffTimerTurnOnAt");
|
|
1897
1897
|
const offEl = document.getElementById("onOffTimerTurnOffAt");
|
|
1898
|
+
const removeBtn = document.getElementById("onOffTimerRemoveBtn");
|
|
1898
1899
|
if (!modal || !onEl || !offEl) return;
|
|
1899
1900
|
|
|
1900
1901
|
const timer = device.onOffTimer || {};
|
|
1902
|
+
const configured = hasConfiguredOnOffTimer(device.onOffTimer);
|
|
1901
1903
|
onEl.value = normalizeTimeInputValue(timer.turnOnAt, "06:00");
|
|
1902
1904
|
offEl.value = normalizeTimeInputValue(timer.turnOffAt, "18:00");
|
|
1905
|
+
if (removeBtn) {
|
|
1906
|
+
removeBtn.classList.toggle("hidden", !configured);
|
|
1907
|
+
removeBtn.disabled = false;
|
|
1908
|
+
}
|
|
1903
1909
|
modal.classList.remove("hidden");
|
|
1904
1910
|
}
|
|
1905
1911
|
|
|
@@ -1916,6 +1922,7 @@ async function saveOnOffTimerModal() {
|
|
|
1916
1922
|
const onEl = document.getElementById("onOffTimerTurnOnAt");
|
|
1917
1923
|
const offEl = document.getElementById("onOffTimerTurnOffAt");
|
|
1918
1924
|
const saveBtn = document.getElementById("onOffTimerSaveBtn");
|
|
1925
|
+
const removeBtn = document.getElementById("onOffTimerRemoveBtn");
|
|
1919
1926
|
if (!onEl || !offEl) return;
|
|
1920
1927
|
|
|
1921
1928
|
const onOffTimer = {
|
|
@@ -1929,6 +1936,7 @@ async function saveOnOffTimerModal() {
|
|
|
1929
1936
|
}
|
|
1930
1937
|
|
|
1931
1938
|
if (saveBtn) saveBtn.disabled = true;
|
|
1939
|
+
if (removeBtn) removeBtn.disabled = true;
|
|
1932
1940
|
try {
|
|
1933
1941
|
const res = await fetch(`/device/${encodeURIComponent(deviceId)}/on-off-timer`, {
|
|
1934
1942
|
method: "POST",
|
|
@@ -1947,6 +1955,43 @@ async function saveOnOffTimerModal() {
|
|
|
1947
1955
|
alert(err?.message || "Could not save on/off timer");
|
|
1948
1956
|
} finally {
|
|
1949
1957
|
if (saveBtn) saveBtn.disabled = false;
|
|
1958
|
+
if (removeBtn) removeBtn.disabled = false;
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
async function removeOnOffTimerModal() {
|
|
1963
|
+
const deviceId = onOffTimerModalDeviceId;
|
|
1964
|
+
if (!deviceId) return;
|
|
1965
|
+
|
|
1966
|
+
if (
|
|
1967
|
+
!confirm(
|
|
1968
|
+
"Remove the on/off timer? The screen will stay in its current on or off state until you set a timer again."
|
|
1969
|
+
)
|
|
1970
|
+
) {
|
|
1971
|
+
return;
|
|
1972
|
+
}
|
|
1973
|
+
|
|
1974
|
+
const saveBtn = document.getElementById("onOffTimerSaveBtn");
|
|
1975
|
+
const removeBtn = document.getElementById("onOffTimerRemoveBtn");
|
|
1976
|
+
if (saveBtn) saveBtn.disabled = true;
|
|
1977
|
+
if (removeBtn) removeBtn.disabled = true;
|
|
1978
|
+
try {
|
|
1979
|
+
const res = await fetch(`/device/${encodeURIComponent(deviceId)}/on-off-timer`, {
|
|
1980
|
+
method: "DELETE"
|
|
1981
|
+
});
|
|
1982
|
+
const data = await res.json();
|
|
1983
|
+
if (!res.ok || data.status === "failed") {
|
|
1984
|
+
alert(data.error || "Could not remove on/off timer");
|
|
1985
|
+
return;
|
|
1986
|
+
}
|
|
1987
|
+
showResult({ deviceId, onOffTimer: null, pushed: data.pushed });
|
|
1988
|
+
closeOnOffTimerModal();
|
|
1989
|
+
await fetchDevices();
|
|
1990
|
+
} catch (err) {
|
|
1991
|
+
alert(err?.message || "Could not remove on/off timer");
|
|
1992
|
+
} finally {
|
|
1993
|
+
if (saveBtn) saveBtn.disabled = false;
|
|
1994
|
+
if (removeBtn) removeBtn.disabled = false;
|
|
1950
1995
|
}
|
|
1951
1996
|
}
|
|
1952
1997
|
|
|
@@ -2275,6 +2320,9 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
2275
2320
|
document
|
|
2276
2321
|
.getElementById("onOffTimerSaveBtn")
|
|
2277
2322
|
?.addEventListener("click", () => void saveOnOffTimerModal());
|
|
2323
|
+
document
|
|
2324
|
+
.getElementById("onOffTimerRemoveBtn")
|
|
2325
|
+
?.addEventListener("click", () => void removeOnOffTimerModal());
|
|
2278
2326
|
document.querySelectorAll("[data-close-download-failed-modal]").forEach((el) => {
|
|
2279
2327
|
el.addEventListener("click", closeDownloadFailedModal);
|
|
2280
2328
|
});
|