@tomorrowos/sdk 0.3.5 → 0.3.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.
@@ -0,0 +1,8 @@
1
+ /** Copy project-root `assets/` into `staticRoot/assets/` when files are missing (common init layout). */
2
+ export declare function syncProjectAssetsToStaticRoot(staticRoot: string): Promise<void>;
3
+ /**
4
+ * Resolve logoPath for players: honor brand.json when the file exists, otherwise
5
+ * pick the best image under `staticRoot/assets/` (any filename).
6
+ */
7
+ export declare function resolveBrandLogoPath(staticRoot: string, configuredPath: unknown): Promise<string | undefined>;
8
+ //# sourceMappingURL=brand-assets.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"brand-assets.d.ts","sourceRoot":"","sources":["../src/brand-assets.ts"],"names":[],"mappings":"AA+BA,yGAAyG;AACzG,wBAAsB,6BAA6B,CACjD,UAAU,EAAE,MAAM,GACjB,OAAO,CAAC,IAAI,CAAC,CA8Bf;AAED;;;GAGG;AACH,wBAAsB,oBAAoB,CACxC,UAAU,EAAE,MAAM,EAClB,cAAc,EAAE,OAAO,GACtB,OAAO,CAAC,MAAM,GAAG,SAAS,CAAC,CAmC7B"}
@@ -0,0 +1,103 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ const IMAGE_EXT = new Set([".png", ".jpg", ".jpeg", ".svg", ".webp", ".gif"]);
4
+ function isImageFilename(name) {
5
+ return IMAGE_EXT.has(path.extname(name).toLowerCase());
6
+ }
7
+ function scoreLogoCandidate(name) {
8
+ const base = path.basename(name, path.extname(name)).toLowerCase();
9
+ if (base === "logo")
10
+ return 100;
11
+ if (base === "brand" || base === "brand-logo")
12
+ return 90;
13
+ if (base === "icon" || base === "favicon")
14
+ return 80;
15
+ if (base.includes("logo"))
16
+ return 70;
17
+ if (base.includes("brand"))
18
+ return 60;
19
+ if (base.includes("icon"))
20
+ return 50;
21
+ return 10;
22
+ }
23
+ function pickBestLogoFile(files) {
24
+ const images = files.filter(isImageFilename);
25
+ if (images.length === 0)
26
+ return null;
27
+ images.sort((a, b) => {
28
+ const diff = scoreLogoCandidate(b) - scoreLogoCandidate(a);
29
+ if (diff !== 0)
30
+ return diff;
31
+ return a.localeCompare(b);
32
+ });
33
+ return images[0];
34
+ }
35
+ /** Copy project-root `assets/` into `staticRoot/assets/` when files are missing (common init layout). */
36
+ export async function syncProjectAssetsToStaticRoot(staticRoot) {
37
+ const resolvedRoot = path.resolve(staticRoot);
38
+ const publicAssets = path.join(resolvedRoot, "assets");
39
+ const projectAssets = path.join(path.dirname(resolvedRoot), "assets");
40
+ await fs.mkdir(publicAssets, { recursive: true });
41
+ let names = [];
42
+ try {
43
+ names = await fs.readdir(projectAssets);
44
+ }
45
+ catch {
46
+ return;
47
+ }
48
+ for (const name of names) {
49
+ if (!isImageFilename(name))
50
+ continue;
51
+ const src = path.join(projectAssets, name);
52
+ const dest = path.join(publicAssets, name);
53
+ try {
54
+ await fs.access(dest);
55
+ continue;
56
+ }
57
+ catch {
58
+ try {
59
+ await fs.copyFile(src, dest);
60
+ console.log(`[TomorrowOS] synced brand asset: assets/${name} → public/assets/${name}`);
61
+ }
62
+ catch (err) {
63
+ console.warn(`[TomorrowOS] could not sync assets/${name}:`, err);
64
+ }
65
+ }
66
+ }
67
+ }
68
+ /**
69
+ * Resolve logoPath for players: honor brand.json when the file exists, otherwise
70
+ * pick the best image under `staticRoot/assets/` (any filename).
71
+ */
72
+ export async function resolveBrandLogoPath(staticRoot, configuredPath) {
73
+ const resolvedRoot = path.resolve(staticRoot);
74
+ const assetsDir = path.join(resolvedRoot, "assets");
75
+ await fs.mkdir(assetsDir, { recursive: true });
76
+ const configured = typeof configuredPath === "string" && configuredPath.trim()
77
+ ? configuredPath.trim()
78
+ : "";
79
+ if (configured) {
80
+ const rel = configured.replace(/^\.\//, "").replace(/^[\\/]+/, "");
81
+ const onDisk = path.join(resolvedRoot, rel);
82
+ try {
83
+ const st = await fs.stat(onDisk);
84
+ if (st.isFile()) {
85
+ return rel.startsWith("assets/") ? `./${rel}` : `./assets/${path.basename(rel)}`;
86
+ }
87
+ }
88
+ catch {
89
+ /* fall through to auto-detect */
90
+ }
91
+ }
92
+ let files = [];
93
+ try {
94
+ files = await fs.readdir(assetsDir);
95
+ }
96
+ catch {
97
+ return configured || undefined;
98
+ }
99
+ const picked = pickBestLogoFile(files);
100
+ if (!picked)
101
+ return configured || undefined;
102
+ return `./assets/${picked}`;
103
+ }
@@ -111,6 +111,7 @@ export declare class TomorrowOS extends EventEmitter {
111
111
  private sendCommandToSocket;
112
112
  listen(options: ListenOptions): http.Server;
113
113
  private handleMediaUpload;
114
+ private refreshResolvedBrandLogo;
114
115
  private tryServeStatic;
115
116
  private handleHttp;
116
117
  private handleDeviceHttp;
@@ -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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"}
@@ -7,6 +7,7 @@ import { WebSocket, WebSocketServer } from "ws";
7
7
  import { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode } from "./pairing-code.js";
8
8
  import { PlaylistCatalog } from "./playlist-catalog.js";
9
9
  import { MemoryStore } from "./store/memory-store.js";
10
+ import { resolveBrandLogoPath, syncProjectAssetsToStaticRoot } from "./brand-assets.js";
10
11
  function formatDurationMs(ms) {
11
12
  if (!Number.isFinite(ms) || ms < 0)
12
13
  return "0s";
@@ -440,6 +441,7 @@ export class TomorrowOS extends EventEmitter {
440
441
  this.staticIndexFile = idx;
441
442
  const uploadsDir = path.join(path.resolve(this.staticRoot), "uploads");
442
443
  void fs.mkdir(uploadsDir, { recursive: true });
444
+ void this.refreshResolvedBrandLogo();
443
445
  }
444
446
  else {
445
447
  this.staticIndexFile = "index.html";
@@ -504,6 +506,21 @@ export class TomorrowOS extends EventEmitter {
504
506
  sendJson(res, 400, { status: "failed", error: msg });
505
507
  }
506
508
  }
509
+ async refreshResolvedBrandLogo() {
510
+ if (!this.staticRoot)
511
+ return;
512
+ try {
513
+ await syncProjectAssetsToStaticRoot(this.staticRoot);
514
+ const logoPath = await resolveBrandLogoPath(this.staticRoot, this.brand.logoPath);
515
+ if (logoPath) {
516
+ this.brand.logoPath = logoPath;
517
+ console.log(`[TomorrowOS] brand logo: ${logoPath}`);
518
+ }
519
+ }
520
+ catch (err) {
521
+ console.warn("[TomorrowOS] brand logo resolve failed:", err);
522
+ }
523
+ }
507
524
  async tryServeStatic(pathname, res) {
508
525
  if (!this.staticRoot)
509
526
  return false;
@@ -523,7 +540,13 @@ export class TomorrowOS extends EventEmitter {
523
540
  ".html": "text/html; charset=utf-8",
524
541
  ".js": "application/javascript; charset=utf-8",
525
542
  ".css": "text/css; charset=utf-8",
526
- ".json": "application/json; charset=utf-8"
543
+ ".json": "application/json; charset=utf-8",
544
+ ".svg": "image/svg+xml",
545
+ ".png": "image/png",
546
+ ".jpg": "image/jpeg",
547
+ ".jpeg": "image/jpeg",
548
+ ".webp": "image/webp",
549
+ ".gif": "image/gif"
527
550
  };
528
551
  const ctype = types[ext] ?? "application/octet-stream";
529
552
  res.writeHead(200, { "Content-Type": ctype });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.3.5",
3
+ "version": "0.3.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",
@@ -8,7 +8,6 @@
8
8
  "textColor": "#0A0908",
9
9
  "logoPath": "./assets/logo.svg",
10
10
  "fontFamily": "Inter",
11
- "cmsEndpoint": "ws://localhost:3000",
12
11
  "cms": {
13
12
  "useCase": "other",
14
13
  "hostingTarget": "self-hosted",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.3.5",
3
+ "version": "0.3.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",
@@ -10,7 +10,7 @@
10
10
  "build-player": "tomorrowos build --platform tizen"
11
11
  },
12
12
  "dependencies": {
13
- "@tomorrowos/sdk": "^0.3.5"
13
+ "@tomorrowos/sdk": "^0.3.6"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@types/node": "^20.0.0",
@@ -217,7 +217,7 @@ async function fetchPlaylists() {
217
217
  showResult({
218
218
  status: "failed",
219
219
  error:
220
- "CMS server is missing /playlists. Restart CMS with @tomorrowos/sdk 0.3.5 or newer."
220
+ "CMS server is missing /playlists. Restart CMS with @tomorrowos/sdk 0.3.6 or newer."
221
221
  });
222
222
  }
223
223
  return;
@@ -1,458 +1,458 @@
1
- * {
2
- box-sizing: border-box;
3
- }
4
-
5
- body {
6
- margin: 0;
7
- font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
8
- background: #f4f3f0;
9
- color: #0a0908;
10
- }
11
-
12
- .app-header {
13
- padding: 1rem 1.5rem;
14
- background: #0a0908;
15
- color: #fafaf9;
16
- }
17
-
18
- .app-header h1 {
19
- margin: 0;
20
- font-size: 1.25rem;
21
- font-weight: 600;
22
- }
23
-
24
- .app-header p {
25
- margin: 0.35rem 0 0;
26
- font-size: 0.875rem;
27
- opacity: 0.8;
28
- }
29
-
30
- .layout {
31
- display: flex;
32
- gap: 0;
33
- min-height: calc(100vh - 4.5rem);
34
- }
35
-
36
- .panel-main {
37
- flex: 1;
38
- padding: 1.25rem 1.5rem 2rem;
39
- overflow: auto;
40
- }
41
-
42
- .panel-playlist-nav {
43
- width: 220px;
44
- flex-shrink: 0;
45
- background: #fff;
46
- border-right: 1px solid #e4e1dc;
47
- display: flex;
48
- flex-direction: column;
49
- padding: 0.75rem;
50
- }
51
-
52
- .playlist-nav-header {
53
- display: flex;
54
- align-items: center;
55
- justify-content: space-between;
56
- margin-bottom: 0.5rem;
57
- }
58
-
59
- .playlist-nav-header h2 {
60
- margin: 0;
61
- font-size: 0.95rem;
62
- }
63
-
64
- .playlist-catalog {
65
- list-style: none;
66
- margin: 0;
67
- padding: 0;
68
- overflow: auto;
69
- flex: 1;
70
- }
71
-
72
- .playlist-catalog-item {
73
- border: 1px solid #e4e1dc;
74
- border-radius: 6px;
75
- padding: 0.5rem 0.6rem;
76
- margin-bottom: 0.4rem;
77
- cursor: pointer;
78
- font-size: 0.85rem;
79
- }
80
-
81
- .playlist-catalog-item--active {
82
- border-color: #ff8a3d;
83
- background: #fff5ee;
84
- }
85
-
86
- .playlist-catalog-item small {
87
- display: block;
88
- color: #666;
89
- margin-top: 0.2rem;
90
- }
91
-
92
- .panel-playlist {
93
- width: 300px;
94
- flex-shrink: 0;
95
- background: #fff;
96
- border-left: 1px solid #e4e1dc;
97
- display: flex;
98
- flex-direction: column;
99
- }
100
-
101
- .card {
102
- background: #fff;
103
- border: 1px solid #e4e1dc;
104
- border-radius: 10px;
105
- padding: 1rem 1.1rem;
106
- margin-bottom: 1rem;
107
- }
108
-
109
- .card h2 {
110
- margin: 0 0 0.75rem;
111
- font-size: 0.95rem;
112
- font-weight: 600;
113
- }
114
-
115
- .hint {
116
- margin: 0 0 0.75rem;
117
- font-size: 0.8rem;
118
- color: #666;
119
- }
120
-
121
- .devices-grid {
122
- display: grid;
123
- grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
124
- gap: 0.85rem;
125
- }
126
-
127
- .devices-empty {
128
- margin: 0;
129
- color: #666;
130
- font-size: 0.875rem;
131
- grid-column: 1 / -1;
132
- }
133
-
134
- .device-card {
135
- border: 1px solid #e4e1dc;
136
- border-radius: 8px;
137
- padding: 0.85rem 0.95rem;
138
- background: #fafaf9;
139
- }
140
-
141
- .device-card-header {
142
- display: flex;
143
- align-items: center;
144
- gap: 0.5rem;
145
- margin-bottom: 0.65rem;
146
- }
147
-
148
- .device-card-title {
149
- margin: 0;
150
- font-size: 0.95rem;
151
- font-weight: 600;
152
- line-height: 1.2;
153
- }
154
-
155
- .status-led {
156
- width: 11px;
157
- height: 11px;
158
- border-radius: 50%;
159
- flex-shrink: 0;
160
- box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06);
161
- }
162
-
163
- .status-led--online {
164
- background: #12b76a;
165
- box-shadow: 0 0 8px rgba(18, 183, 106, 0.55);
166
- }
167
-
168
- .status-led--offline {
169
- background: #f04438;
170
- box-shadow: 0 0 8px rgba(240, 68, 56, 0.45);
171
- }
172
-
173
- .device-meta {
174
- display: grid;
175
- gap: 0.35rem;
176
- font-size: 0.78rem;
177
- color: #444;
178
- margin-bottom: 0.75rem;
179
- }
180
-
181
- .device-meta dt {
182
- font-weight: 600;
183
- color: #666;
184
- display: inline;
185
- }
186
-
187
- .device-meta dt::after {
188
- content: ": ";
189
- }
190
-
191
- .device-meta dd {
192
- display: inline;
193
- margin: 0;
194
- word-break: break-all;
195
- }
196
-
197
- .device-meta-row {
198
- display: block;
199
- }
200
-
201
- .device-card-actions {
202
- display: flex;
203
- flex-wrap: wrap;
204
- gap: 0.4rem;
205
- }
206
-
207
- .device-card-actions button {
208
- font-size: 0.78rem;
209
- padding: 0.35rem 0.6rem;
210
- }
211
-
212
- .row {
213
- display: flex;
214
- flex-wrap: wrap;
215
- gap: 0.5rem;
216
- align-items: center;
217
- }
218
-
219
- input[type="text"],
220
- input[type="date"],
221
- input[type="time"],
222
- select {
223
- font: inherit;
224
- padding: 0.45rem 0.6rem;
225
- border: 1px solid #d6d2cb;
226
- border-radius: 6px;
227
- background: #fff;
228
- }
229
-
230
- button {
231
- font: inherit;
232
- padding: 0.45rem 0.85rem;
233
- border-radius: 6px;
234
- border: 1px solid #d6d2cb;
235
- background: #fff;
236
- cursor: pointer;
237
- }
238
-
239
- button:hover {
240
- background: #f5f3ef;
241
- }
242
-
243
- button.primary {
244
- background: #ff8a3d;
245
- border-color: #e67320;
246
- color: #0a0908;
247
- font-weight: 600;
248
- }
249
-
250
- button.primary:hover {
251
- background: #ff9a57;
252
- }
253
-
254
- button.danger {
255
- color: #b42318;
256
- border-color: #fecdca;
257
- }
258
-
259
- .schedule-grid {
260
- display: grid;
261
- grid-template-columns: 1fr 1fr;
262
- gap: 0.65rem 1rem;
263
- }
264
-
265
- .schedule-grid label {
266
- display: flex;
267
- flex-direction: column;
268
- gap: 0.25rem;
269
- font-size: 0.8rem;
270
- color: #444;
271
- }
272
-
273
- .days-row {
274
- grid-column: 1 / -1;
275
- display: flex;
276
- flex-wrap: wrap;
277
- gap: 0.5rem 0.75rem;
278
- font-size: 0.8rem;
279
- }
280
-
281
- .days-row label {
282
- flex-direction: row;
283
- align-items: center;
284
- gap: 0.35rem;
285
- color: #0a0908;
286
- }
287
-
288
- .playlist-header {
289
- display: flex;
290
- align-items: center;
291
- justify-content: space-between;
292
- padding: 1rem 1rem 0.75rem;
293
- border-bottom: 1px solid #e4e1dc;
294
- }
295
-
296
- .playlist-header h2 {
297
- margin: 0;
298
- font-size: 0.95rem;
299
- }
300
-
301
- .playlist-list {
302
- list-style: none;
303
- margin: 0;
304
- padding: 0.5rem;
305
- flex: 1;
306
- overflow-y: auto;
307
- }
308
-
309
- .playlist-empty {
310
- padding: 1.5rem 1rem;
311
- text-align: center;
312
- color: #888;
313
- font-size: 0.875rem;
314
- }
315
-
316
- .playlist-item {
317
- border: 1px solid #e4e1dc;
318
- border-radius: 8px;
319
- padding: 0.65rem;
320
- margin-bottom: 0.5rem;
321
- background: #fafaf9;
322
- }
323
-
324
- .playlist-item-thumb {
325
- width: 100%;
326
- aspect-ratio: 16 / 9;
327
- object-fit: cover;
328
- border-radius: 4px;
329
- background: #e4e1dc;
330
- display: block;
331
- margin-bottom: 0.5rem;
332
- }
333
-
334
- .playlist-item-name {
335
- font-size: 0.8rem;
336
- font-weight: 600;
337
- word-break: break-all;
338
- margin-bottom: 0.35rem;
339
- }
340
-
341
- .playlist-item-meta {
342
- font-size: 0.75rem;
343
- color: #666;
344
- margin-bottom: 0.45rem;
345
- }
346
-
347
- .playlist-item-actions {
348
- display: flex;
349
- gap: 0.35rem;
350
- align-items: center;
351
- }
352
-
353
- .playlist-item-actions input[type="number"] {
354
- width: 5rem;
355
- font: inherit;
356
- padding: 0.25rem 0.4rem;
357
- border: 1px solid #d6d2cb;
358
- border-radius: 4px;
359
- }
360
-
361
- .publish-bar {
362
- padding: 1rem 0 0;
363
- }
364
-
365
- #result {
366
- margin: 0;
367
- padding: 0.75rem;
368
- background: #0a0908;
369
- color: #e8e6e3;
370
- border-radius: 8px;
371
- font-size: 0.75rem;
372
- max-height: 10rem;
373
- overflow: auto;
374
- white-space: pre-wrap;
375
- }
376
-
377
- .hidden {
378
- display: none !important;
379
- }
380
-
381
- .field-label {
382
- display: block;
383
- margin-bottom: 0.75rem;
384
- font-size: 0.85rem;
385
- font-weight: 600;
386
- }
387
-
388
- .field-label input {
389
- display: block;
390
- width: 100%;
391
- margin-top: 0.35rem;
392
- font-weight: 400;
393
- }
394
-
395
- .subheading {
396
- margin: 0.5rem 0 0.35rem;
397
- font-size: 0.9rem;
398
- }
399
-
400
- .editor-actions {
401
- display: flex;
402
- flex-wrap: wrap;
403
- gap: 0.5rem;
404
- margin-top: 0.75rem;
405
- }
406
-
407
- .device-published-list {
408
- margin: 0 0 0.65rem;
409
- padding: 0;
410
- list-style: none;
411
- font-size: 0.78rem;
412
- }
413
-
414
- .device-published-list li {
415
- display: flex;
416
- align-items: center;
417
- justify-content: space-between;
418
- gap: 0.35rem;
419
- margin-bottom: 0.25rem;
420
- }
421
-
422
- .modal {
423
- position: fixed;
424
- inset: 0;
425
- z-index: 1000;
426
- display: flex;
427
- align-items: center;
428
- justify-content: center;
429
- }
430
-
431
- .modal-backdrop {
432
- position: absolute;
433
- inset: 0;
434
- background: rgba(0, 0, 0, 0.45);
435
- }
436
-
437
- .modal-card {
438
- position: relative;
439
- background: #fff;
440
- border-radius: 10px;
441
- padding: 1.25rem;
442
- width: min(92vw, 420px);
443
- max-height: 80vh;
444
- overflow: auto;
445
- box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
446
- }
447
-
448
- .publish-checklist label {
449
- display: block;
450
- margin-bottom: 0.45rem;
451
- font-size: 0.9rem;
452
- }
453
-
454
- .modal-actions {
455
- display: flex;
456
- gap: 0.5rem;
457
- margin-top: 1rem;
458
- }
1
+ * {
2
+ box-sizing: border-box;
3
+ }
4
+
5
+ body {
6
+ margin: 0;
7
+ font-family: system-ui, -apple-system, Segoe UI, Roboto, sans-serif;
8
+ background: #f4f3f0;
9
+ color: #0a0908;
10
+ }
11
+
12
+ .app-header {
13
+ padding: 1rem 1.5rem;
14
+ background: #0a0908;
15
+ color: #fafaf9;
16
+ }
17
+
18
+ .app-header h1 {
19
+ margin: 0;
20
+ font-size: 1.25rem;
21
+ font-weight: 600;
22
+ }
23
+
24
+ .app-header p {
25
+ margin: 0.35rem 0 0;
26
+ font-size: 0.875rem;
27
+ opacity: 0.8;
28
+ }
29
+
30
+ .layout {
31
+ display: flex;
32
+ gap: 0;
33
+ min-height: calc(100vh - 4.5rem);
34
+ }
35
+
36
+ .panel-main {
37
+ flex: 1;
38
+ padding: 1.25rem 1.5rem 2rem;
39
+ overflow: auto;
40
+ }
41
+
42
+ .panel-playlist-nav {
43
+ width: 220px;
44
+ flex-shrink: 0;
45
+ background: #fff;
46
+ border-right: 1px solid #e4e1dc;
47
+ display: flex;
48
+ flex-direction: column;
49
+ padding: 0.75rem;
50
+ }
51
+
52
+ .playlist-nav-header {
53
+ display: flex;
54
+ align-items: center;
55
+ justify-content: space-between;
56
+ margin-bottom: 0.5rem;
57
+ }
58
+
59
+ .playlist-nav-header h2 {
60
+ margin: 0;
61
+ font-size: 0.95rem;
62
+ }
63
+
64
+ .playlist-catalog {
65
+ list-style: none;
66
+ margin: 0;
67
+ padding: 0;
68
+ overflow: auto;
69
+ flex: 1;
70
+ }
71
+
72
+ .playlist-catalog-item {
73
+ border: 1px solid #e4e1dc;
74
+ border-radius: 6px;
75
+ padding: 0.5rem 0.6rem;
76
+ margin-bottom: 0.4rem;
77
+ cursor: pointer;
78
+ font-size: 0.85rem;
79
+ }
80
+
81
+ .playlist-catalog-item--active {
82
+ border-color: #ff8a3d;
83
+ background: #fff5ee;
84
+ }
85
+
86
+ .playlist-catalog-item small {
87
+ display: block;
88
+ color: #666;
89
+ margin-top: 0.2rem;
90
+ }
91
+
92
+ .panel-playlist {
93
+ width: 300px;
94
+ flex-shrink: 0;
95
+ background: #fff;
96
+ border-left: 1px solid #e4e1dc;
97
+ display: flex;
98
+ flex-direction: column;
99
+ }
100
+
101
+ .card {
102
+ background: #fff;
103
+ border: 1px solid #e4e1dc;
104
+ border-radius: 10px;
105
+ padding: 1rem 1.1rem;
106
+ margin-bottom: 1rem;
107
+ }
108
+
109
+ .card h2 {
110
+ margin: 0 0 0.75rem;
111
+ font-size: 0.95rem;
112
+ font-weight: 600;
113
+ }
114
+
115
+ .hint {
116
+ margin: 0 0 0.75rem;
117
+ font-size: 0.8rem;
118
+ color: #666;
119
+ }
120
+
121
+ .devices-grid {
122
+ display: grid;
123
+ grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
124
+ gap: 0.85rem;
125
+ }
126
+
127
+ .devices-empty {
128
+ margin: 0;
129
+ color: #666;
130
+ font-size: 0.875rem;
131
+ grid-column: 1 / -1;
132
+ }
133
+
134
+ .device-card {
135
+ border: 1px solid #e4e1dc;
136
+ border-radius: 8px;
137
+ padding: 0.85rem 0.95rem;
138
+ background: #fafaf9;
139
+ }
140
+
141
+ .device-card-header {
142
+ display: flex;
143
+ align-items: center;
144
+ gap: 0.5rem;
145
+ margin-bottom: 0.65rem;
146
+ }
147
+
148
+ .device-card-title {
149
+ margin: 0;
150
+ font-size: 0.95rem;
151
+ font-weight: 600;
152
+ line-height: 1.2;
153
+ }
154
+
155
+ .status-led {
156
+ width: 11px;
157
+ height: 11px;
158
+ border-radius: 50%;
159
+ flex-shrink: 0;
160
+ box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06);
161
+ }
162
+
163
+ .status-led--online {
164
+ background: #12b76a;
165
+ box-shadow: 0 0 8px rgba(18, 183, 106, 0.55);
166
+ }
167
+
168
+ .status-led--offline {
169
+ background: #f04438;
170
+ box-shadow: 0 0 8px rgba(240, 68, 56, 0.45);
171
+ }
172
+
173
+ .device-meta {
174
+ display: grid;
175
+ gap: 0.35rem;
176
+ font-size: 0.78rem;
177
+ color: #444;
178
+ margin-bottom: 0.75rem;
179
+ }
180
+
181
+ .device-meta dt {
182
+ font-weight: 600;
183
+ color: #666;
184
+ display: inline;
185
+ }
186
+
187
+ .device-meta dt::after {
188
+ content: ": ";
189
+ }
190
+
191
+ .device-meta dd {
192
+ display: inline;
193
+ margin: 0;
194
+ word-break: break-all;
195
+ }
196
+
197
+ .device-meta-row {
198
+ display: block;
199
+ }
200
+
201
+ .device-card-actions {
202
+ display: flex;
203
+ flex-wrap: wrap;
204
+ gap: 0.4rem;
205
+ }
206
+
207
+ .device-card-actions button {
208
+ font-size: 0.78rem;
209
+ padding: 0.35rem 0.6rem;
210
+ }
211
+
212
+ .row {
213
+ display: flex;
214
+ flex-wrap: wrap;
215
+ gap: 0.5rem;
216
+ align-items: center;
217
+ }
218
+
219
+ input[type="text"],
220
+ input[type="date"],
221
+ input[type="time"],
222
+ select {
223
+ font: inherit;
224
+ padding: 0.45rem 0.6rem;
225
+ border: 1px solid #d6d2cb;
226
+ border-radius: 6px;
227
+ background: #fff;
228
+ }
229
+
230
+ button {
231
+ font: inherit;
232
+ padding: 0.45rem 0.85rem;
233
+ border-radius: 6px;
234
+ border: 1px solid #d6d2cb;
235
+ background: #fff;
236
+ cursor: pointer;
237
+ }
238
+
239
+ button:hover {
240
+ background: #f5f3ef;
241
+ }
242
+
243
+ button.primary {
244
+ background: #ff8a3d;
245
+ border-color: #e67320;
246
+ color: #0a0908;
247
+ font-weight: 600;
248
+ }
249
+
250
+ button.primary:hover {
251
+ background: #ff9a57;
252
+ }
253
+
254
+ button.danger {
255
+ color: #b42318;
256
+ border-color: #fecdca;
257
+ }
258
+
259
+ .schedule-grid {
260
+ display: grid;
261
+ grid-template-columns: 1fr 1fr;
262
+ gap: 0.65rem 1rem;
263
+ }
264
+
265
+ .schedule-grid label {
266
+ display: flex;
267
+ flex-direction: column;
268
+ gap: 0.25rem;
269
+ font-size: 0.8rem;
270
+ color: #444;
271
+ }
272
+
273
+ .days-row {
274
+ grid-column: 1 / -1;
275
+ display: flex;
276
+ flex-wrap: wrap;
277
+ gap: 0.5rem 0.75rem;
278
+ font-size: 0.8rem;
279
+ }
280
+
281
+ .days-row label {
282
+ flex-direction: row;
283
+ align-items: center;
284
+ gap: 0.35rem;
285
+ color: #0a0908;
286
+ }
287
+
288
+ .playlist-header {
289
+ display: flex;
290
+ align-items: center;
291
+ justify-content: space-between;
292
+ padding: 1rem 1rem 0.75rem;
293
+ border-bottom: 1px solid #e4e1dc;
294
+ }
295
+
296
+ .playlist-header h2 {
297
+ margin: 0;
298
+ font-size: 0.95rem;
299
+ }
300
+
301
+ .playlist-list {
302
+ list-style: none;
303
+ margin: 0;
304
+ padding: 0.5rem;
305
+ flex: 1;
306
+ overflow-y: auto;
307
+ }
308
+
309
+ .playlist-empty {
310
+ padding: 1.5rem 1rem;
311
+ text-align: center;
312
+ color: #888;
313
+ font-size: 0.875rem;
314
+ }
315
+
316
+ .playlist-item {
317
+ border: 1px solid #e4e1dc;
318
+ border-radius: 8px;
319
+ padding: 0.65rem;
320
+ margin-bottom: 0.5rem;
321
+ background: #fafaf9;
322
+ }
323
+
324
+ .playlist-item-thumb {
325
+ width: 100%;
326
+ aspect-ratio: 16 / 9;
327
+ object-fit: cover;
328
+ border-radius: 4px;
329
+ background: #e4e1dc;
330
+ display: block;
331
+ margin-bottom: 0.5rem;
332
+ }
333
+
334
+ .playlist-item-name {
335
+ font-size: 0.8rem;
336
+ font-weight: 600;
337
+ word-break: break-all;
338
+ margin-bottom: 0.35rem;
339
+ }
340
+
341
+ .playlist-item-meta {
342
+ font-size: 0.75rem;
343
+ color: #666;
344
+ margin-bottom: 0.45rem;
345
+ }
346
+
347
+ .playlist-item-actions {
348
+ display: flex;
349
+ gap: 0.35rem;
350
+ align-items: center;
351
+ }
352
+
353
+ .playlist-item-actions input[type="number"] {
354
+ width: 5rem;
355
+ font: inherit;
356
+ padding: 0.25rem 0.4rem;
357
+ border: 1px solid #d6d2cb;
358
+ border-radius: 4px;
359
+ }
360
+
361
+ .publish-bar {
362
+ padding: 1rem 0 0;
363
+ }
364
+
365
+ #result {
366
+ margin: 0;
367
+ padding: 0.75rem;
368
+ background: #0a0908;
369
+ color: #e8e6e3;
370
+ border-radius: 8px;
371
+ font-size: 0.75rem;
372
+ max-height: 10rem;
373
+ overflow: auto;
374
+ white-space: pre-wrap;
375
+ }
376
+
377
+ .hidden {
378
+ display: none !important;
379
+ }
380
+
381
+ .field-label {
382
+ display: block;
383
+ margin-bottom: 0.75rem;
384
+ font-size: 0.85rem;
385
+ font-weight: 600;
386
+ }
387
+
388
+ .field-label input {
389
+ display: block;
390
+ width: 100%;
391
+ margin-top: 0.35rem;
392
+ font-weight: 400;
393
+ }
394
+
395
+ .subheading {
396
+ margin: 0.5rem 0 0.35rem;
397
+ font-size: 0.9rem;
398
+ }
399
+
400
+ .editor-actions {
401
+ display: flex;
402
+ flex-wrap: wrap;
403
+ gap: 0.5rem;
404
+ margin-top: 0.75rem;
405
+ }
406
+
407
+ .device-published-list {
408
+ margin: 0 0 0.65rem;
409
+ padding: 0;
410
+ list-style: none;
411
+ font-size: 0.78rem;
412
+ }
413
+
414
+ .device-published-list li {
415
+ display: flex;
416
+ align-items: center;
417
+ justify-content: space-between;
418
+ gap: 0.35rem;
419
+ margin-bottom: 0.25rem;
420
+ }
421
+
422
+ .modal {
423
+ position: fixed;
424
+ inset: 0;
425
+ z-index: 1000;
426
+ display: flex;
427
+ align-items: center;
428
+ justify-content: center;
429
+ }
430
+
431
+ .modal-backdrop {
432
+ position: absolute;
433
+ inset: 0;
434
+ background: rgba(0, 0, 0, 0.45);
435
+ }
436
+
437
+ .modal-card {
438
+ position: relative;
439
+ background: #fff;
440
+ border-radius: 10px;
441
+ padding: 1.25rem;
442
+ width: min(92vw, 420px);
443
+ max-height: 80vh;
444
+ overflow: auto;
445
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.2);
446
+ }
447
+
448
+ .publish-checklist label {
449
+ display: block;
450
+ margin-bottom: 0.45rem;
451
+ font-size: 0.9rem;
452
+ }
453
+
454
+ .modal-actions {
455
+ display: flex;
456
+ gap: 0.5rem;
457
+ margin-top: 1rem;
458
+ }
@@ -1,55 +1,55 @@
1
- /**
2
- * TomorrowOS CMS server — minimal starter.
3
- * Add routes, database, or serve a React build via listen({ staticRoot }).
4
- */
5
-
6
- import { readFileSync } from "fs";
7
- import { fileURLToPath } from "url";
8
- import { dirname, join } from "path";
9
- import { TomorrowOS } from "@tomorrowos/sdk";
10
-
11
- const __dirname = dirname(fileURLToPath(import.meta.url));
12
- const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
13
-
14
- const tomorrowos = new TomorrowOS({ brand });
15
-
16
- tomorrowos.listen({
17
- port: Number(process.env.PORT) || 3000,
18
- host: "0.0.0.0",
19
- staticRoot: join(__dirname,"public"),
20
- });
21
-
22
- tomorrowos.on("device.paired", (event) => {
23
- console.log(`[TomorrowOS] device paired: ${event.deviceId}`);
24
- });
25
-
26
- tomorrowos.on("device.online", (event) => {
27
- console.log(`[TomorrowOS] device online: ${event.deviceId}`);
28
- void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
29
- if (r.pushed) {
30
- console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId}`);
31
- }
32
- }).catch((err) => {
33
- console.error(`[TomorrowOS] latest policy push failed for ${event.deviceId}:`, err);
34
- });
35
- });
36
-
37
- tomorrowos.on("device.offline", (event) => {
38
- console.log(
39
- `[TomorrowOS] device offline: ${event.deviceId} (lastSeen: ${event.lastSeen})`
40
- );
41
- });
42
-
43
- tomorrowos.on("command.verified", (event) => {
44
- console.log(
45
- `[TomorrowOS] command verified: ${event.commandId} (${event.method})`
46
- );
47
- });
48
-
49
- tomorrowos.on("command.failed", (event) => {
50
- console.error(
51
- `[TomorrowOS] command failed: ${event.commandId} (${event.method}) — ${event.error.message}`
52
- );
53
- });
54
-
55
- export default tomorrowos;
1
+ /**
2
+ * TomorrowOS CMS server — minimal starter.
3
+ * Add routes, database, or serve a React build via listen({ staticRoot }).
4
+ */
5
+
6
+ import { readFileSync } from "fs";
7
+ import { fileURLToPath } from "url";
8
+ import { dirname, join } from "path";
9
+ import { TomorrowOS } from "@tomorrowos/sdk";
10
+
11
+ const __dirname = dirname(fileURLToPath(import.meta.url));
12
+ const brand = JSON.parse(readFileSync(join(__dirname, "brand.json"), "utf8"));
13
+
14
+ const tomorrowos = new TomorrowOS({ brand });
15
+
16
+ tomorrowos.listen({
17
+ port: Number(process.env.PORT) || 3000,
18
+ host: "0.0.0.0",
19
+ staticRoot: join(__dirname,"public"),
20
+ });
21
+
22
+ tomorrowos.on("device.paired", (event) => {
23
+ console.log(`[TomorrowOS] device paired: ${event.deviceId}`);
24
+ });
25
+
26
+ tomorrowos.on("device.online", (event) => {
27
+ console.log(`[TomorrowOS] device online: ${event.deviceId}`);
28
+ void tomorrowos.pushLatestPolicyToDevice(event.deviceId).then((r) => {
29
+ if (r.pushed) {
30
+ console.log(`[TomorrowOS] pushed latest policy to ${event.deviceId}`);
31
+ }
32
+ }).catch((err) => {
33
+ console.error(`[TomorrowOS] latest policy push failed for ${event.deviceId}:`, err);
34
+ });
35
+ });
36
+
37
+ tomorrowos.on("device.offline", (event) => {
38
+ console.log(
39
+ `[TomorrowOS] device offline: ${event.deviceId} (lastSeen: ${event.lastSeen})`
40
+ );
41
+ });
42
+
43
+ tomorrowos.on("command.verified", (event) => {
44
+ console.log(
45
+ `[TomorrowOS] command verified: ${event.commandId} (${event.method})`
46
+ );
47
+ });
48
+
49
+ tomorrowos.on("command.failed", (event) => {
50
+ console.error(
51
+ `[TomorrowOS] command failed: ${event.commandId} (${event.method}) — ${event.error.message}`
52
+ );
53
+ });
54
+
55
+ export default tomorrowos;