@tomorrowos/sdk 0.8.2 → 0.8.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/REPLIT_SETUP.md +59 -20
- package/dist/store/memory-store.d.ts +1 -1
- package/dist/store/memory-store.d.ts.map +1 -1
- package/dist/store/memory-store.js +2 -3
- package/dist/store/postgres-store.d.ts +3 -2
- package/dist/store/postgres-store.d.ts.map +1 -1
- package/dist/store/postgres-store.js +89 -17
- package/dist/store/sqlite-store.d.ts +3 -2
- package/dist/store/sqlite-store.d.ts.map +1 -1
- package/dist/store/sqlite-store.js +104 -17
- package/dist/store/types.d.ts +4 -3
- package/dist/store/types.d.ts.map +1 -1
- package/dist/tomorrowos.d.ts +4 -2
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +76 -41
- package/package.json +1 -1
- package/templates/cms-starter/.replit-artifact/artifact.toml +5 -0
- package/templates/cms-starter/package.json +2 -2
package/REPLIT_SETUP.md
CHANGED
|
@@ -12,6 +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 4 only updates **brand.json**.
|
|
15
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.
|
|
16
17
|
Do not skip steps.
|
|
17
18
|
```
|
|
@@ -23,7 +24,31 @@ Alternate triggers that also activate this protocol:
|
|
|
23
24
|
|
|
24
25
|
When any of these triggers fire, **ignore `LLM_PROMPT.md` entirely** for the Q&A phase.
|
|
25
26
|
|
|
26
|
-
**Before Question 1:** ensure the Replit workspace
|
|
27
|
+
**Before Question 1:** ensure the Replit workspace primary artifact **`kind` is `web`** (see **IRON RULE: `web` kind only**). If the project is `api` / API-only or not publishable, **fix artifact kind first** — do not continue the questionnaire on a broken project type.
|
|
28
|
+
|
|
29
|
+
---
|
|
30
|
+
|
|
31
|
+
## IRON RULE: `web` kind only (Publish will fail on `api`)
|
|
32
|
+
|
|
33
|
+
> **This is the #1 deployability rule. Non-negotiable.**
|
|
34
|
+
|
|
35
|
+
TomorrowOS CMS serves a **browser Control Panel at `/`** plus WebSocket for TVs. Replit’s publish detector treats **`kind = "api"`** artifacts as **not deployable** as a user-facing app — Publish may only see design mockups / libs and fail with *“api-server artifact isn't being recognized as deployable”*.
|
|
36
|
+
|
|
37
|
+
| Required | Forbidden |
|
|
38
|
+
|----------|-----------|
|
|
39
|
+
| Primary artifact **`kind = "web"`** | **`kind = "api"`** on the CMS artifact |
|
|
40
|
+
| Homepage / project type: **Web app** | **API server** as the only publishable artifact |
|
|
41
|
+
| `GET /` → Control Panel HTML (`public/index.html`) | `artifacts/api-server` with `kind = "api"` for this CMS |
|
|
42
|
+
| `.replit` + `npm run start` → live Node (`tsx server.ts`) | Splitting CMS into a non-publishable API artifact |
|
|
43
|
+
|
|
44
|
+
**Agent must enforce before and after Q&A:**
|
|
45
|
+
|
|
46
|
+
1. If `.replit-artifact/artifact.toml` (or artifact metadata) shows **`kind = "api"`** → change to **`kind = "web"`** immediately (validated temp-file flow). **Never** leave the CMS on `api` kind.
|
|
47
|
+
2. **Do not** scaffold TomorrowOS CMS under `artifacts/api-server` with `kind = "api"`. The CMS lives at the **project root** (or a **`web`** artifact at `/`).
|
|
48
|
+
3. **Do not** create a separate `api` artifact “for the backend” — TomorrowOS is one **web** app (HTTP + WebSocket in `server.ts`).
|
|
49
|
+
4. After setup, confirm publish detector sees a **web** artifact serving `/` — not api-only.
|
|
50
|
+
|
|
51
|
+
**If Publish says api-kind / api-server not deployable:** fix `kind` to **`web`** first, then `.replit` ports + `deploymentTarget = "autoscale"`. Creating a **new Web app** Repl is the fastest fix if Agent locked the project as API-only.
|
|
27
52
|
|
|
28
53
|
---
|
|
29
54
|
|
|
@@ -64,6 +89,7 @@ Then ask **Question 1** — do not switch to `LLM_PROMPT.md`.
|
|
|
64
89
|
7. After Q&A, **configure and start** — do **not** run a long test suite (see **Post-Q&A: minimal verification only**).
|
|
65
90
|
8. **Follow the Runtime & Replit publish rules below exactly.** A project that cannot Deploy / Publish on Replit is incomplete.
|
|
66
91
|
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
|
+
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.
|
|
67
93
|
|
|
68
94
|
**Question order after Q1:**
|
|
69
95
|
|
|
@@ -76,7 +102,9 @@ Never insert extra questions between these steps. When the user chooses Cloudina
|
|
|
76
102
|
|
|
77
103
|
## Deployability: Web app artifact (mandatory — fixes “api-kind not deployable”)
|
|
78
104
|
|
|
79
|
-
|
|
105
|
+
> **Reminder:** Re-read **IRON RULE: `web` kind only** — `api` kind is never acceptable for this CMS.
|
|
106
|
+
|
|
107
|
+
TomorrowOS CMS is a **user-facing Web app** (Control Panel at `/` + WebSocket for TVs). Replit must treat it as a **`web`** artifact, **not** `api` / API-only.
|
|
80
108
|
|
|
81
109
|
### Create the project correctly (do this first)
|
|
82
110
|
|
|
@@ -89,6 +117,7 @@ When scaffolding a **new** Repl:
|
|
|
89
117
|
|
|
90
118
|
**Forbidden setups that cause publish failure:**
|
|
91
119
|
|
|
120
|
+
- ❌ **`kind = "api"`** on the CMS artifact (`api-server`, `artifacts/api-server`, etc.)
|
|
92
121
|
- ❌ Project / primary artifact classified as **API** (`api-kind` artifact)
|
|
93
122
|
- ❌ Backend-only Express/Fastify service with **no** `staticRoot` / Control Panel at `/`
|
|
94
123
|
- ❌ Splitting CMS into a non-publishable API artifact + separate unpublished frontend
|
|
@@ -133,7 +162,8 @@ ignorePorts = false
|
|
|
133
162
|
- Set `entrypoint = "server.ts"` (not `index.js` unless that file exists and starts TomorrowOS).
|
|
134
163
|
- Set `deploymentTarget = "autoscale"` (WebSocket + long-lived connections; Static Deploy is wrong).
|
|
135
164
|
- Keep exactly **one** primary `[[ports]]` entry with `externalPort = 80` for the CMS server.
|
|
136
|
-
-
|
|
165
|
+
- Ensure `.replit-artifact/artifact.toml` has **`kind = "web"`** (shipped in cms-starter). If `kind = "api"`, change to **`web`** before Publish.
|
|
166
|
+
- If `.replit` contains `[[artifacts]]` with **only** `artifacts/api-server` and Publish fails, set artifact **`kind = "web"`** or create a **new Web app** Repl and migrate — do not tell the user publish succeeded.
|
|
137
167
|
|
|
138
168
|
If Replit assigns a different `localPort` via `PORT`, update `localPort` to match the port in `[TomorrowOS] listening on http://0.0.0.0:<PORT>` logs, but keep `externalPort = 80`.
|
|
139
169
|
|
|
@@ -154,14 +184,15 @@ After Publish, the public URL is:
|
|
|
154
184
|
|
|
155
185
|
**Do not** keep retrying publish unchanged. Fix in this order:
|
|
156
186
|
|
|
157
|
-
1.
|
|
158
|
-
2.
|
|
159
|
-
3.
|
|
160
|
-
4. Confirm `
|
|
161
|
-
5.
|
|
162
|
-
6.
|
|
187
|
+
1. Open `.replit-artifact/artifact.toml` (or artifact config) — set **`kind = "web"`** (never `api`). This is the most common fix.
|
|
188
|
+
2. Verify homepage artifact type is **Web app**; if the Repl was created as API-only, **create a new Web app Repl** and re-run this setup (fastest fix).
|
|
189
|
+
3. Replace `.replit` with the template above (`entrypoint`, `modules`, `[[ports]]`, `deploymentTarget = "autoscale"`).
|
|
190
|
+
4. Confirm `server.ts` calls `tomorrowos.listen({ staticRoot: join(__dirname, "public"), host: "0.0.0.0", port: Number(process.env.PORT) || 3000 })`.
|
|
191
|
+
5. Confirm `GET /` returns Control Panel HTML (not JSON-only API root).
|
|
192
|
+
6. In Publishing, switch to **Autoscale** if currently Static.
|
|
193
|
+
7. Restart workspace → `npm run start` → Publish again.
|
|
163
194
|
|
|
164
|
-
Tell the user plainly if step
|
|
195
|
+
Tell the user plainly if step 2 requires a new Repl — API-only projects cannot publish a browser CMS without **`kind = "web"`**.
|
|
165
196
|
|
|
166
197
|
---
|
|
167
198
|
|
|
@@ -201,7 +232,7 @@ Allowed extras (optional): `"build-player": "tomorrowos build --platform tizen"`
|
|
|
201
232
|
"node": ">=20"
|
|
202
233
|
},
|
|
203
234
|
"dependencies": {
|
|
204
|
-
"@tomorrowos/sdk": "^0.8.
|
|
235
|
+
"@tomorrowos/sdk": "^0.8.4",
|
|
205
236
|
"dotenv": "^17.2.3",
|
|
206
237
|
"tsx": "^4.19.0"
|
|
207
238
|
},
|
|
@@ -255,7 +286,7 @@ If the workspace already has a `.replit`, **merge** these keys — do not wipe u
|
|
|
255
286
|
|
|
256
287
|
**Only these three checks matter after setup.** Do not add more unless Publish fails.
|
|
257
288
|
|
|
258
|
-
1. **Web app, not API** —
|
|
289
|
+
1. **Web app, not API** — primary artifact **`kind = "web"`** (not `api` / API-only).
|
|
259
290
|
2. **`.replit` is correct** — `entrypoint = "server.ts"`, `modules = ["nodejs-20"]`, `[[ports]]` with `externalPort = 80`, `deploymentTarget = "autoscale"`, Run/Deploy = `npm run start`.
|
|
260
291
|
3. **Preview at `/`** — after `npm run start`, logs show `[TomorrowOS] listening on http://0.0.0.0:...` and Preview shows the Control Panel (HTML), not empty / API JSON.
|
|
261
292
|
|
|
@@ -486,7 +517,11 @@ The SDK auto-detects these env vars and uploads media to Cloudinary. New uploads
|
|
|
486
517
|
|
|
487
518
|
---
|
|
488
519
|
|
|
489
|
-
## Question 4 — Brand / TomorrowOS app look
|
|
520
|
+
## Question 4 — Brand / TomorrowOS app look (`brand.json` only)
|
|
521
|
+
|
|
522
|
+
> **Scope:** Question 4 answers **only** update **`brand.json`** — colours, name, tagline, logo path, `cms.expectedScreens`, etc. They configure how the **TomorrowOS player app** looks and metadata flags.
|
|
523
|
+
>
|
|
524
|
+
> **Question 4 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.
|
|
490
525
|
|
|
491
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.
|
|
492
527
|
|
|
@@ -505,7 +540,7 @@ The SDK auto-detects these env vars and uploads media to Cloudinary. New uploads
|
|
|
505
540
|
|
|
506
541
|
If the user only gives a name and primary colour, use defaults for the rest and say what you assumed.
|
|
507
542
|
|
|
508
|
-
**Then write / update `brand.json
|
|
543
|
+
**Then write / update `brand.json` only** at the project root (validate mentally against `brand.schema.json` in the SDK). **Do not** modify `.replit`, `server.ts`, Secrets, or deployment settings from these answers. Minimum example:
|
|
509
544
|
|
|
510
545
|
```json
|
|
511
546
|
{
|
|
@@ -541,9 +576,9 @@ Set `cms.expectedScreens` from Question 1 (do not ask screen count again). Alway
|
|
|
541
576
|
|
|
542
577
|
Run these steps in order. Tick mentally; do not skip.
|
|
543
578
|
|
|
544
|
-
### A. Seed the project (Web app first)
|
|
579
|
+
### A. Seed the project (Web app + `kind = "web"` first)
|
|
545
580
|
|
|
546
|
-
**If creating a new Repl:** choose **Web app** on the Replit homepage before `init`.
|
|
581
|
+
**If creating a new Repl:** choose **Web app** on the Replit homepage before `init`. Confirm `.replit-artifact/artifact.toml` has **`kind = "web"`** after init.
|
|
547
582
|
|
|
548
583
|
```bash
|
|
549
584
|
npx @tomorrowos/sdk@latest init .
|
|
@@ -657,6 +692,8 @@ Do **not**:
|
|
|
657
692
|
- Use Canvas preview as the primary CMS verification surface
|
|
658
693
|
- Serve a different app or API-only response at `/` instead of the Control Panel
|
|
659
694
|
- Create the Repl as **API-only** or publish with **Static Deployment** only
|
|
695
|
+
- Set or leave artifact **`kind = "api"`** for the TomorrowOS CMS
|
|
696
|
+
- Change `.replit`, `server.ts`, Secrets, or deployment config based on **Question 4** branding answers (those go in **`brand.json` only**)
|
|
660
697
|
- Omit `[[ports]]` / `deploymentTarget` from `.replit` when setting up for Publish
|
|
661
698
|
- Run **Supabase connection tests**, **WebSocket console tests**, or other post-setup smoke tests unless Preview fails or the user asks
|
|
662
699
|
- **Debug or “fix” Supabase** because dev logs show `getaddrinfo ENOTFOUND` or `database connection failure` — save config and finish setup instead
|
|
@@ -685,7 +722,7 @@ TOMORROWOS_STORE=sqlite
|
|
|
685
722
|
|
|
686
723
|
| Symptom | Likely cause | Fix |
|
|
687
724
|
|---------|--------------|-----|
|
|
688
|
-
| Publish: **api-kind artifact not deployable** |
|
|
725
|
+
| Publish: **api-kind artifact not deployable** | CMS artifact `kind = "api"` or API-only Repl | Set **`kind = "web"`** in `.replit-artifact/artifact.toml`; new **Web app** Repl if needed; `.replit` ports + `deploymentTarget=autoscale` |
|
|
689
726
|
| Preview empty / wrong app at `/` | Project type API, wrong staticRoot, or stale process | Set project to **Web app**; confirm `staticRoot: public`; stop old servers; restart workspace |
|
|
690
727
|
| TV / browser: WebSocket handshake timeout | Node CMS not running or crash-loop | Fix Run → `npm run start`; check logs for `listening` |
|
|
691
728
|
| Deploy/Publish fails / `tsx: not found` | `tsx` only in devDependencies | Move `tsx` to `dependencies`, reinstall, keep `start`: `tsx server.ts` |
|
|
@@ -701,11 +738,13 @@ TOMORROWOS_STORE=sqlite
|
|
|
701
738
|
|
|
702
739
|
## Protocol version
|
|
703
740
|
|
|
704
|
-
`replit-setup/1.
|
|
741
|
+
`replit-setup/1.7` — aligned with TomorrowOS protocol `1.0` and `@tomorrowos/sdk` store drivers `sqlite` | `supabase` | `postgres` | `memory`.
|
|
742
|
+
|
|
743
|
+
**Changelog 1.7:** **IRON RULE** — artifact **`kind = "web"` only** (never `api`); Question 4 **only** writes `brand.json` (no deploy/server changes).
|
|
705
744
|
|
|
706
|
-
**Changelog 1.6:** **Never test Supabase** in Replit dev — `ENOTFOUND`
|
|
745
|
+
**Changelog 1.6:** **Never test Supabase** in Replit dev — `ENOTFOUND` in Preview logs is expected.
|
|
707
746
|
|
|
708
|
-
**Changelog 1.5:** Cloudinary Secrets
|
|
747
|
+
**Changelog 1.5:** Cloudinary Secrets inside Question 3 — no “3b” step.
|
|
709
748
|
|
|
710
749
|
**Changelog 1.4:** mandatory Cloudinary credential collection before Question 4 (superseded by 1.5 flow).
|
|
711
750
|
|
|
@@ -29,7 +29,7 @@ export declare class MemoryStore implements TomorrowOSStore {
|
|
|
29
29
|
setPlaylist(record: StoredPlaylist): Promise<void>;
|
|
30
30
|
isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
|
|
31
31
|
getUploadedAsset(id: string): Promise<UploadedAssetRecord | undefined>;
|
|
32
|
-
getUploadedAssetBySha256(sha256: string
|
|
32
|
+
getUploadedAssetBySha256(sha256: string): Promise<UploadedAssetRecord | undefined>;
|
|
33
33
|
setUploadedAsset(record: UploadedAssetRecord): Promise<void>;
|
|
34
34
|
deleteUploadedAsset(id: string): Promise<void>;
|
|
35
35
|
getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0C;IACzE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiD;IAE7E,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAItC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IASV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAQpE,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAOjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUvE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAItE,wBAAwB,CAC5B,MAAM,EAAE,MAAM,
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,wBAAwB,EACxB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,cAAc,EACd,eAAe,EACf,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IACvE,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAqC;IAC/D,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA0C;IACzE,OAAO,CAAC,QAAQ,CAAC,iBAAiB,CAAiD;IAE7E,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAItC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IASV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAQpE,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAOjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAI1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAI5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IAIlD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAUvE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAItE,wBAAwB,CAC5B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IASrC,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAI5D,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAIhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;CASjB"}
|
|
@@ -78,10 +78,9 @@ export class MemoryStore {
|
|
|
78
78
|
async getUploadedAsset(id) {
|
|
79
79
|
return this.uploadedAssets.get(id);
|
|
80
80
|
}
|
|
81
|
-
async getUploadedAssetBySha256(sha256
|
|
81
|
+
async getUploadedAssetBySha256(sha256) {
|
|
82
82
|
for (const asset of this.uploadedAssets.values()) {
|
|
83
|
-
if (asset.sha256 === sha256
|
|
84
|
-
(!storageProvider || asset.storageProvider === storageProvider)) {
|
|
83
|
+
if (asset.sha256 === sha256) {
|
|
85
84
|
return asset;
|
|
86
85
|
}
|
|
87
86
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type PoolConfig } from "pg";
|
|
2
|
-
import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore, UploadedAssetRecord
|
|
2
|
+
import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore, UploadedAssetRecord } from "./types.js";
|
|
3
3
|
export interface PostgresStoreOptions {
|
|
4
4
|
connectionString: string;
|
|
5
5
|
ssl?: PoolConfig["ssl"];
|
|
@@ -13,6 +13,7 @@ export declare class PostgresStore implements TomorrowOSMigratableStore {
|
|
|
13
13
|
private ready?;
|
|
14
14
|
constructor(options: PostgresStoreOptions | string);
|
|
15
15
|
init(): Promise<void>;
|
|
16
|
+
private migrateDropUploadedAssetStorageProvider;
|
|
16
17
|
private migrateDropPlaylistVersionColumns;
|
|
17
18
|
close(): Promise<void>;
|
|
18
19
|
private ensureReady;
|
|
@@ -36,7 +37,7 @@ export declare class PostgresStore implements TomorrowOSMigratableStore {
|
|
|
36
37
|
setPlaylist(record: StoredPlaylist): Promise<void>;
|
|
37
38
|
isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
|
|
38
39
|
getUploadedAsset(id: string): Promise<UploadedAssetRecord | undefined>;
|
|
39
|
-
getUploadedAssetBySha256(sha256: string
|
|
40
|
+
getUploadedAssetBySha256(sha256: string): Promise<UploadedAssetRecord | undefined>;
|
|
40
41
|
setUploadedAsset(record: UploadedAssetRecord): Promise<void>;
|
|
41
42
|
deleteUploadedAsset(id: string): Promise<void>;
|
|
42
43
|
listUploadedAssets(): Promise<UploadedAssetRecord[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/store/postgres-store.ts"],"names":[],"mappings":"AAAA,OAAO,EAAQ,KAAK,UAAU,EAAE,MAAM,IAAI,CAAC;AAE3C,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,yBAAyB,EACzB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"postgres-store.d.ts","sourceRoot":"","sources":["../../src/store/postgres-store.ts"],"names":[],"mappings":"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"}
|
|
@@ -95,8 +95,7 @@ export class PostgresStore {
|
|
|
95
95
|
|
|
96
96
|
CREATE TABLE IF NOT EXISTS uploaded_assets (
|
|
97
97
|
id TEXT PRIMARY KEY,
|
|
98
|
-
sha256 TEXT NOT NULL,
|
|
99
|
-
storage_provider TEXT NOT NULL,
|
|
98
|
+
sha256 TEXT NOT NULL UNIQUE,
|
|
100
99
|
storage_key TEXT NOT NULL,
|
|
101
100
|
url TEXT NOT NULL,
|
|
102
101
|
original_filename TEXT,
|
|
@@ -104,8 +103,7 @@ export class PostgresStore {
|
|
|
104
103
|
resource_type TEXT,
|
|
105
104
|
bytes BIGINT,
|
|
106
105
|
created_at TEXT NOT NULL,
|
|
107
|
-
updated_at TEXT NOT NULL
|
|
108
|
-
UNIQUE (storage_provider, sha256)
|
|
106
|
+
updated_at TEXT NOT NULL
|
|
109
107
|
);
|
|
110
108
|
|
|
111
109
|
CREATE INDEX IF NOT EXISTS uploaded_assets_sha256_idx
|
|
@@ -120,8 +118,79 @@ export class PostgresStore {
|
|
|
120
118
|
|
|
121
119
|
ALTER TABLE paired_devices
|
|
122
120
|
ADD COLUMN IF NOT EXISTS system_version TEXT;
|
|
121
|
+
|
|
122
|
+
ALTER TABLE paired_devices
|
|
123
|
+
ADD COLUMN IF NOT EXISTS last_screenshot_asset_id TEXT;
|
|
124
|
+
|
|
125
|
+
ALTER TABLE paired_devices
|
|
126
|
+
ADD COLUMN IF NOT EXISTS last_screenshot_captured_at TEXT;
|
|
123
127
|
`);
|
|
124
128
|
await this.migrateDropPlaylistVersionColumns();
|
|
129
|
+
await this.migrateDropUploadedAssetStorageProvider();
|
|
130
|
+
}
|
|
131
|
+
async migrateDropUploadedAssetStorageProvider() {
|
|
132
|
+
const applied = await this.pool.query(`
|
|
133
|
+
SELECT 1
|
|
134
|
+
FROM schema_migrations
|
|
135
|
+
WHERE name = 'drop_uploaded_asset_storage_provider'
|
|
136
|
+
LIMIT 1
|
|
137
|
+
`);
|
|
138
|
+
if (applied.rows.length > 0)
|
|
139
|
+
return;
|
|
140
|
+
const columnExists = await this.pool.query(`
|
|
141
|
+
SELECT EXISTS (
|
|
142
|
+
SELECT 1
|
|
143
|
+
FROM information_schema.columns
|
|
144
|
+
WHERE table_name = 'uploaded_assets'
|
|
145
|
+
AND column_name = 'storage_provider'
|
|
146
|
+
) AS exists
|
|
147
|
+
`);
|
|
148
|
+
if (!columnExists.rows[0]?.exists) {
|
|
149
|
+
await this.pool.query(`
|
|
150
|
+
INSERT INTO schema_migrations (id, name)
|
|
151
|
+
VALUES (2, 'drop_uploaded_asset_storage_provider')
|
|
152
|
+
ON CONFLICT (id) DO NOTHING
|
|
153
|
+
`);
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
await this.pool.query(`
|
|
157
|
+
DELETE FROM uploaded_assets a
|
|
158
|
+
USING uploaded_assets b
|
|
159
|
+
WHERE a.sha256 = b.sha256
|
|
160
|
+
AND (
|
|
161
|
+
CASE WHEN a.url LIKE 'http%' THEN 0 ELSE 1 END,
|
|
162
|
+
a.updated_at
|
|
163
|
+
) > (
|
|
164
|
+
CASE WHEN b.url LIKE 'http%' THEN 0 ELSE 1 END,
|
|
165
|
+
b.updated_at
|
|
166
|
+
)
|
|
167
|
+
`);
|
|
168
|
+
await this.pool.query(`
|
|
169
|
+
ALTER TABLE uploaded_assets
|
|
170
|
+
DROP CONSTRAINT IF EXISTS uploaded_assets_storage_provider_sha256_key
|
|
171
|
+
`);
|
|
172
|
+
await this.pool.query(`
|
|
173
|
+
ALTER TABLE uploaded_assets
|
|
174
|
+
DROP COLUMN IF EXISTS storage_provider
|
|
175
|
+
`);
|
|
176
|
+
await this.pool.query(`
|
|
177
|
+
DO $$
|
|
178
|
+
BEGIN
|
|
179
|
+
IF NOT EXISTS (
|
|
180
|
+
SELECT 1
|
|
181
|
+
FROM pg_constraint
|
|
182
|
+
WHERE conname = 'uploaded_assets_sha256_key'
|
|
183
|
+
) THEN
|
|
184
|
+
ALTER TABLE uploaded_assets
|
|
185
|
+
ADD CONSTRAINT uploaded_assets_sha256_key UNIQUE (sha256);
|
|
186
|
+
END IF;
|
|
187
|
+
END $$;
|
|
188
|
+
`);
|
|
189
|
+
await this.pool.query(`
|
|
190
|
+
INSERT INTO schema_migrations (id, name)
|
|
191
|
+
VALUES (2, 'drop_uploaded_asset_storage_provider')
|
|
192
|
+
ON CONFLICT (id) DO NOTHING
|
|
193
|
+
`);
|
|
125
194
|
}
|
|
126
195
|
async migrateDropPlaylistVersionColumns() {
|
|
127
196
|
const applied = await this.pool.query(`
|
|
@@ -286,9 +355,11 @@ export class PostgresStore {
|
|
|
286
355
|
last_boot_at,
|
|
287
356
|
last_online_at,
|
|
288
357
|
last_offline_at,
|
|
289
|
-
last_policy_push_at
|
|
358
|
+
last_policy_push_at,
|
|
359
|
+
last_screenshot_asset_id,
|
|
360
|
+
last_screenshot_captured_at
|
|
290
361
|
)
|
|
291
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
|
362
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14)
|
|
292
363
|
ON CONFLICT (device_id) DO UPDATE SET
|
|
293
364
|
pairing_token = EXCLUDED.pairing_token,
|
|
294
365
|
paired_at = EXCLUDED.paired_at,
|
|
@@ -300,7 +371,9 @@ export class PostgresStore {
|
|
|
300
371
|
last_boot_at = EXCLUDED.last_boot_at,
|
|
301
372
|
last_online_at = EXCLUDED.last_online_at,
|
|
302
373
|
last_offline_at = EXCLUDED.last_offline_at,
|
|
303
|
-
last_policy_push_at = EXCLUDED.last_policy_push_at
|
|
374
|
+
last_policy_push_at = EXCLUDED.last_policy_push_at,
|
|
375
|
+
last_screenshot_asset_id = EXCLUDED.last_screenshot_asset_id,
|
|
376
|
+
last_screenshot_captured_at = EXCLUDED.last_screenshot_captured_at
|
|
304
377
|
`, [
|
|
305
378
|
deviceId,
|
|
306
379
|
record.pairingToken,
|
|
@@ -313,7 +386,9 @@ export class PostgresStore {
|
|
|
313
386
|
record.lastBootAt ?? null,
|
|
314
387
|
record.lastOnlineAt ?? null,
|
|
315
388
|
record.lastOfflineAt ?? null,
|
|
316
|
-
record.lastPolicyPushAt ?? null
|
|
389
|
+
record.lastPolicyPushAt ?? null,
|
|
390
|
+
record.lastScreenshotAssetId ?? null,
|
|
391
|
+
record.lastScreenshotCapturedAt ?? null
|
|
317
392
|
]);
|
|
318
393
|
}
|
|
319
394
|
async getPairedDevice(deviceId) {
|
|
@@ -418,15 +493,14 @@ export class PostgresStore {
|
|
|
418
493
|
const row = result.rows[0];
|
|
419
494
|
return row ? this.mapUploadedAssetRow(row) : undefined;
|
|
420
495
|
}
|
|
421
|
-
async getUploadedAssetBySha256(sha256
|
|
496
|
+
async getUploadedAssetBySha256(sha256) {
|
|
422
497
|
await this.ensureReady();
|
|
423
498
|
const result = await this.pool.query(`
|
|
424
499
|
SELECT *
|
|
425
500
|
FROM uploaded_assets
|
|
426
501
|
WHERE sha256 = $1
|
|
427
|
-
AND ($2::text IS NULL OR storage_provider = $2)
|
|
428
502
|
LIMIT 1
|
|
429
|
-
`, [sha256
|
|
503
|
+
`, [sha256]);
|
|
430
504
|
const row = result.rows[0];
|
|
431
505
|
return row ? this.mapUploadedAssetRow(row) : undefined;
|
|
432
506
|
}
|
|
@@ -436,7 +510,6 @@ export class PostgresStore {
|
|
|
436
510
|
INSERT INTO uploaded_assets (
|
|
437
511
|
id,
|
|
438
512
|
sha256,
|
|
439
|
-
storage_provider,
|
|
440
513
|
storage_key,
|
|
441
514
|
url,
|
|
442
515
|
original_filename,
|
|
@@ -446,10 +519,9 @@ export class PostgresStore {
|
|
|
446
519
|
created_at,
|
|
447
520
|
updated_at
|
|
448
521
|
)
|
|
449
|
-
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10
|
|
522
|
+
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10)
|
|
450
523
|
ON CONFLICT (id) DO UPDATE SET
|
|
451
524
|
sha256 = EXCLUDED.sha256,
|
|
452
|
-
storage_provider = EXCLUDED.storage_provider,
|
|
453
525
|
storage_key = EXCLUDED.storage_key,
|
|
454
526
|
url = EXCLUDED.url,
|
|
455
527
|
original_filename = EXCLUDED.original_filename,
|
|
@@ -460,7 +532,6 @@ export class PostgresStore {
|
|
|
460
532
|
`, [
|
|
461
533
|
record.id,
|
|
462
534
|
record.sha256,
|
|
463
|
-
record.storageProvider,
|
|
464
535
|
record.storageKey,
|
|
465
536
|
record.url,
|
|
466
537
|
record.originalFilename ?? null,
|
|
@@ -559,7 +630,9 @@ export class PostgresStore {
|
|
|
559
630
|
lastBootAt: optionalString(row.last_boot_at),
|
|
560
631
|
lastOnlineAt: optionalString(row.last_online_at),
|
|
561
632
|
lastOfflineAt: optionalString(row.last_offline_at),
|
|
562
|
-
lastPolicyPushAt: optionalString(row.last_policy_push_at)
|
|
633
|
+
lastPolicyPushAt: optionalString(row.last_policy_push_at),
|
|
634
|
+
lastScreenshotAssetId: optionalString(row.last_screenshot_asset_id),
|
|
635
|
+
lastScreenshotCapturedAt: optionalString(row.last_screenshot_captured_at)
|
|
563
636
|
};
|
|
564
637
|
}
|
|
565
638
|
mapPlaylistRow(row) {
|
|
@@ -577,7 +650,6 @@ export class PostgresStore {
|
|
|
577
650
|
return {
|
|
578
651
|
id: row.id,
|
|
579
652
|
sha256: row.sha256,
|
|
580
|
-
storageProvider: row.storage_provider,
|
|
581
653
|
storageKey: row.storage_key,
|
|
582
654
|
url: row.url,
|
|
583
655
|
originalFilename: optionalString(row.original_filename),
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore, UploadedAssetRecord
|
|
1
|
+
import type { DevicePlaylistAssignment, DeviceRegistryEntry, DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, StoredPlaylist, TomorrowOSMigratableStore, UploadedAssetRecord } from "./types.js";
|
|
2
2
|
interface SQLiteStoreOptions {
|
|
3
3
|
databasePath?: string;
|
|
4
4
|
}
|
|
@@ -11,6 +11,7 @@ export declare class SQLiteStore implements TomorrowOSMigratableStore {
|
|
|
11
11
|
private readonly db;
|
|
12
12
|
constructor(options?: SQLiteStoreOptions | string);
|
|
13
13
|
init(): void;
|
|
14
|
+
private migrateDropUploadedAssetStorageProvider;
|
|
14
15
|
private tableHasColumn;
|
|
15
16
|
private migrateDropPlaylistVersionColumns;
|
|
16
17
|
private addColumnIfMissing;
|
|
@@ -35,7 +36,7 @@ export declare class SQLiteStore implements TomorrowOSMigratableStore {
|
|
|
35
36
|
setPlaylist(record: StoredPlaylist): Promise<void>;
|
|
36
37
|
isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
|
|
37
38
|
getUploadedAsset(id: string): Promise<UploadedAssetRecord | undefined>;
|
|
38
|
-
getUploadedAssetBySha256(sha256: string
|
|
39
|
+
getUploadedAssetBySha256(sha256: string): Promise<UploadedAssetRecord | undefined>;
|
|
39
40
|
setUploadedAsset(record: UploadedAssetRecord): Promise<void>;
|
|
40
41
|
deleteUploadedAsset(id: string): Promise<void>;
|
|
41
42
|
listUploadedAssets(): Promise<UploadedAssetRecord[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/store/sqlite-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,yBAAyB,EACzB,mBAAmB,
|
|
1
|
+
{"version":3,"file":"sqlite-store.d.ts","sourceRoot":"","sources":["../../src/store/sqlite-store.ts"],"names":[],"mappings":"AAIA,OAAO,KAAK,EACV,wBAAwB,EACxB,mBAAmB,EACnB,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EAGjB,cAAc,EACd,yBAAyB,EACzB,mBAAmB,EACpB,MAAM,YAAY,CAAC;AAGpB,UAAU,kBAAkB;IAC1B,YAAY,CAAC,EAAE,MAAM,CAAC;CACvB;AA8ED;;;GAGG;AACH,qBAAa,WAAY,YAAW,yBAAyB;IAC3D,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,OAAO,CAAC,QAAQ,CAAC,EAAE,CAAoB;gBAE3B,OAAO,GAAE,kBAAkB,GAAG,MAAW;IAerD,IAAI,IAAI,IAAI;IA8FZ,OAAO,CAAC,uCAAuC;IAuF/C,OAAO,CAAC,cAAc;IAKtB,OAAO,CAAC,iCAAiC;IA4GzC,OAAO,CAAC,kBAAkB;IAS1B,KAAK,IAAI,IAAI;IAIP,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAUtE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAUpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,gBAAgB,IAAI,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAY/C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAStC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IA2BV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAapE,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IAYpD,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAmDV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IASpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAYjD,aAAa,IAAI,OAAO,CAAC,cAAc,EAAE,CAAC;IAS1C,WAAW,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,SAAS,CAAC;IAS5D,WAAW,CAAC,MAAM,EAAE,cAAc,GAAG,OAAO,CAAC,IAAI,CAAC;IA8BlD,mBAAmB,CAAC,IAAI,EAAE,MAAM,EAAE,SAAS,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,OAAO,CAAC;IAcvE,gBAAgB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAStE,wBAAwB,CAC5B,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,mBAAmB,GAAG,SAAS,CAAC;IAUrC,gBAAgB,CAAC,MAAM,EAAE,mBAAmB,GAAG,OAAO,CAAC,IAAI,CAAC;IAsC5D,mBAAmB,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,kBAAkB,IAAI,OAAO,CAAC,mBAAmB,EAAE,CAAC;IASpD,oBAAoB,CACxB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAuBhC,oBAAoB,CACxB,QAAQ,EAAE,MAAM,EAChB,WAAW,EAAE,wBAAwB,EAAE,GACtC,OAAO,CAAC,IAAI,CAAC;IA4BhB,OAAO,CAAC,oBAAoB;IAU5B,OAAO,CAAC,kBAAkB;IAkB1B,OAAO,CAAC,cAAc;IAYtB,OAAO,CAAC,mBAAmB;CAc5B"}
|
|
@@ -101,8 +101,7 @@ export class SQLiteStore {
|
|
|
101
101
|
|
|
102
102
|
CREATE TABLE IF NOT EXISTS uploaded_assets (
|
|
103
103
|
id TEXT PRIMARY KEY,
|
|
104
|
-
sha256 TEXT NOT NULL,
|
|
105
|
-
storage_provider TEXT NOT NULL,
|
|
104
|
+
sha256 TEXT NOT NULL UNIQUE,
|
|
106
105
|
storage_key TEXT NOT NULL,
|
|
107
106
|
url TEXT NOT NULL,
|
|
108
107
|
original_filename TEXT,
|
|
@@ -110,8 +109,7 @@ export class SQLiteStore {
|
|
|
110
109
|
resource_type TEXT,
|
|
111
110
|
bytes INTEGER,
|
|
112
111
|
created_at TEXT NOT NULL,
|
|
113
|
-
updated_at TEXT NOT NULL
|
|
114
|
-
UNIQUE (storage_provider, sha256)
|
|
112
|
+
updated_at TEXT NOT NULL
|
|
115
113
|
);
|
|
116
114
|
|
|
117
115
|
CREATE INDEX IF NOT EXISTS uploaded_assets_sha256_idx
|
|
@@ -122,7 +120,94 @@ export class SQLiteStore {
|
|
|
122
120
|
`);
|
|
123
121
|
this.addColumnIfMissing("paired_devices", "player_version TEXT");
|
|
124
122
|
this.addColumnIfMissing("paired_devices", "system_version TEXT");
|
|
123
|
+
this.addColumnIfMissing("paired_devices", "last_screenshot_asset_id TEXT");
|
|
124
|
+
this.addColumnIfMissing("paired_devices", "last_screenshot_captured_at TEXT");
|
|
125
125
|
this.migrateDropPlaylistVersionColumns();
|
|
126
|
+
this.migrateDropUploadedAssetStorageProvider();
|
|
127
|
+
}
|
|
128
|
+
migrateDropUploadedAssetStorageProvider() {
|
|
129
|
+
const alreadyApplied = this.db.prepare(`
|
|
130
|
+
SELECT 1
|
|
131
|
+
FROM schema_migrations
|
|
132
|
+
WHERE name = 'drop_uploaded_asset_storage_provider'
|
|
133
|
+
LIMIT 1
|
|
134
|
+
`).get();
|
|
135
|
+
if (alreadyApplied)
|
|
136
|
+
return;
|
|
137
|
+
if (!this.tableHasColumn("uploaded_assets", "storage_provider")) {
|
|
138
|
+
this.db.prepare(`
|
|
139
|
+
INSERT OR IGNORE INTO schema_migrations (id, name)
|
|
140
|
+
VALUES (2, 'drop_uploaded_asset_storage_provider')
|
|
141
|
+
`).run();
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
this.db.exec(`
|
|
145
|
+
CREATE TABLE uploaded_assets_new (
|
|
146
|
+
id TEXT PRIMARY KEY,
|
|
147
|
+
sha256 TEXT NOT NULL UNIQUE,
|
|
148
|
+
storage_key TEXT NOT NULL,
|
|
149
|
+
url TEXT NOT NULL,
|
|
150
|
+
original_filename TEXT,
|
|
151
|
+
mime_type TEXT,
|
|
152
|
+
resource_type TEXT,
|
|
153
|
+
bytes INTEGER,
|
|
154
|
+
created_at TEXT NOT NULL,
|
|
155
|
+
updated_at TEXT NOT NULL
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
INSERT INTO uploaded_assets_new (
|
|
159
|
+
id,
|
|
160
|
+
sha256,
|
|
161
|
+
storage_key,
|
|
162
|
+
url,
|
|
163
|
+
original_filename,
|
|
164
|
+
mime_type,
|
|
165
|
+
resource_type,
|
|
166
|
+
bytes,
|
|
167
|
+
created_at,
|
|
168
|
+
updated_at
|
|
169
|
+
)
|
|
170
|
+
SELECT
|
|
171
|
+
id,
|
|
172
|
+
sha256,
|
|
173
|
+
storage_key,
|
|
174
|
+
url,
|
|
175
|
+
original_filename,
|
|
176
|
+
mime_type,
|
|
177
|
+
resource_type,
|
|
178
|
+
bytes,
|
|
179
|
+
created_at,
|
|
180
|
+
updated_at
|
|
181
|
+
FROM (
|
|
182
|
+
SELECT
|
|
183
|
+
id,
|
|
184
|
+
sha256,
|
|
185
|
+
storage_key,
|
|
186
|
+
url,
|
|
187
|
+
original_filename,
|
|
188
|
+
mime_type,
|
|
189
|
+
resource_type,
|
|
190
|
+
bytes,
|
|
191
|
+
created_at,
|
|
192
|
+
updated_at,
|
|
193
|
+
ROW_NUMBER() OVER (
|
|
194
|
+
PARTITION BY sha256
|
|
195
|
+
ORDER BY CASE WHEN url LIKE 'http%' THEN 0 ELSE 1 END, updated_at DESC
|
|
196
|
+
) AS rn
|
|
197
|
+
FROM uploaded_assets
|
|
198
|
+
)
|
|
199
|
+
WHERE rn = 1;
|
|
200
|
+
|
|
201
|
+
DROP TABLE uploaded_assets;
|
|
202
|
+
ALTER TABLE uploaded_assets_new RENAME TO uploaded_assets;
|
|
203
|
+
|
|
204
|
+
CREATE INDEX IF NOT EXISTS uploaded_assets_sha256_idx
|
|
205
|
+
ON uploaded_assets (sha256);
|
|
206
|
+
`);
|
|
207
|
+
this.db.prepare(`
|
|
208
|
+
INSERT OR IGNORE INTO schema_migrations (id, name)
|
|
209
|
+
VALUES (2, 'drop_uploaded_asset_storage_provider')
|
|
210
|
+
`).run();
|
|
126
211
|
}
|
|
127
212
|
tableHasColumn(table, column) {
|
|
128
213
|
const rows = this.db.pragma(`table_info(${table})`);
|
|
@@ -323,9 +408,11 @@ export class SQLiteStore {
|
|
|
323
408
|
last_boot_at,
|
|
324
409
|
last_online_at,
|
|
325
410
|
last_offline_at,
|
|
326
|
-
last_policy_push_at
|
|
411
|
+
last_policy_push_at,
|
|
412
|
+
last_screenshot_asset_id,
|
|
413
|
+
last_screenshot_captured_at
|
|
327
414
|
)
|
|
328
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
415
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
329
416
|
ON CONFLICT(device_id) DO UPDATE SET
|
|
330
417
|
pairing_token = excluded.pairing_token,
|
|
331
418
|
paired_at = excluded.paired_at,
|
|
@@ -337,8 +424,10 @@ export class SQLiteStore {
|
|
|
337
424
|
last_boot_at = excluded.last_boot_at,
|
|
338
425
|
last_online_at = excluded.last_online_at,
|
|
339
426
|
last_offline_at = excluded.last_offline_at,
|
|
340
|
-
last_policy_push_at = excluded.last_policy_push_at
|
|
341
|
-
|
|
427
|
+
last_policy_push_at = excluded.last_policy_push_at,
|
|
428
|
+
last_screenshot_asset_id = excluded.last_screenshot_asset_id,
|
|
429
|
+
last_screenshot_captured_at = excluded.last_screenshot_captured_at
|
|
430
|
+
`).run(deviceId, record.pairingToken, record.pairedAt, record.deviceName ?? null, record.platform ?? null, record.system ?? null, record.playerVersion ?? null, record.systemVersion ?? null, record.lastBootAt ?? null, record.lastOnlineAt ?? null, record.lastOfflineAt ?? null, record.lastPolicyPushAt ?? null, record.lastScreenshotAssetId ?? null, record.lastScreenshotCapturedAt ?? null);
|
|
342
431
|
}
|
|
343
432
|
async getPairedDevice(deviceId) {
|
|
344
433
|
const row = this.db.prepare(`
|
|
@@ -421,14 +510,13 @@ export class SQLiteStore {
|
|
|
421
510
|
`).get(id);
|
|
422
511
|
return row ? this.mapUploadedAssetRow(row) : undefined;
|
|
423
512
|
}
|
|
424
|
-
async getUploadedAssetBySha256(sha256
|
|
513
|
+
async getUploadedAssetBySha256(sha256) {
|
|
425
514
|
const row = this.db.prepare(`
|
|
426
515
|
SELECT *
|
|
427
516
|
FROM uploaded_assets
|
|
428
517
|
WHERE sha256 = ?
|
|
429
|
-
AND (? IS NULL OR storage_provider = ?)
|
|
430
518
|
LIMIT 1
|
|
431
|
-
`).get(sha256
|
|
519
|
+
`).get(sha256);
|
|
432
520
|
return row ? this.mapUploadedAssetRow(row) : undefined;
|
|
433
521
|
}
|
|
434
522
|
async setUploadedAsset(record) {
|
|
@@ -436,7 +524,6 @@ export class SQLiteStore {
|
|
|
436
524
|
INSERT INTO uploaded_assets (
|
|
437
525
|
id,
|
|
438
526
|
sha256,
|
|
439
|
-
storage_provider,
|
|
440
527
|
storage_key,
|
|
441
528
|
url,
|
|
442
529
|
original_filename,
|
|
@@ -446,10 +533,9 @@ export class SQLiteStore {
|
|
|
446
533
|
created_at,
|
|
447
534
|
updated_at
|
|
448
535
|
)
|
|
449
|
-
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?,
|
|
536
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
450
537
|
ON CONFLICT(id) DO UPDATE SET
|
|
451
538
|
sha256 = excluded.sha256,
|
|
452
|
-
storage_provider = excluded.storage_provider,
|
|
453
539
|
storage_key = excluded.storage_key,
|
|
454
540
|
url = excluded.url,
|
|
455
541
|
original_filename = excluded.original_filename,
|
|
@@ -457,7 +543,7 @@ export class SQLiteStore {
|
|
|
457
543
|
resource_type = excluded.resource_type,
|
|
458
544
|
bytes = excluded.bytes,
|
|
459
545
|
updated_at = excluded.updated_at
|
|
460
|
-
`).run(record.id, record.sha256, record.
|
|
546
|
+
`).run(record.id, record.sha256, record.storageKey, record.url, record.originalFilename ?? null, record.mimeType ?? null, record.resourceType ?? null, record.bytes ?? null, record.createdAt, record.updatedAt);
|
|
461
547
|
}
|
|
462
548
|
async deleteUploadedAsset(id) {
|
|
463
549
|
this.db.prepare("DELETE FROM uploaded_assets WHERE id = ?").run(id);
|
|
@@ -527,7 +613,9 @@ export class SQLiteStore {
|
|
|
527
613
|
lastBootAt: optionalString(row.last_boot_at),
|
|
528
614
|
lastOnlineAt: optionalString(row.last_online_at),
|
|
529
615
|
lastOfflineAt: optionalString(row.last_offline_at),
|
|
530
|
-
lastPolicyPushAt: optionalString(row.last_policy_push_at)
|
|
616
|
+
lastPolicyPushAt: optionalString(row.last_policy_push_at),
|
|
617
|
+
lastScreenshotAssetId: optionalString(row.last_screenshot_asset_id),
|
|
618
|
+
lastScreenshotCapturedAt: optionalString(row.last_screenshot_captured_at)
|
|
531
619
|
};
|
|
532
620
|
}
|
|
533
621
|
mapPlaylistRow(row) {
|
|
@@ -545,7 +633,6 @@ export class SQLiteStore {
|
|
|
545
633
|
return {
|
|
546
634
|
id: row.id,
|
|
547
635
|
sha256: row.sha256,
|
|
548
|
-
storageProvider: row.storage_provider,
|
|
549
636
|
storageKey: row.storage_key,
|
|
550
637
|
url: row.url,
|
|
551
638
|
originalFilename: optionalString(row.original_filename),
|
package/dist/store/types.d.ts
CHANGED
|
@@ -26,6 +26,9 @@ export interface PairedDeviceRecord {
|
|
|
26
26
|
lastOnlineAt?: string;
|
|
27
27
|
lastOfflineAt?: string;
|
|
28
28
|
lastPolicyPushAt?: string;
|
|
29
|
+
/** Latest device screenshot stored via uploaded_assets (local or Cloudinary). */
|
|
30
|
+
lastScreenshotAssetId?: string;
|
|
31
|
+
lastScreenshotCapturedAt?: string;
|
|
29
32
|
}
|
|
30
33
|
export interface PairedDeviceEntry {
|
|
31
34
|
deviceId: string;
|
|
@@ -45,11 +48,9 @@ export interface PlaylistItemRecord {
|
|
|
45
48
|
type?: string;
|
|
46
49
|
durationMs?: number;
|
|
47
50
|
}
|
|
48
|
-
export type UploadedAssetStorageProvider = "local" | "cloudinary";
|
|
49
51
|
export interface UploadedAssetRecord {
|
|
50
52
|
id: string;
|
|
51
53
|
sha256: string;
|
|
52
|
-
storageProvider: UploadedAssetStorageProvider;
|
|
53
54
|
storageKey: string;
|
|
54
55
|
url: string;
|
|
55
56
|
originalFilename?: string;
|
|
@@ -113,7 +114,7 @@ export interface TomorrowOSStore {
|
|
|
113
114
|
setPlaylist(record: StoredPlaylist): Promise<void>;
|
|
114
115
|
isPlaylistNameTaken(name: string, excludeId?: string): Promise<boolean>;
|
|
115
116
|
getUploadedAsset(id: string): Promise<UploadedAssetRecord | undefined>;
|
|
116
|
-
getUploadedAssetBySha256(sha256: string
|
|
117
|
+
getUploadedAssetBySha256(sha256: string): Promise<UploadedAssetRecord | undefined>;
|
|
117
118
|
setUploadedAsset(record: UploadedAssetRecord): Promise<void>;
|
|
118
119
|
deleteUploadedAsset(id: string): Promise<void>;
|
|
119
120
|
getDeviceAssignments(deviceId: string): Promise<DevicePlaylistAssignment[]>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wFAAwF;AACxF,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.d.ts
CHANGED
|
@@ -78,6 +78,7 @@ export interface DeviceScreenshotInfo {
|
|
|
78
78
|
url: string;
|
|
79
79
|
capturedAt: string;
|
|
80
80
|
mimeType: string;
|
|
81
|
+
assetId?: string;
|
|
81
82
|
width?: number;
|
|
82
83
|
height?: number;
|
|
83
84
|
}
|
|
@@ -150,12 +151,13 @@ export declare class TomorrowOS extends EventEmitter {
|
|
|
150
151
|
private getAssetIdsFromItems;
|
|
151
152
|
private isUploadedAssetReferenced;
|
|
152
153
|
private releaseRemovedPlaylistAssets;
|
|
154
|
+
private isUploadedAssetReferencedAsDeviceScreenshot;
|
|
153
155
|
private deleteUploadedAssetIfUnreferenced;
|
|
154
156
|
private refreshResolvedBrandLogo;
|
|
155
157
|
private tryServeStatic;
|
|
156
|
-
private
|
|
157
|
-
private screenshotBaseName;
|
|
158
|
+
private getLegacyScreenshotsDir;
|
|
158
159
|
private screenshotExtension;
|
|
160
|
+
private getLegacyDeviceScreenshot;
|
|
159
161
|
private saveDeviceScreenshot;
|
|
160
162
|
private getLatestDeviceScreenshot;
|
|
161
163
|
private captureDeviceScreenshot;
|
package/dist/tomorrowos.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,IAAI,MAAM,MAAM,CAAC;AAYxB,OAAO,EAAE,eAAe,EAAE,KAAK,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAChF,OAAO,KAAK,EAKV,eAAe,
|
|
1
|
+
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -44,6 +44,9 @@ function normalizeDeviceSystemLabel(platform, system) {
|
|
|
44
44
|
const label = typeof system === "string" ? system.trim() : "";
|
|
45
45
|
return label || undefined;
|
|
46
46
|
}
|
|
47
|
+
function isRemoteUploadedAssetUrl(url) {
|
|
48
|
+
return /^https?:\/\//i.test(String(url || "").trim());
|
|
49
|
+
}
|
|
47
50
|
function formatDurationMs(ms) {
|
|
48
51
|
if (!Number.isFinite(ms) || ms < 0)
|
|
49
52
|
return "0s";
|
|
@@ -286,8 +289,13 @@ export class TomorrowOS extends EventEmitter {
|
|
|
286
289
|
err.code = "PAIRING_INVALID";
|
|
287
290
|
throw err;
|
|
288
291
|
}
|
|
292
|
+
const paired = await this.store.getPairedDevice(id);
|
|
293
|
+
const screenshotAssetId = paired?.lastScreenshotAssetId;
|
|
289
294
|
await this.store.deletePairedDevice(id);
|
|
290
295
|
this.pendingDeviceMeta.delete(id);
|
|
296
|
+
if (screenshotAssetId) {
|
|
297
|
+
await this.deleteUploadedAssetIfUnreferenced(screenshotAssetId);
|
|
298
|
+
}
|
|
291
299
|
const ws = this.devices.get(id);
|
|
292
300
|
let notified = false;
|
|
293
301
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
@@ -625,9 +633,7 @@ export class TomorrowOS extends EventEmitter {
|
|
|
625
633
|
}
|
|
626
634
|
this.staticIndexFile = idx;
|
|
627
635
|
const uploadsDir = path.join(path.resolve(this.staticRoot), "uploads");
|
|
628
|
-
const screenshotsDir = path.join(path.resolve(this.staticRoot), "screenshots");
|
|
629
636
|
void fs.mkdir(uploadsDir, { recursive: true });
|
|
630
|
-
void fs.mkdir(screenshotsDir, { recursive: true });
|
|
631
637
|
void this.refreshResolvedBrandLogo();
|
|
632
638
|
}
|
|
633
639
|
else {
|
|
@@ -685,10 +691,9 @@ export class TomorrowOS extends EventEmitter {
|
|
|
685
691
|
status: "success",
|
|
686
692
|
url: asset.url,
|
|
687
693
|
assetId: asset.id,
|
|
688
|
-
filename: asset.
|
|
694
|
+
filename: isRemoteUploadedAssetUrl(asset.url) ? safeName : asset.storageKey,
|
|
689
695
|
size: asset.bytes ?? body.length,
|
|
690
696
|
contentHash: asset.sha256,
|
|
691
|
-
storageProvider: asset.storageProvider,
|
|
692
697
|
deduplicated
|
|
693
698
|
};
|
|
694
699
|
if (durationMs != null)
|
|
@@ -703,12 +708,9 @@ export class TomorrowOS extends EventEmitter {
|
|
|
703
708
|
async storeUploadedMediaAsset(body, safeName, mimeType) {
|
|
704
709
|
const sha256 = contentHashHex(body);
|
|
705
710
|
const cloudinaryConfig = resolveCloudinaryConfig();
|
|
706
|
-
const
|
|
707
|
-
? "cloudinary"
|
|
708
|
-
: "local";
|
|
709
|
-
const existing = await this.store.getUploadedAssetBySha256(sha256, storageProvider);
|
|
711
|
+
const existing = await this.store.getUploadedAssetBySha256(sha256);
|
|
710
712
|
if (existing) {
|
|
711
|
-
if (existing.
|
|
713
|
+
if (isRemoteUploadedAssetUrl(existing.url)) {
|
|
712
714
|
return { asset: existing, deduplicated: true };
|
|
713
715
|
}
|
|
714
716
|
const existingLocalPath = path.join(path.resolve(this.staticRoot || "."), "uploads", existing.storageKey);
|
|
@@ -733,7 +735,6 @@ export class TomorrowOS extends EventEmitter {
|
|
|
733
735
|
const asset = {
|
|
734
736
|
id: randomUUID(),
|
|
735
737
|
sha256,
|
|
736
|
-
storageProvider: "cloudinary",
|
|
737
738
|
storageKey: uploaded.publicId,
|
|
738
739
|
url: uploaded.secureUrl,
|
|
739
740
|
originalFilename: safeName,
|
|
@@ -752,7 +753,6 @@ export class TomorrowOS extends EventEmitter {
|
|
|
752
753
|
const asset = {
|
|
753
754
|
id: existing?.id ?? randomUUID(),
|
|
754
755
|
sha256,
|
|
755
|
-
storageProvider: "local",
|
|
756
756
|
storageKey: storedName,
|
|
757
757
|
url: `/uploads/${storedName}`,
|
|
758
758
|
originalFilename: safeName,
|
|
@@ -790,14 +790,20 @@ export class TomorrowOS extends EventEmitter {
|
|
|
790
790
|
await this.deleteUploadedAssetIfUnreferenced(assetId);
|
|
791
791
|
}
|
|
792
792
|
}
|
|
793
|
+
async isUploadedAssetReferencedAsDeviceScreenshot(assetId) {
|
|
794
|
+
const devices = await this.store.listPairedDevices();
|
|
795
|
+
return devices.some((entry) => entry.record.lastScreenshotAssetId === assetId);
|
|
796
|
+
}
|
|
793
797
|
async deleteUploadedAssetIfUnreferenced(assetId) {
|
|
794
798
|
try {
|
|
795
799
|
if (await this.isUploadedAssetReferenced(assetId))
|
|
796
800
|
return;
|
|
801
|
+
if (await this.isUploadedAssetReferencedAsDeviceScreenshot(assetId))
|
|
802
|
+
return;
|
|
797
803
|
const asset = await this.store.getUploadedAsset(assetId);
|
|
798
804
|
if (!asset)
|
|
799
805
|
return;
|
|
800
|
-
if (asset.
|
|
806
|
+
if (isRemoteUploadedAssetUrl(asset.url)) {
|
|
801
807
|
const cloudinaryConfig = resolveCloudinaryConfig();
|
|
802
808
|
if (!cloudinaryConfig) {
|
|
803
809
|
console.warn(`[TomorrowOS] Cloudinary config missing; cannot delete asset ${asset.id}.`);
|
|
@@ -871,15 +877,12 @@ export class TomorrowOS extends EventEmitter {
|
|
|
871
877
|
return false;
|
|
872
878
|
}
|
|
873
879
|
}
|
|
874
|
-
|
|
880
|
+
getLegacyScreenshotsDir() {
|
|
875
881
|
if (!this.staticRoot) {
|
|
876
882
|
throw new Error("Screenshot storage requires listen({ staticRoot })");
|
|
877
883
|
}
|
|
878
884
|
return path.join(path.resolve(this.staticRoot), "screenshots");
|
|
879
885
|
}
|
|
880
|
-
screenshotBaseName(deviceId) {
|
|
881
|
-
return sanitizeStorageSegment(deviceId);
|
|
882
|
-
}
|
|
883
886
|
screenshotExtension(mimeType) {
|
|
884
887
|
const normalized = mimeType.toLowerCase();
|
|
885
888
|
if (normalized === "image/png")
|
|
@@ -888,6 +891,29 @@ export class TomorrowOS extends EventEmitter {
|
|
|
888
891
|
return ".webp";
|
|
889
892
|
return ".jpg";
|
|
890
893
|
}
|
|
894
|
+
async getLegacyDeviceScreenshot(deviceId) {
|
|
895
|
+
if (!this.staticRoot)
|
|
896
|
+
return null;
|
|
897
|
+
const base = sanitizeStorageSegment(deviceId);
|
|
898
|
+
const metaPath = path.join(this.getLegacyScreenshotsDir(), `${base}.json`);
|
|
899
|
+
try {
|
|
900
|
+
const raw = await fs.readFile(metaPath, "utf8");
|
|
901
|
+
const parsed = JSON.parse(raw);
|
|
902
|
+
if (!parsed || typeof parsed.url !== "string")
|
|
903
|
+
return null;
|
|
904
|
+
return {
|
|
905
|
+
deviceId,
|
|
906
|
+
url: parsed.url,
|
|
907
|
+
capturedAt: parsed.capturedAt,
|
|
908
|
+
mimeType: parsed.mimeType || "image/jpeg",
|
|
909
|
+
width: parsed.width,
|
|
910
|
+
height: parsed.height
|
|
911
|
+
};
|
|
912
|
+
}
|
|
913
|
+
catch {
|
|
914
|
+
return null;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
891
917
|
async saveDeviceScreenshot(deviceId, payload) {
|
|
892
918
|
const mimeType = typeof payload.mimeType === "string" && payload.mimeType.startsWith("image/")
|
|
893
919
|
? payload.mimeType
|
|
@@ -896,42 +922,51 @@ export class TomorrowOS extends EventEmitter {
|
|
|
896
922
|
if (!dataBase64.trim()) {
|
|
897
923
|
throw new Error("Screenshot payload missing dataBase64");
|
|
898
924
|
}
|
|
899
|
-
const dir = this.getScreenshotsDir();
|
|
900
|
-
await fs.mkdir(dir, { recursive: true });
|
|
901
|
-
const base = this.screenshotBaseName(deviceId);
|
|
902
|
-
const ext = this.screenshotExtension(mimeType);
|
|
903
|
-
const filename = `${base}${ext}`;
|
|
904
|
-
const filePath = path.join(dir, filename);
|
|
905
|
-
const metaPath = path.join(dir, `${base}.json`);
|
|
906
925
|
const capturedAt = typeof payload.capturedAt === "string" && payload.capturedAt.trim()
|
|
907
926
|
? payload.capturedAt
|
|
908
927
|
: new Date().toISOString();
|
|
909
|
-
|
|
910
|
-
const
|
|
928
|
+
const body = Buffer.from(dataBase64, "base64");
|
|
929
|
+
const safeName = sanitizeUploadFilename(`screenshot-${sanitizeStorageSegment(deviceId)}-${Date.now()}${this.screenshotExtension(mimeType)}`);
|
|
930
|
+
const paired = await this.store.getPairedDevice(deviceId);
|
|
931
|
+
const previousAssetId = paired?.lastScreenshotAssetId;
|
|
932
|
+
const { asset } = await this.storeUploadedMediaAsset(body, safeName, mimeType);
|
|
933
|
+
if (paired) {
|
|
934
|
+
await this.store.setPairedDevice(deviceId, {
|
|
935
|
+
...paired,
|
|
936
|
+
lastScreenshotAssetId: asset.id,
|
|
937
|
+
lastScreenshotCapturedAt: capturedAt
|
|
938
|
+
});
|
|
939
|
+
}
|
|
940
|
+
if (previousAssetId && previousAssetId !== asset.id) {
|
|
941
|
+
await this.deleteUploadedAssetIfUnreferenced(previousAssetId);
|
|
942
|
+
}
|
|
943
|
+
return {
|
|
911
944
|
deviceId,
|
|
912
|
-
|
|
945
|
+
assetId: asset.id,
|
|
946
|
+
url: asset.url,
|
|
913
947
|
capturedAt,
|
|
914
|
-
mimeType,
|
|
948
|
+
mimeType: asset.mimeType || mimeType,
|
|
915
949
|
width: typeof payload.width === "number" ? payload.width : undefined,
|
|
916
950
|
height: typeof payload.height === "number" ? payload.height : undefined
|
|
917
951
|
};
|
|
918
|
-
await fs.writeFile(metaPath, JSON.stringify(info, null, 2));
|
|
919
|
-
return info;
|
|
920
952
|
}
|
|
921
953
|
async getLatestDeviceScreenshot(deviceId) {
|
|
922
|
-
const
|
|
923
|
-
const
|
|
924
|
-
const
|
|
925
|
-
|
|
926
|
-
const
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
954
|
+
const paired = await this.store.getPairedDevice(deviceId);
|
|
955
|
+
const assetId = paired?.lastScreenshotAssetId;
|
|
956
|
+
const capturedAt = paired?.lastScreenshotCapturedAt;
|
|
957
|
+
if (assetId && capturedAt) {
|
|
958
|
+
const asset = await this.store.getUploadedAsset(assetId);
|
|
959
|
+
if (asset) {
|
|
960
|
+
return {
|
|
961
|
+
deviceId,
|
|
962
|
+
assetId: asset.id,
|
|
963
|
+
url: asset.url,
|
|
964
|
+
capturedAt,
|
|
965
|
+
mimeType: asset.mimeType || "image/jpeg"
|
|
966
|
+
};
|
|
967
|
+
}
|
|
934
968
|
}
|
|
969
|
+
return this.getLegacyDeviceScreenshot(deviceId);
|
|
935
970
|
}
|
|
936
971
|
async captureDeviceScreenshot(deviceId) {
|
|
937
972
|
const result = await this.sendDeviceCommand(deviceId, "device.captureScreen", {});
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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.8.
|
|
3
|
+
"version": "0.8.4",
|
|
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.8.
|
|
16
|
+
"@tomorrowos/sdk": "^0.8.4",
|
|
17
17
|
"dotenv": "^17.2.3",
|
|
18
18
|
"tsx": "^4.19.0"
|
|
19
19
|
},
|