@tomorrowos/sdk 0.9.4 → 0.9.6
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/REPLIT_SETUP.md +53 -77
- package/dist/cloudinary-storage.d.ts +2 -0
- package/dist/cloudinary-storage.d.ts.map +1 -1
- package/dist/cloudinary-storage.js +5 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/server-status.d.ts +30 -0
- package/dist/server-status.d.ts.map +1 -0
- package/dist/server-status.js +244 -0
- package/dist/tomorrowos.d.ts +1 -0
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +9 -0
- package/package.json +1 -1
- package/templates/cms-starter/package.json +2 -2
- package/templates/cms-starter/public/index.html +10 -0
- package/templates/cms-starter/public/methods.js +163 -0
- package/templates/cms-starter/public/panel.css +147 -0
package/REPLIT_SETUP.md
CHANGED
|
@@ -12,7 +12,7 @@ When a human pastes a prompt like this, **read this file and start Question 1 im
|
|
|
12
12
|
```text
|
|
13
13
|
Follow @tomorrowos/sdk REPLIT_SETUP.md and set up my TomorrowOS CMS.
|
|
14
14
|
Ask only the questions listed in REPLIT_SETUP.md, in order. Do not use LLM_PROMPT.md.
|
|
15
|
-
Artifact kind must be **web** (never **api**). Question
|
|
15
|
+
Artifact kind must be **web** (never **api**). Question 3 only updates **brand.json**.
|
|
16
16
|
After all setup questions are answered (including Cloudinary Secrets if you chose Cloudinary), configure and start — minimal verification only (Web app type + Preview at /). **Never test Supabase connection** — save `SUPABASE_URL` and move on. No Supabase or WebSocket tests.
|
|
17
17
|
Do not skip steps.
|
|
18
18
|
```
|
|
@@ -58,21 +58,20 @@ Replit Agent must ask **only** the questions defined in **this file**, in **this
|
|
|
58
58
|
|
|
59
59
|
| Step | Section | When |
|
|
60
60
|
|------|---------|------|
|
|
61
|
-
| 1 | **Question 1** —
|
|
62
|
-
| 2 | **Question 2** —
|
|
63
|
-
| 3 | **Question 3** —
|
|
64
|
-
| 4 | **Question 4** — Brand / TomorrowOS app look | Always last, before execution |
|
|
61
|
+
| 1 | **Question 1** — Supabase connection string | Always |
|
|
62
|
+
| 2 | **Question 2** — Media storage (+ Cloudinary Secrets if chosen) | Always (after Q1) |
|
|
63
|
+
| 3 | **Question 3** — Brand / TomorrowOS app look | Always last, before execution |
|
|
65
64
|
|
|
66
|
-
**That is the complete list.** There are no other setup questions. There is **no** “Question
|
|
65
|
+
**That is the complete list.** There are no other setup questions. **Do not** ask how many screens / devices. There is **no** “Question 2b” — Cloudinary credentials are collected **inside Question 2**, immediately after the user chooses Cloudinary.
|
|
67
66
|
|
|
68
|
-
**Critical:** Question
|
|
67
|
+
**Critical:** Question 2 is **not complete** when the user says “Cloudinary” or “yes”. You **must** immediately show the Cloudinary Secrets input (same step — do **not** label a new question) and collect `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, and `CLOUDINARY_API_SECRET` **before** Question 3. Do **not** skip to branding with placeholder or invented credentials.
|
|
69
68
|
|
|
70
69
|
|
|
71
|
-
If the user volunteers extra info early (e.g. brand name before
|
|
70
|
+
If the user volunteers extra info early (e.g. brand name before Q3), **record it** and still ask the current question’s required fields you do not yet have. Do not skip ahead to execution until Q1–Q3 are complete (Question 2 includes Cloudinary Secrets when applicable).
|
|
72
71
|
|
|
73
72
|
If the user says “just set it up, don’t ask questions,” respond:
|
|
74
73
|
|
|
75
|
-
> I need a few quick answers from REPLIT_SETUP.md (
|
|
74
|
+
> I need a few quick answers from REPLIT_SETUP.md (Supabase connection string, media storage — plus Cloudinary credentials if you choose Cloudinary — and branding). It takes about a minute and ensures the CMS deploys correctly on Replit.
|
|
76
75
|
|
|
77
76
|
Then ask **Question 1** — do not switch to `LLM_PROMPT.md`.
|
|
78
77
|
|
|
@@ -80,9 +79,9 @@ Then ask **Question 1** — do not switch to `LLM_PROMPT.md`.
|
|
|
80
79
|
|
|
81
80
|
## Hard rules
|
|
82
81
|
|
|
83
|
-
1. **Ask only REPLIT_SETUP.md questions 1–
|
|
84
|
-
2. **One question at a time.** Wait for the user’s answer before asking the next question (unless they already answered several in one message). **Exception:** If the user chooses Cloudinary in Question
|
|
85
|
-
3. **Do not invent**
|
|
82
|
+
1. **Ask only REPLIT_SETUP.md questions 1–3** (see **Questionnaire scope**). Never use `LLM_PROMPT.md` on Replit.
|
|
83
|
+
2. **One question at a time.** Wait for the user’s answer before asking the next question (unless they already answered several in one message). **Exception:** If the user chooses Cloudinary in Question 2, **stay on Question 2** and immediately show the Secrets form — do **not** open a separate “2b” step or announce a new question number.
|
|
84
|
+
3. **Do not invent** Cloudinary credentials, Supabase URLs, or brand colours. **Do not ask** screen / device count.
|
|
86
85
|
4. **Do not use Replit’s reserved `DATABASE_URL` Secret for Supabase.** On Replit, `DATABASE_URL` is often pre-claimed. Always use **`SUPABASE_URL`** for TomorrowOS ↔ Supabase.
|
|
87
86
|
5. **Prefer `npx @tomorrowos/sdk init`** (or the package’s `templates/cms-starter`) as the project seed. Do not rebuild pairing, WebSocket, or playlist APIs from scratch.
|
|
88
87
|
6. **Never commit secrets** into git. Put credentials only in Replit **Secrets** (and optionally mirror non-secret flags in `.env` that reference env vars).
|
|
@@ -91,12 +90,11 @@ Then ask **Question 1** — do not switch to `LLM_PROMPT.md`.
|
|
|
91
90
|
9. **Supabase: configure only — never test.** Save `SUPABASE_URL` + `TOMORROWOS_STORE=supabase` and proceed. **Do not** ping Postgres, run `psql`, start the server solely to verify DB connectivity, or treat log errors like `ENOTFOUND` / `database connection failure` in **Replit dev / Preview** as a failed setup. Supabase often works only after **Publish** (production networking), not in the dev shell.
|
|
92
91
|
10. **Artifact kind = `web` only.** **`kind = "api"` is forbidden** for the TomorrowOS CMS artifact. See **IRON RULE** above. Setup is incomplete if Publish would still see an `api` artifact.
|
|
93
92
|
|
|
94
|
-
**Question order
|
|
93
|
+
**Question order (always):**
|
|
95
94
|
|
|
96
|
-
-
|
|
97
|
-
- If **> 5 screens:** Q1 → Q2 → Q3 → Q4 → execution checklist
|
|
95
|
+
- Q1 → Q2 → Q3 → execution checklist
|
|
98
96
|
|
|
99
|
-
Never insert extra questions between these steps. When the user chooses Cloudinary, **Question
|
|
97
|
+
Never insert extra questions between these steps. When the user chooses Cloudinary, **Question 2 continues** with the Secrets form immediately — then proceed to Q3 only after secrets are saved.
|
|
100
98
|
|
|
101
99
|
---
|
|
102
100
|
|
|
@@ -232,7 +230,7 @@ Allowed extras (optional): `"build-player": "tomorrowos build --platform tizen"`
|
|
|
232
230
|
"node": ">=20"
|
|
233
231
|
},
|
|
234
232
|
"dependencies": {
|
|
235
|
-
"@tomorrowos/sdk": "^0.9.
|
|
233
|
+
"@tomorrowos/sdk": "^0.9.6",
|
|
236
234
|
"dotenv": "^17.2.3",
|
|
237
235
|
"tsx": "^4.19.0"
|
|
238
236
|
},
|
|
@@ -330,13 +328,13 @@ Replace `YOUR-REPLIT-DOMAIN` with the actual published hostname (e.g. `my-cms.re
|
|
|
330
328
|
|
|
331
329
|
## Post-Q&A: minimal verification only
|
|
332
330
|
|
|
333
|
-
After Questions 1–
|
|
331
|
+
After Questions 1–3, **configure → `npm install` → `npm run start` → done.**
|
|
334
332
|
|
|
335
333
|
### Supabase: never test in Replit dev (mandatory)
|
|
336
334
|
|
|
337
|
-
|
|
335
|
+
Setup always uses Supabase: **save credentials + wire `server.ts` + start** — **nothing else**.
|
|
338
336
|
|
|
339
|
-
**Forbidden after Q&A (and after Question
|
|
337
|
+
**Forbidden after Q&A (and after Question 1):**
|
|
340
338
|
|
|
341
339
|
- ❌ “Testing Supabase connection…”
|
|
342
340
|
- ❌ `psql`, `pg`, Prisma `db pull`, or any one-off DB probe script
|
|
@@ -374,34 +372,17 @@ Then (and only then) check: API vs Web type → `.replit` ports/`deploymentTarge
|
|
|
374
372
|
|
|
375
373
|
---
|
|
376
374
|
|
|
377
|
-
## Question 1 —
|
|
375
|
+
## Question 1 — Supabase connection (always)
|
|
378
376
|
|
|
379
|
-
> **This is the first
|
|
377
|
+
> **This is the first setup question.** Always configure Supabase. **Do not** ask how many screens / devices.
|
|
380
378
|
|
|
381
|
-
**
|
|
382
|
-
|
|
383
|
-
> How many TomorrowOS screens (devices) do you expect to manage with this CMS?
|
|
384
|
-
>
|
|
385
|
-
> - **5 or fewer** → I’ll use the default **SQLite** database (simple, good for demos and small venues).
|
|
386
|
-
> - **More than 5** → I’ll configure **Supabase (Postgres)** so pairings and playlists scale more reliably.
|
|
387
|
-
|
|
388
|
-
**Branching:**
|
|
389
|
-
|
|
390
|
-
| Answer | Store driver | Next |
|
|
391
|
-
|--------|--------------|------|
|
|
392
|
-
| ≤ 5 | `TOMORROWOS_STORE=sqlite` | Skip Question 2 → go to Question 3 |
|
|
393
|
-
| > 5 | `TOMORROWOS_STORE=supabase` | Ask Question 2 |
|
|
394
|
-
| Unclear | Ask again | Do not proceed |
|
|
379
|
+
**Preferred on Replit:** open an **input / Secrets** UI where the user pastes the connection string into a field for:
|
|
395
380
|
|
|
396
|
-
|
|
381
|
+
- `SUPABASE_URL`
|
|
397
382
|
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
## Question 2 — Supabase connection (only if > 5 screens)
|
|
401
|
-
|
|
402
|
-
**Ask exactly:**
|
|
383
|
+
**Ask exactly (copy wording; do not substitute LLM_PROMPT questions):**
|
|
403
384
|
|
|
404
|
-
> Paste your Supabase Postgres connection string. I will store it as the Replit Secret **`SUPABASE_URL`** (not `DATABASE_URL` — that name is often reserved by Replit).
|
|
385
|
+
> Paste your Supabase Postgres connection string below. I will store it as the Replit Secret **`SUPABASE_URL`** (not `DATABASE_URL` — that name is often reserved by Replit).
|
|
405
386
|
>
|
|
406
387
|
> Example shape: `postgresql://postgres.[PROJECT]:[PASSWORD]@aws-0-[REGION].pooler.supabase.com:6543/postgres`
|
|
407
388
|
>
|
|
@@ -449,13 +430,16 @@ DATABASE_SSL=true
|
|
|
449
430
|
```
|
|
450
431
|
|
|
451
432
|
5. **Do not** write the real password into committed files.
|
|
452
|
-
6. **Do not** test the Supabase connection after saving the Secret — **configuration only**. No probe, no `npm run start` just to verify DB, no fixing “connection failure” in dev. Saving `SUPABASE_URL` completes Question
|
|
433
|
+
6. **Do not** test the Supabase connection after saving the Secret — **configuration only**. No probe, no `npm run start` just to verify DB, no fixing “connection failure” in dev. Saving `SUPABASE_URL` completes Question 1.
|
|
434
|
+
7. **Do not** proceed to Question 2 until `SUPABASE_URL` is saved.
|
|
453
435
|
|
|
454
|
-
If the user refuses Supabase
|
|
436
|
+
If the user refuses Supabase, warn that Replit fleets need a durable Postgres store, then offer SQLite only after they explicitly confirm.
|
|
437
|
+
|
|
438
|
+
**Store in `brand.json` later (Question 3):** `cms.hostingTarget`: `"here"`. Do **not** ask for `expectedScreens`; leave a default (e.g. `5`) when writing `brand.json`.
|
|
455
439
|
|
|
456
440
|
---
|
|
457
441
|
|
|
458
|
-
## Question
|
|
442
|
+
## Question 2 — Media storage (uploads / thumbnails)
|
|
459
443
|
|
|
460
444
|
### Step A — Ask storage choice
|
|
461
445
|
|
|
@@ -471,9 +455,9 @@ If the user refuses Supabase but chose > 5 screens, warn them SQLite on Replit i
|
|
|
471
455
|
>
|
|
472
456
|
> Do you want me to set up **Cloudinary**? (yes / no)
|
|
473
457
|
|
|
474
|
-
### Step B — If YES / Cloudinary (same Question
|
|
458
|
+
### Step B — If YES / Cloudinary (same Question 2 — no “2b”, no extra question)
|
|
475
459
|
|
|
476
|
-
**Do this immediately** when the user chooses Cloudinary. **Do not** say “Question
|
|
460
|
+
**Do this immediately** when the user chooses Cloudinary. **Do not** say “Question 2b”, “next step”, or “Great — I’ll use Cloudinary” as a separate message before the form. **Go straight to the Secrets input.**
|
|
477
461
|
|
|
478
462
|
**Preferred on Replit:** open the **Replit Secrets** UI with fields for:
|
|
479
463
|
|
|
@@ -497,10 +481,10 @@ If the user refuses Supabase but chose > 5 screens, warn them SQLite on Replit i
|
|
|
497
481
|
1. Save Replit Secrets: `CLOUDINARY_CLOUD_NAME`, `CLOUDINARY_API_KEY`, `CLOUDINARY_API_SECRET` (and optional `CLOUDINARY_FOLDER`).
|
|
498
482
|
2. **Do not** write real secrets into committed files.
|
|
499
483
|
3. **Do not** invent or placeholder credentials.
|
|
500
|
-
4. **Do not** proceed to Question
|
|
484
|
+
4. **Do not** proceed to Question 3 until all three required Secrets exist.
|
|
501
485
|
5. **Do not** run a Cloudinary upload test — configuration only.
|
|
502
486
|
|
|
503
|
-
**If the user does not have a Cloudinary account yet:** tell them to sign up at cloudinary.com and return with API keys. **Wait** on Question
|
|
487
|
+
**If the user does not have a Cloudinary account yet:** tell them to sign up at cloudinary.com and return with API keys. **Wait** on Question 2 — do not skip to Question 3.
|
|
504
488
|
|
|
505
489
|
The SDK auto-detects these env vars and uploads media to Cloudinary. New uploads will return `https://res.cloudinary.com/...` URLs.
|
|
506
490
|
|
|
@@ -513,17 +497,15 @@ The SDK auto-detects these env vars and uploads media to Cloudinary. New uploads
|
|
|
513
497
|
3. Warn: if Object Storage IAM denies `storage.objects.create`, uploads will fail with a Google Cloud Storage permission error — fix Object Storage permissions or switch to Cloudinary.
|
|
514
498
|
4. Do **not** invent Cloudinary credentials.
|
|
515
499
|
|
|
516
|
-
**Also persist metadata:** keep `data/tomorrowos.db` (SQLite path) on durable storage when using SQLite.
|
|
517
|
-
|
|
518
500
|
---
|
|
519
501
|
|
|
520
|
-
## Question
|
|
502
|
+
## Question 3 — Brand / TomorrowOS app look (`brand.json` only)
|
|
521
503
|
|
|
522
|
-
> **Scope:** Question
|
|
504
|
+
> **Scope:** Question 3 answers **only** update **`brand.json`** — colours, name, tagline, logo path, etc. They configure how the **TomorrowOS player app** looks and metadata flags.
|
|
523
505
|
>
|
|
524
|
-
> **Question
|
|
506
|
+
> **Question 3 does NOT affect:** Replit Publish, artifact `kind`, `.replit`, `server.ts`, Secrets, Supabase, Cloudinary, ports, deployment target, or CMS server wiring. **Never** change deploy/runtime config based on branding answers.
|
|
525
507
|
|
|
526
|
-
> **This is the only branding / platform / use-case question block.** Do not ask separate LLM_PROMPT “target platform”, “use case”, or “hosting” questions before this.
|
|
508
|
+
> **This is the only branding / platform / use-case question block.** Do not ask separate LLM_PROMPT “target platform”, “use case”, or “hosting” questions before this. **Do not** ask screen count.
|
|
527
509
|
|
|
528
510
|
**Ask exactly (one message; user may answer in one reply):**
|
|
529
511
|
|
|
@@ -568,7 +550,7 @@ If the user only gives a name and primary colour, use defaults for the rest and
|
|
|
568
550
|
}
|
|
569
551
|
```
|
|
570
552
|
|
|
571
|
-
|
|
553
|
+
Do **not** ask for screen count. Keep a default `cms.expectedScreens` (e.g. `5`) unless the user already volunteered a number. Always set `cms.hostingTarget` to `"here"` on Replit without asking. Set `cmsEndpoint` only if the user already knows their public `wss://…replit.app` URL; otherwise leave it out and tell them to pair TVs using the published HTTPS URL (player converts `https://` → `wss://`).
|
|
572
554
|
|
|
573
555
|
---
|
|
574
556
|
|
|
@@ -593,14 +575,7 @@ If `init` is inappropriate (existing customised project), merge carefully:
|
|
|
593
575
|
|
|
594
576
|
### B. Apply store + media Secrets
|
|
595
577
|
|
|
596
|
-
**
|
|
597
|
-
|
|
598
|
-
```env
|
|
599
|
-
TOMORROWOS_STORE=sqlite
|
|
600
|
-
TOMORROWOS_DB_PATH=./data/tomorrowos.db
|
|
601
|
-
```
|
|
602
|
-
|
|
603
|
-
**Supabase (>5):**
|
|
578
|
+
**Supabase (always from Question 1):**
|
|
604
579
|
|
|
605
580
|
```env
|
|
606
581
|
TOMORROWOS_STORE=supabase
|
|
@@ -669,8 +644,8 @@ Only investigate further if Preview is empty, server crashed, or user reports Pu
|
|
|
669
644
|
|
|
670
645
|
### Forbidden questionnaire sources
|
|
671
646
|
|
|
672
|
-
- **`LLM_PROMPT.md`** — not used on Replit setup; its
|
|
673
|
-
- Standalone “target platform” / “hosting target” / “use case” prompts outside Question
|
|
647
|
+
- **`LLM_PROMPT.md`** — not used on Replit setup; its questions are **replaced** by REPLIT_SETUP Questions 1–3
|
|
648
|
+
- Standalone “target platform” / “hosting target” / “use case” / **screen count** prompts outside Question 3
|
|
674
649
|
|
|
675
650
|
Do **not** ask the user how to implement:
|
|
676
651
|
|
|
@@ -685,24 +660,23 @@ Do **not**:
|
|
|
685
660
|
- Change the production start command away from `tsx server.ts` / `npm run start`
|
|
686
661
|
- Point Supabase at Replit’s reserved `DATABASE_URL` when `SUPABASE_URL` should be used
|
|
687
662
|
- Skip Cloudinary recommendation without stating the persistence tradeoff
|
|
688
|
-
- **Skip Cloudinary Secrets** after the user chooses Cloudinary — always collect `CLOUDINARY_*` inside Question
|
|
689
|
-
- **Announce “Question
|
|
663
|
+
- **Skip Cloudinary Secrets** after the user chooses Cloudinary — always collect `CLOUDINARY_*` inside Question 2 before Question 3
|
|
664
|
+
- **Announce “Question 2b”** or a separate follow-up question — show the Secrets form immediately instead
|
|
690
665
|
- Claim uploads are “permanent” on Replit local disk without Object Storage or Cloudinary
|
|
691
666
|
- Leave `tsx` only in `devDependencies` if the Deploy pipeline installs production deps only
|
|
692
667
|
- Use Canvas preview as the primary CMS verification surface
|
|
693
668
|
- Serve a different app or API-only response at `/` instead of the Control Panel
|
|
694
669
|
- Create the Repl as **API-only** or publish with **Static Deployment** only
|
|
695
670
|
- Set or leave artifact **`kind = "api"`** for the TomorrowOS CMS
|
|
696
|
-
- Change `.replit`, `server.ts`, Secrets, or deployment config based on **Question
|
|
671
|
+
- Change `.replit`, `server.ts`, Secrets, or deployment config based on **Question 3** branding answers (those go in **`brand.json` only**)
|
|
697
672
|
- Omit `[[ports]]` / `deploymentTarget` from `.replit` when setting up for Publish
|
|
698
673
|
- Run **Supabase connection tests**, **WebSocket console tests**, or other post-setup smoke tests unless Preview fails or the user asks
|
|
699
674
|
- **Debug or “fix” Supabase** because dev logs show `getaddrinfo ENOTFOUND` or `database connection failure` — save config and finish setup instead
|
|
675
|
+
- Ask how many screens / devices (≤5 vs >5) — always start with Supabase
|
|
700
676
|
|
|
701
677
|
---
|
|
702
678
|
|
|
703
|
-
## SQLite
|
|
704
|
-
|
|
705
|
-
Default starter is fine:
|
|
679
|
+
## SQLite fallback — `server.ts` reference (only if user refuses Supabase)
|
|
706
680
|
|
|
707
681
|
```ts
|
|
708
682
|
const store = createTomorrowOSStore({
|
|
@@ -738,14 +712,16 @@ TOMORROWOS_STORE=sqlite
|
|
|
738
712
|
|
|
739
713
|
## Protocol version
|
|
740
714
|
|
|
741
|
-
`replit-setup/1.
|
|
715
|
+
`replit-setup/1.8` — aligned with TomorrowOS protocol `1.0` and `@tomorrowos/sdk` store drivers `sqlite` | `supabase` | `postgres` | `memory`.
|
|
716
|
+
|
|
717
|
+
**Changelog 1.8:** Question 1 is always Supabase connection string (input / Secret). Removed screen-count branching (≤5 SQLite vs >5 Supabase). Media storage is Question 2; branding is Question 3.
|
|
742
718
|
|
|
743
|
-
**Changelog 1.7:** **IRON RULE** — artifact **`kind = "web"` only** (never `api`); Question 4 **only** writes `brand.json` (no deploy/server changes).
|
|
719
|
+
**Changelog 1.7:** **IRON RULE** — artifact **`kind = "web"` only** (never `api`); Question 4 (**now 3**) **only** writes `brand.json` (no deploy/server changes).
|
|
744
720
|
|
|
745
721
|
**Changelog 1.6:** **Never test Supabase** in Replit dev — `ENOTFOUND` in Preview logs is expected.
|
|
746
722
|
|
|
747
|
-
**Changelog 1.5:** Cloudinary Secrets inside
|
|
723
|
+
**Changelog 1.5:** Cloudinary Secrets inside media question — no “3b” step.
|
|
748
724
|
|
|
749
|
-
**Changelog 1.4:** mandatory Cloudinary credential collection before
|
|
725
|
+
**Changelog 1.4:** mandatory Cloudinary credential collection before branding (superseded by 1.5 flow).
|
|
750
726
|
|
|
751
727
|
**Changelog 1.3:** minimal post-Q&A verification — no Supabase/WS smoke tests; prioritize **Web app vs API** only.
|
|
@@ -17,4 +17,6 @@ export declare function uploadBufferToCloudinary(body: Buffer, options: {
|
|
|
17
17
|
filename: string;
|
|
18
18
|
}): Promise<CloudinaryUploadResult>;
|
|
19
19
|
export declare function deleteCloudinaryAsset(config: CloudinaryStorageConfig, publicId: string, resourceType?: string): Promise<void>;
|
|
20
|
+
/** Lightweight credential / reachability check for CMS status UI. */
|
|
21
|
+
export declare function pingCloudinary(config: CloudinaryStorageConfig): Promise<void>;
|
|
20
22
|
//# sourceMappingURL=cloudinary-storage.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cloudinary-storage.d.ts","sourceRoot":"","sources":["../src/cloudinary-storage.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,uBAAuB,GAAG,IAAI,CAoBhC;AAWD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,MAAM,EAAE,uBAAuB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,sBAAsB,CAAC,CAsCjC;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,MAAM,EAChB,YAAY,SAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAKf"}
|
|
1
|
+
{"version":3,"file":"cloudinary-storage.d.ts","sourceRoot":"","sources":["../src/cloudinary-storage.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,uBAAuB,GAAG,IAAI,CAoBhC;AAWD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,MAAM,EAAE,uBAAuB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,sBAAsB,CAAC,CAsCjC;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,MAAM,EAChB,YAAY,SAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,qEAAqE;AACrE,wBAAsB,cAAc,CAClC,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAGf"}
|
|
@@ -68,3 +68,8 @@ export async function deleteCloudinaryAsset(config, publicId, resourceType = "im
|
|
|
68
68
|
resource_type: resourceType
|
|
69
69
|
});
|
|
70
70
|
}
|
|
71
|
+
/** Lightweight credential / reachability check for CMS status UI. */
|
|
72
|
+
export async function pingCloudinary(config) {
|
|
73
|
+
configureCloudinary(config);
|
|
74
|
+
await cloudinary.api.ping();
|
|
75
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
export { TomorrowOS } from "./tomorrowos.js";
|
|
2
2
|
export type { DeviceListItem, DeviceScreenshotInfo, ListenOptions, TomorrowOSBrand, TomorrowOSOptions } from "./tomorrowos.js";
|
|
3
|
+
export type { ConnectorState, ConnectorStatus, ServerStatusReport, StatusBlocker } from "./server-status.js";
|
|
4
|
+
export { buildServerStatus } from "./server-status.js";
|
|
3
5
|
export type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, DeviceRegistryEntry, PlaylistItemRecord, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSDataSnapshot, TomorrowOSMigratableStore, TomorrowOSStore } from "./store/types.js";
|
|
4
6
|
export { PlaylistCatalog } from "./playlist-catalog.js";
|
|
5
7
|
export type { BuiltDevicePolicy, SavePlaylistInput } from "./playlist-catalog.js";
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,2BAA2B,EAC3B,gCAAgC,EAChC,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
export { TomorrowOS } from "./tomorrowos.js";
|
|
2
|
+
export { buildServerStatus } from "./server-status.js";
|
|
2
3
|
export { PlaylistCatalog } from "./playlist-catalog.js";
|
|
3
4
|
export { DEFAULT_PAIRING_CODE_SECRET, generateDeterministicPairingCode, generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH, resolvePairingCodeSecret } from "./pairing-code.js";
|
|
4
5
|
export { MemoryStore } from "./store/memory-store.js";
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import type { TomorrowOSStore } from "./store/types.js";
|
|
2
|
+
export type ConnectorState = "ok" | "warn" | "error" | "missing";
|
|
3
|
+
export interface ConnectorStatus {
|
|
4
|
+
id: string;
|
|
5
|
+
label: string;
|
|
6
|
+
provider?: string;
|
|
7
|
+
state: ConnectorState;
|
|
8
|
+
detail: string;
|
|
9
|
+
}
|
|
10
|
+
export interface StatusBlocker {
|
|
11
|
+
connectorId: string;
|
|
12
|
+
title: string;
|
|
13
|
+
message: string;
|
|
14
|
+
fixHint: string;
|
|
15
|
+
}
|
|
16
|
+
export interface ServerStatusReport {
|
|
17
|
+
status: "success";
|
|
18
|
+
overall: "ok" | "degraded" | "blocked";
|
|
19
|
+
checkedAt: string;
|
|
20
|
+
connectors: ConnectorStatus[];
|
|
21
|
+
blockers: StatusBlocker[];
|
|
22
|
+
}
|
|
23
|
+
export interface BuildServerStatusOptions {
|
|
24
|
+
store: TomorrowOSStore;
|
|
25
|
+
staticRoot?: string | null;
|
|
26
|
+
env?: NodeJS.ProcessEnv;
|
|
27
|
+
probeTimeoutMs?: number;
|
|
28
|
+
}
|
|
29
|
+
export declare function buildServerStatus(options: BuildServerStatusOptions): Promise<ServerStatusReport>;
|
|
30
|
+
//# sourceMappingURL=server-status.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"server-status.d.ts","sourceRoot":"","sources":["../src/server-status.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,IAAI,GAAG,UAAU,GAAG,SAAS,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,eAAe,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAyQD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA4B7B"}
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
import fs from "fs/promises";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import { pingCloudinary, resolveCloudinaryConfig } from "./cloudinary-storage.js";
|
|
4
|
+
function isReplitHost(env) {
|
|
5
|
+
return !!(env.REPL_ID ||
|
|
6
|
+
env.REPLIT_DEV_DOMAIN ||
|
|
7
|
+
env.REPLIT_DEPLOYMENT ||
|
|
8
|
+
env.REPL_SLUG);
|
|
9
|
+
}
|
|
10
|
+
function resolveDatabaseProvider(env) {
|
|
11
|
+
const driver = String(env.TOMORROWOS_STORE || "").trim().toLowerCase();
|
|
12
|
+
const hasUrl = !!(env.SUPABASE_URL || env.DATABASE_URL);
|
|
13
|
+
if (driver === "supabase" || (!driver && env.SUPABASE_URL)) {
|
|
14
|
+
return {
|
|
15
|
+
provider: "supabase",
|
|
16
|
+
label: "Supabase",
|
|
17
|
+
configured: !!env.SUPABASE_URL || !!env.DATABASE_URL
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
if (driver === "postgres" || (!driver && hasUrl && !env.SUPABASE_URL)) {
|
|
21
|
+
return {
|
|
22
|
+
provider: "postgres",
|
|
23
|
+
label: "Postgres",
|
|
24
|
+
configured: hasUrl
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
if (driver === "memory") {
|
|
28
|
+
return { provider: "memory", label: "Memory Store", configured: true };
|
|
29
|
+
}
|
|
30
|
+
if (driver === "sqlite" || !driver) {
|
|
31
|
+
return { provider: "sqlite", label: "SQLite", configured: true };
|
|
32
|
+
}
|
|
33
|
+
return {
|
|
34
|
+
provider: driver || "unknown",
|
|
35
|
+
label: driver ? `Store (${driver})` : "Database",
|
|
36
|
+
configured: hasUrl || driver === "sqlite" || driver === "memory"
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async function withTimeout(promise, timeoutMs, label) {
|
|
40
|
+
let timer;
|
|
41
|
+
try {
|
|
42
|
+
return await Promise.race([
|
|
43
|
+
promise,
|
|
44
|
+
new Promise((_, reject) => {
|
|
45
|
+
timer = setTimeout(() => reject(new Error(`${label} probe timed out after ${timeoutMs}ms`)), timeoutMs);
|
|
46
|
+
})
|
|
47
|
+
]);
|
|
48
|
+
}
|
|
49
|
+
finally {
|
|
50
|
+
if (timer)
|
|
51
|
+
clearTimeout(timer);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
async function probeDatabase(store, env, timeoutMs) {
|
|
55
|
+
const meta = resolveDatabaseProvider(env);
|
|
56
|
+
const wantsRemote = meta.provider === "supabase" || meta.provider === "postgres";
|
|
57
|
+
if (wantsRemote && !meta.configured) {
|
|
58
|
+
return {
|
|
59
|
+
id: "database",
|
|
60
|
+
label: meta.label,
|
|
61
|
+
provider: meta.provider,
|
|
62
|
+
state: "missing",
|
|
63
|
+
detail: "Connection string not set. Add SUPABASE_URL (preferred) or DATABASE_URL."
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
try {
|
|
67
|
+
await withTimeout(store.listPairedDevices(), timeoutMs, meta.label);
|
|
68
|
+
if (meta.provider === "memory") {
|
|
69
|
+
return {
|
|
70
|
+
id: "database",
|
|
71
|
+
label: meta.label,
|
|
72
|
+
provider: meta.provider,
|
|
73
|
+
state: "warn",
|
|
74
|
+
detail: "In-memory only — data is lost on restart."
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
if (meta.provider === "sqlite" && isReplitHost(env)) {
|
|
78
|
+
return {
|
|
79
|
+
id: "database",
|
|
80
|
+
label: meta.label,
|
|
81
|
+
provider: meta.provider,
|
|
82
|
+
state: "warn",
|
|
83
|
+
detail: "SQLite works for demos; pairings may reset on rebuild. Prefer Supabase for production."
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
return {
|
|
87
|
+
id: "database",
|
|
88
|
+
label: meta.label,
|
|
89
|
+
provider: meta.provider,
|
|
90
|
+
state: "ok",
|
|
91
|
+
detail: `${meta.label} reachable`
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
catch (err) {
|
|
95
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
96
|
+
return {
|
|
97
|
+
id: "database",
|
|
98
|
+
label: meta.label,
|
|
99
|
+
provider: meta.provider,
|
|
100
|
+
state: "error",
|
|
101
|
+
detail: message
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
async function probeMedia(staticRoot, env, timeoutMs) {
|
|
106
|
+
const cloudinary = resolveCloudinaryConfig(env);
|
|
107
|
+
if (cloudinary) {
|
|
108
|
+
try {
|
|
109
|
+
await withTimeout(pingCloudinary(cloudinary), timeoutMs, "Cloudinary");
|
|
110
|
+
return {
|
|
111
|
+
id: "media",
|
|
112
|
+
label: "Media Server",
|
|
113
|
+
provider: "cloudinary",
|
|
114
|
+
state: "ok",
|
|
115
|
+
detail: `Cloudinary (${cloudinary.cloudName}) reachable`
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
catch (err) {
|
|
119
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
120
|
+
return {
|
|
121
|
+
id: "media",
|
|
122
|
+
label: "Media Server",
|
|
123
|
+
provider: "cloudinary",
|
|
124
|
+
state: "error",
|
|
125
|
+
detail: message
|
|
126
|
+
};
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
const hasPartialCloudinary = !!(env.CLOUDINARY_CLOUD_NAME ||
|
|
130
|
+
env.CLOUDINARY_API_KEY ||
|
|
131
|
+
env.CLOUDINARY_API_SECRET);
|
|
132
|
+
if (hasPartialCloudinary) {
|
|
133
|
+
return {
|
|
134
|
+
id: "media",
|
|
135
|
+
label: "Media Server",
|
|
136
|
+
provider: "cloudinary",
|
|
137
|
+
state: "error",
|
|
138
|
+
detail: "Cloudinary config incomplete — set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET."
|
|
139
|
+
};
|
|
140
|
+
}
|
|
141
|
+
const provider = isReplitHost(env) ? "replit-object-storage" : "local";
|
|
142
|
+
const providerLabel = provider === "replit-object-storage"
|
|
143
|
+
? "Replit Object Storage / local uploads"
|
|
144
|
+
: "Local uploads";
|
|
145
|
+
if (!staticRoot) {
|
|
146
|
+
return {
|
|
147
|
+
id: "media",
|
|
148
|
+
label: "Media Server",
|
|
149
|
+
provider,
|
|
150
|
+
state: "missing",
|
|
151
|
+
detail: "No Cloudinary and staticRoot is unset — media uploads are unavailable."
|
|
152
|
+
};
|
|
153
|
+
}
|
|
154
|
+
const uploadsDir = path.join(path.resolve(staticRoot), "uploads");
|
|
155
|
+
try {
|
|
156
|
+
await fs.mkdir(uploadsDir, { recursive: true });
|
|
157
|
+
await fs.access(uploadsDir);
|
|
158
|
+
return {
|
|
159
|
+
id: "media",
|
|
160
|
+
label: "Media Server",
|
|
161
|
+
provider,
|
|
162
|
+
state: "warn",
|
|
163
|
+
detail: `${providerLabel} writable. Uploads may not survive rebuilds — Cloudinary recommended.`
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
catch (err) {
|
|
167
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
168
|
+
return {
|
|
169
|
+
id: "media",
|
|
170
|
+
label: "Media Server",
|
|
171
|
+
provider,
|
|
172
|
+
state: "error",
|
|
173
|
+
detail: `Uploads directory not writable (${uploadsDir}): ${message}`
|
|
174
|
+
};
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
function buildBlockers(connectors) {
|
|
178
|
+
const blockers = [];
|
|
179
|
+
for (const c of connectors) {
|
|
180
|
+
if (c.id === "database" && (c.state === "missing" || c.state === "error")) {
|
|
181
|
+
blockers.push({
|
|
182
|
+
connectorId: c.id,
|
|
183
|
+
title: `${c.label} not connected`,
|
|
184
|
+
message: c.detail,
|
|
185
|
+
fixHint: c.state === "missing"
|
|
186
|
+
? "Paste your Supabase Postgres connection string as Secret SUPABASE_URL, set TOMORROWOS_STORE=supabase, then restart the CMS."
|
|
187
|
+
: "Check SUPABASE_URL / network / SSL (DATABASE_SSL=true). After Publish, confirm the live URL can reach Supabase."
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
if (c.id === "media" && c.state === "error") {
|
|
191
|
+
blockers.push({
|
|
192
|
+
connectorId: c.id,
|
|
193
|
+
title: "Media storage not ready",
|
|
194
|
+
message: c.detail,
|
|
195
|
+
fixHint: c.provider === "cloudinary"
|
|
196
|
+
? "Fix Cloudinary Secrets (CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET) and restart."
|
|
197
|
+
: "Enable Object Storage for public/uploads, or configure Cloudinary for durable media URLs."
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
if (c.id === "media" && c.state === "missing") {
|
|
201
|
+
blockers.push({
|
|
202
|
+
connectorId: c.id,
|
|
203
|
+
title: "Media storage not configured",
|
|
204
|
+
message: c.detail,
|
|
205
|
+
fixHint: "Set Cloudinary Secrets, or ensure the CMS listens with staticRoot pointing at public/ (uploads folder)."
|
|
206
|
+
});
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return blockers;
|
|
210
|
+
}
|
|
211
|
+
function overallFrom(connectors) {
|
|
212
|
+
const hardFail = connectors.some((c) => (c.id === "database" || c.id === "media") &&
|
|
213
|
+
(c.state === "error" || c.state === "missing"));
|
|
214
|
+
if (hardFail)
|
|
215
|
+
return "blocked";
|
|
216
|
+
if (connectors.some((c) => c.state === "warn"))
|
|
217
|
+
return "degraded";
|
|
218
|
+
return "ok";
|
|
219
|
+
}
|
|
220
|
+
export async function buildServerStatus(options) {
|
|
221
|
+
const env = options.env ?? process.env;
|
|
222
|
+
const timeoutMs = options.probeTimeoutMs ?? 5000;
|
|
223
|
+
const server = {
|
|
224
|
+
id: "server",
|
|
225
|
+
label: "Server",
|
|
226
|
+
provider: "tomorrowos",
|
|
227
|
+
state: "ok",
|
|
228
|
+
detail: "CMS process is running"
|
|
229
|
+
};
|
|
230
|
+
const [database, media] = await Promise.all([
|
|
231
|
+
probeDatabase(options.store, env, timeoutMs),
|
|
232
|
+
probeMedia(options.staticRoot, env, timeoutMs)
|
|
233
|
+
]);
|
|
234
|
+
const connectors = [server, database, media];
|
|
235
|
+
const blockers = buildBlockers(connectors);
|
|
236
|
+
const overall = overallFrom(connectors);
|
|
237
|
+
return {
|
|
238
|
+
status: "success",
|
|
239
|
+
overall,
|
|
240
|
+
checkedAt: new Date().toISOString(),
|
|
241
|
+
connectors,
|
|
242
|
+
blockers
|
|
243
|
+
};
|
|
244
|
+
}
|
package/dist/tomorrowos.d.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { EventEmitter } from "events";
|
|
|
2
2
|
import http from "http";
|
|
3
3
|
import { PlaylistCatalog, type BuiltDevicePolicy } from "./playlist-catalog.js";
|
|
4
4
|
import type { TomorrowOSStore } from "./store/types.js";
|
|
5
|
+
export type { ConnectorState, ConnectorStatus, ServerStatusReport, StatusBlocker } from "./server-status.js";
|
|
5
6
|
export interface TomorrowOSBrand {
|
|
6
7
|
name?: string;
|
|
7
8
|
cmsEndpoint?: 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
|
@@ -11,6 +11,7 @@ import { resolveBrandLogoPath, syncProjectAssetsToStaticRoot } from "./brand-ass
|
|
|
11
11
|
import { deleteCloudinaryAsset, resolveCloudinaryConfig, uploadBufferToCloudinary } from "./cloudinary-storage.js";
|
|
12
12
|
import { probeVideoDurationMs } from "./media-probe.js";
|
|
13
13
|
import { contentHashHex, storeUploadIfNeeded } from "./upload-storage.js";
|
|
14
|
+
import { buildServerStatus } from "./server-status.js";
|
|
14
15
|
function normalizeDevicePlatform(platform, system) {
|
|
15
16
|
const explicit = typeof platform === "string" ? platform.trim().toLowerCase() : "";
|
|
16
17
|
if (explicit)
|
|
@@ -1024,6 +1025,14 @@ export class TomorrowOS extends EventEmitter {
|
|
|
1024
1025
|
res.end(JSON.stringify(this.brand));
|
|
1025
1026
|
return;
|
|
1026
1027
|
}
|
|
1028
|
+
if (req.method === "GET" && pathname === "/status") {
|
|
1029
|
+
const report = await buildServerStatus({
|
|
1030
|
+
store: this.store,
|
|
1031
|
+
staticRoot: this.staticRoot
|
|
1032
|
+
});
|
|
1033
|
+
sendJson(res, 200, report);
|
|
1034
|
+
return;
|
|
1035
|
+
}
|
|
1027
1036
|
if (req.method === "POST" && pathname === "/media/upload") {
|
|
1028
1037
|
await this.handleMediaUpload(req, res, url);
|
|
1029
1038
|
return;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.6",
|
|
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.6",
|
|
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.6",
|
|
17
17
|
"dotenv": "^17.2.3",
|
|
18
18
|
"tsx": "^4.19.0"
|
|
19
19
|
},
|
|
@@ -27,6 +27,16 @@
|
|
|
27
27
|
</aside>
|
|
28
28
|
|
|
29
29
|
<main class="panel-main">
|
|
30
|
+
<section class="card server-status-card" id="serverStatusSection" aria-live="polite">
|
|
31
|
+
<div class="server-status-header">
|
|
32
|
+
<h2>Server status</h2>
|
|
33
|
+
<button type="button" id="serverStatusRefreshBtn" title="Refresh status">Refresh</button>
|
|
34
|
+
</div>
|
|
35
|
+
<p class="hint server-status-overall" id="serverStatusOverall">Checking connectors…</p>
|
|
36
|
+
<ul class="server-status-list" id="serverStatusList"></ul>
|
|
37
|
+
<div id="serverStatusBlockers" class="server-status-blockers hidden"></div>
|
|
38
|
+
</section>
|
|
39
|
+
|
|
30
40
|
<section class="card">
|
|
31
41
|
<h2>Pair device</h2>
|
|
32
42
|
<p class="hint">Enter the 6-character code on the screen (A–Z or 0–9).</p>
|
|
@@ -27,6 +27,7 @@ let editingDeviceNameId = null;
|
|
|
27
27
|
let editingDeviceNameValue = "";
|
|
28
28
|
|
|
29
29
|
let devicePollTimer = null;
|
|
30
|
+
let serverStatusTimer = null;
|
|
30
31
|
/** @type {number|null} CMS server boot time (ms) from GET /devices. */
|
|
31
32
|
let serverStartedAtMs = null;
|
|
32
33
|
/** @type {ReturnType<typeof setTimeout>|null} */
|
|
@@ -1857,12 +1858,169 @@ async function downloadMediaAsset(item) {
|
|
|
1857
1858
|
openDownloadFailedModal(lastError);
|
|
1858
1859
|
}
|
|
1859
1860
|
|
|
1861
|
+
function connectorStateLabel(state) {
|
|
1862
|
+
if (state === "ok") return "OK";
|
|
1863
|
+
if (state === "warn") return "Warning";
|
|
1864
|
+
if (state === "missing") return "Not set";
|
|
1865
|
+
return "Error";
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
function overallStatusCopy(overall) {
|
|
1869
|
+
if (overall === "ok") return "All connectors look healthy.";
|
|
1870
|
+
if (overall === "degraded") {
|
|
1871
|
+
return "CMS is running with warnings — review media or database durability below.";
|
|
1872
|
+
}
|
|
1873
|
+
if (overall === "blocked") {
|
|
1874
|
+
return "Setup incomplete — fix the blockers below so pairing and media work reliably.";
|
|
1875
|
+
}
|
|
1876
|
+
return "Checking connectors…";
|
|
1877
|
+
}
|
|
1878
|
+
|
|
1879
|
+
function renderServerStatus(report) {
|
|
1880
|
+
const section = document.getElementById("serverStatusSection");
|
|
1881
|
+
const list = document.getElementById("serverStatusList");
|
|
1882
|
+
const overallEl = document.getElementById("serverStatusOverall");
|
|
1883
|
+
const blockersEl = document.getElementById("serverStatusBlockers");
|
|
1884
|
+
if (!section || !list || !overallEl || !blockersEl) return;
|
|
1885
|
+
|
|
1886
|
+
const overall = report?.overall || "blocked";
|
|
1887
|
+
section.classList.remove(
|
|
1888
|
+
"server-status-card--ok",
|
|
1889
|
+
"server-status-card--degraded",
|
|
1890
|
+
"server-status-card--blocked"
|
|
1891
|
+
);
|
|
1892
|
+
section.classList.add(`server-status-card--${overall}`);
|
|
1893
|
+
overallEl.textContent = overallStatusCopy(overall);
|
|
1894
|
+
|
|
1895
|
+
list.innerHTML = "";
|
|
1896
|
+
const connectors = Array.isArray(report?.connectors) ? report.connectors : [];
|
|
1897
|
+
for (const connector of connectors) {
|
|
1898
|
+
const li = document.createElement("li");
|
|
1899
|
+
li.className = "server-status-row";
|
|
1900
|
+
|
|
1901
|
+
const main = document.createElement("div");
|
|
1902
|
+
main.className = "server-status-row-main";
|
|
1903
|
+
|
|
1904
|
+
const label = document.createElement("span");
|
|
1905
|
+
label.className = "server-status-label";
|
|
1906
|
+
label.textContent = connector.label || connector.id || "Connector";
|
|
1907
|
+
main.appendChild(label);
|
|
1908
|
+
|
|
1909
|
+
if (connector.provider) {
|
|
1910
|
+
const provider = document.createElement("span");
|
|
1911
|
+
provider.className = "server-status-provider";
|
|
1912
|
+
provider.textContent = connector.provider;
|
|
1913
|
+
main.appendChild(provider);
|
|
1914
|
+
}
|
|
1915
|
+
|
|
1916
|
+
const badge = document.createElement("span");
|
|
1917
|
+
const state = connector.state || "error";
|
|
1918
|
+
badge.className = `server-status-badge server-status-badge--${state}`;
|
|
1919
|
+
badge.textContent = connectorStateLabel(state);
|
|
1920
|
+
|
|
1921
|
+
const detail = document.createElement("p");
|
|
1922
|
+
detail.className = "server-status-detail";
|
|
1923
|
+
detail.textContent = connector.detail || "";
|
|
1924
|
+
|
|
1925
|
+
li.appendChild(main);
|
|
1926
|
+
li.appendChild(badge);
|
|
1927
|
+
li.appendChild(detail);
|
|
1928
|
+
list.appendChild(li);
|
|
1929
|
+
}
|
|
1930
|
+
|
|
1931
|
+
const blockers = Array.isArray(report?.blockers) ? report.blockers : [];
|
|
1932
|
+
blockersEl.innerHTML = "";
|
|
1933
|
+
if (blockers.length === 0) {
|
|
1934
|
+
blockersEl.classList.add("hidden");
|
|
1935
|
+
return;
|
|
1936
|
+
}
|
|
1937
|
+
|
|
1938
|
+
blockersEl.classList.remove("hidden");
|
|
1939
|
+
for (const blocker of blockers) {
|
|
1940
|
+
const card = document.createElement("div");
|
|
1941
|
+
card.className = "server-status-blocker";
|
|
1942
|
+
|
|
1943
|
+
const title = document.createElement("h3");
|
|
1944
|
+
title.textContent = blocker.title || "Connector issue";
|
|
1945
|
+
|
|
1946
|
+
const message = document.createElement("p");
|
|
1947
|
+
message.textContent = blocker.message || "";
|
|
1948
|
+
|
|
1949
|
+
const fix = document.createElement("p");
|
|
1950
|
+
fix.className = "server-status-blocker-fix";
|
|
1951
|
+
fix.textContent = blocker.fixHint
|
|
1952
|
+
? `How to fix: ${blocker.fixHint}`
|
|
1953
|
+
: "How to fix: update Secrets / env and restart the CMS.";
|
|
1954
|
+
|
|
1955
|
+
card.appendChild(title);
|
|
1956
|
+
card.appendChild(message);
|
|
1957
|
+
card.appendChild(fix);
|
|
1958
|
+
blockersEl.appendChild(card);
|
|
1959
|
+
}
|
|
1960
|
+
}
|
|
1961
|
+
|
|
1962
|
+
async function fetchServerStatus() {
|
|
1963
|
+
try {
|
|
1964
|
+
const res = await fetch("/status", { cache: "no-store" });
|
|
1965
|
+
const data = await res.json();
|
|
1966
|
+
if (!res.ok) {
|
|
1967
|
+
renderServerStatus({
|
|
1968
|
+
overall: "blocked",
|
|
1969
|
+
connectors: [
|
|
1970
|
+
{
|
|
1971
|
+
id: "server",
|
|
1972
|
+
label: "Server",
|
|
1973
|
+
state: "error",
|
|
1974
|
+
detail: data?.error || `HTTP ${res.status}`
|
|
1975
|
+
}
|
|
1976
|
+
],
|
|
1977
|
+
blockers: [
|
|
1978
|
+
{
|
|
1979
|
+
connectorId: "server",
|
|
1980
|
+
title: "Could not load server status",
|
|
1981
|
+
message: data?.error || `HTTP ${res.status}`,
|
|
1982
|
+
fixHint: "Confirm the CMS is running with @tomorrowos/sdk that includes GET /status."
|
|
1983
|
+
}
|
|
1984
|
+
]
|
|
1985
|
+
});
|
|
1986
|
+
return;
|
|
1987
|
+
}
|
|
1988
|
+
renderServerStatus(data);
|
|
1989
|
+
} catch (err) {
|
|
1990
|
+
renderServerStatus({
|
|
1991
|
+
overall: "blocked",
|
|
1992
|
+
connectors: [
|
|
1993
|
+
{
|
|
1994
|
+
id: "server",
|
|
1995
|
+
label: "Server",
|
|
1996
|
+
state: "error",
|
|
1997
|
+
detail: err?.message || "Network error"
|
|
1998
|
+
}
|
|
1999
|
+
],
|
|
2000
|
+
blockers: [
|
|
2001
|
+
{
|
|
2002
|
+
connectorId: "server",
|
|
2003
|
+
title: "CMS unreachable",
|
|
2004
|
+
message: err?.message || "Network error",
|
|
2005
|
+
fixHint: "Start the CMS (`npm run start`) and reload this page."
|
|
2006
|
+
}
|
|
2007
|
+
]
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
|
|
1860
2012
|
function startDevicePolling() {
|
|
1861
2013
|
if (devicePollTimer) clearInterval(devicePollTimer);
|
|
1862
2014
|
void fetchDevices();
|
|
1863
2015
|
devicePollTimer = setInterval(() => void fetchDevices(), 8000);
|
|
1864
2016
|
}
|
|
1865
2017
|
|
|
2018
|
+
function startServerStatusPolling() {
|
|
2019
|
+
if (serverStatusTimer) clearInterval(serverStatusTimer);
|
|
2020
|
+
void fetchServerStatus();
|
|
2021
|
+
serverStatusTimer = setInterval(() => void fetchServerStatus(), 30000);
|
|
2022
|
+
}
|
|
2023
|
+
|
|
1866
2024
|
document.addEventListener("DOMContentLoaded", () => {
|
|
1867
2025
|
const cmsUrlSection = document.getElementById("cmsUrlSection");
|
|
1868
2026
|
if (cmsUrlSection && !isLocalPanelHost(window.location.hostname)) {
|
|
@@ -1876,6 +2034,11 @@ document.addEventListener("DOMContentLoaded", () => {
|
|
|
1876
2034
|
updatePlaylistEditorVisibility();
|
|
1877
2035
|
void fetchPlaylists();
|
|
1878
2036
|
startDevicePolling();
|
|
2037
|
+
startServerStatusPolling();
|
|
2038
|
+
|
|
2039
|
+
document
|
|
2040
|
+
.getElementById("serverStatusRefreshBtn")
|
|
2041
|
+
?.addEventListener("click", () => void fetchServerStatus());
|
|
1879
2042
|
|
|
1880
2043
|
document.getElementById("newPlaylistBtn")?.addEventListener("click", newPlaylistDraft);
|
|
1881
2044
|
document.getElementById("savePlaylistBtn")?.addEventListener("click", () => void saveCurrentPlaylist());
|
|
@@ -118,6 +118,153 @@ body {
|
|
|
118
118
|
color: #666;
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
+
.server-status-card {
|
|
122
|
+
border-color: #e4e1dc;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
.server-status-header {
|
|
126
|
+
display: flex;
|
|
127
|
+
align-items: center;
|
|
128
|
+
justify-content: space-between;
|
|
129
|
+
gap: 0.75rem;
|
|
130
|
+
margin-bottom: 0.35rem;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
.server-status-header h2 {
|
|
134
|
+
margin: 0;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.server-status-header button {
|
|
138
|
+
font-size: 0.78rem;
|
|
139
|
+
padding: 0.35rem 0.65rem;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
.server-status-overall {
|
|
143
|
+
margin-bottom: 0.65rem;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
.server-status-list {
|
|
147
|
+
list-style: none;
|
|
148
|
+
margin: 0;
|
|
149
|
+
padding: 0;
|
|
150
|
+
display: grid;
|
|
151
|
+
gap: 0.45rem;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.server-status-row {
|
|
155
|
+
display: grid;
|
|
156
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
157
|
+
gap: 0.35rem 0.75rem;
|
|
158
|
+
align-items: baseline;
|
|
159
|
+
padding: 0.55rem 0.65rem;
|
|
160
|
+
border: 1px solid #ece8e2;
|
|
161
|
+
border-radius: 8px;
|
|
162
|
+
background: #fafaf9;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
.server-status-row-main {
|
|
166
|
+
display: flex;
|
|
167
|
+
flex-wrap: wrap;
|
|
168
|
+
align-items: baseline;
|
|
169
|
+
gap: 0.35rem 0.55rem;
|
|
170
|
+
min-width: 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
.server-status-label {
|
|
174
|
+
font-size: 0.85rem;
|
|
175
|
+
font-weight: 600;
|
|
176
|
+
color: #0a0908;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
.server-status-provider {
|
|
180
|
+
font-size: 0.72rem;
|
|
181
|
+
color: #888;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
.server-status-detail {
|
|
185
|
+
grid-column: 1 / -1;
|
|
186
|
+
margin: 0;
|
|
187
|
+
font-size: 0.75rem;
|
|
188
|
+
color: #555;
|
|
189
|
+
line-height: 1.4;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
.server-status-badge {
|
|
193
|
+
font-size: 0.72rem;
|
|
194
|
+
font-weight: 600;
|
|
195
|
+
letter-spacing: 0.02em;
|
|
196
|
+
text-transform: uppercase;
|
|
197
|
+
padding: 0.18rem 0.45rem;
|
|
198
|
+
border-radius: 999px;
|
|
199
|
+
border: 1px solid transparent;
|
|
200
|
+
white-space: nowrap;
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
.server-status-badge--ok {
|
|
204
|
+
color: #067647;
|
|
205
|
+
background: #ecfdf3;
|
|
206
|
+
border-color: #abefc6;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.server-status-badge--warn {
|
|
210
|
+
color: #b54708;
|
|
211
|
+
background: #fffaeb;
|
|
212
|
+
border-color: #fedf89;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.server-status-badge--error,
|
|
216
|
+
.server-status-badge--missing {
|
|
217
|
+
color: #b42318;
|
|
218
|
+
background: #fff5f5;
|
|
219
|
+
border-color: #fecdca;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
.server-status-blockers {
|
|
223
|
+
margin-top: 0.75rem;
|
|
224
|
+
display: grid;
|
|
225
|
+
gap: 0.55rem;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
.server-status-blocker {
|
|
229
|
+
padding: 0.7rem 0.8rem;
|
|
230
|
+
border-radius: 8px;
|
|
231
|
+
border: 1px solid #fecdca;
|
|
232
|
+
background: #fff8f7;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.server-status-blocker h3 {
|
|
236
|
+
margin: 0 0 0.3rem;
|
|
237
|
+
font-size: 0.85rem;
|
|
238
|
+
color: #b42318;
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
.server-status-blocker p {
|
|
242
|
+
margin: 0;
|
|
243
|
+
font-size: 0.78rem;
|
|
244
|
+
color: #444;
|
|
245
|
+
line-height: 1.45;
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
.server-status-blocker p + p {
|
|
249
|
+
margin-top: 0.35rem;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
.server-status-blocker-fix {
|
|
253
|
+
color: #0a0908;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
.server-status-card--ok {
|
|
257
|
+
border-color: #abefc6;
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
.server-status-card--degraded {
|
|
261
|
+
border-color: #fedf89;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
.server-status-card--blocked {
|
|
265
|
+
border-color: #fecdca;
|
|
266
|
+
}
|
|
267
|
+
|
|
121
268
|
.devices-grid {
|
|
122
269
|
display: grid;
|
|
123
270
|
grid-template-columns: repeat(auto-fill, minmax(308px, 1fr));
|