@tomorrowos/sdk 0.3.5 → 0.3.7

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";
@@ -25,6 +26,29 @@ function formatDurationMs(ms) {
25
26
  parts.push(`${seconds}s`);
26
27
  return parts.join(" ");
27
28
  }
29
+ function parseBootIsoMs(iso) {
30
+ if (!iso)
31
+ return null;
32
+ const ms = new Date(iso).getTime();
33
+ return Number.isFinite(ms) ? ms : null;
34
+ }
35
+ /** Merge device-reported boot time with stored value (ignore page-reload noise, accept reboots). */
36
+ function resolveLastBootAt(incoming, existing) {
37
+ const inMs = parseBootIsoMs(incoming);
38
+ const exMs = parseBootIsoMs(existing);
39
+ if (inMs == null && exMs == null)
40
+ return undefined;
41
+ if (inMs == null)
42
+ return existing;
43
+ if (exMs == null)
44
+ return incoming;
45
+ const gapMs = inMs - exMs;
46
+ if (gapMs < 0)
47
+ return incoming;
48
+ if (gapMs > 120_000)
49
+ return incoming;
50
+ return new Date(Math.min(inMs, exMs)).toISOString();
51
+ }
28
52
  function resolveHelloDeviceId(msg) {
29
53
  const serial = typeof msg.serialNumber === "string" && msg.serialNumber.trim()
30
54
  ? msg.serialNumber.trim()
@@ -144,13 +168,14 @@ export class TomorrowOS extends EventEmitter {
144
168
  const pairedAt = new Date().toISOString();
145
169
  const meta = this.pendingDeviceMeta.get(deviceId);
146
170
  const now = pairedAt;
171
+ const lastBootAt = resolveLastBootAt(meta?.bootedAt, undefined);
147
172
  await this.store.setPairedDevice(deviceId, {
148
173
  pairingToken,
149
174
  pairedAt,
150
175
  deviceName: meta?.deviceName,
151
176
  platform: meta?.platform,
152
177
  system: meta?.system,
153
- lastBootAt: meta?.bootedAt ?? now,
178
+ ...(lastBootAt ? { lastBootAt } : {}),
154
179
  lastOnlineAt: this.isDeviceConnected(deviceId) ? now : undefined,
155
180
  lastOfflineAt: this.isDeviceConnected(deviceId) ? undefined : now
156
181
  });
@@ -292,6 +317,7 @@ export class TomorrowOS extends EventEmitter {
292
317
  const existing = await this.store.getPairedDevice(deviceId);
293
318
  if (!existing)
294
319
  return;
320
+ const lastBootAt = resolveLastBootAt(bootedAt, existing.lastBootAt);
295
321
  await this.store.setPairedDevice(deviceId, {
296
322
  ...existing,
297
323
  deviceName: (typeof msg.deviceName === "string" ? msg.deviceName : undefined) ??
@@ -300,7 +326,7 @@ export class TomorrowOS extends EventEmitter {
300
326
  existing.platform,
301
327
  system: (typeof msg.system === "string" ? msg.system : undefined) ??
302
328
  existing.system,
303
- lastBootAt: bootedAt ?? existing.lastBootAt ?? now,
329
+ ...(lastBootAt ? { lastBootAt } : {}),
304
330
  lastOnlineAt: now,
305
331
  lastOfflineAt: existing.lastOfflineAt
306
332
  });
@@ -440,6 +466,7 @@ export class TomorrowOS extends EventEmitter {
440
466
  this.staticIndexFile = idx;
441
467
  const uploadsDir = path.join(path.resolve(this.staticRoot), "uploads");
442
468
  void fs.mkdir(uploadsDir, { recursive: true });
469
+ void this.refreshResolvedBrandLogo();
443
470
  }
444
471
  else {
445
472
  this.staticIndexFile = "index.html";
@@ -504,6 +531,21 @@ export class TomorrowOS extends EventEmitter {
504
531
  sendJson(res, 400, { status: "failed", error: msg });
505
532
  }
506
533
  }
534
+ async refreshResolvedBrandLogo() {
535
+ if (!this.staticRoot)
536
+ return;
537
+ try {
538
+ await syncProjectAssetsToStaticRoot(this.staticRoot);
539
+ const logoPath = await resolveBrandLogoPath(this.staticRoot, this.brand.logoPath);
540
+ if (logoPath) {
541
+ this.brand.logoPath = logoPath;
542
+ console.log(`[TomorrowOS] brand logo: ${logoPath}`);
543
+ }
544
+ }
545
+ catch (err) {
546
+ console.warn("[TomorrowOS] brand logo resolve failed:", err);
547
+ }
548
+ }
507
549
  async tryServeStatic(pathname, res) {
508
550
  if (!this.staticRoot)
509
551
  return false;
@@ -523,7 +565,13 @@ export class TomorrowOS extends EventEmitter {
523
565
  ".html": "text/html; charset=utf-8",
524
566
  ".js": "application/javascript; charset=utf-8",
525
567
  ".css": "text/css; charset=utf-8",
526
- ".json": "application/json; charset=utf-8"
568
+ ".json": "application/json; charset=utf-8",
569
+ ".svg": "image/svg+xml",
570
+ ".png": "image/png",
571
+ ".jpg": "image/jpeg",
572
+ ".jpeg": "image/jpeg",
573
+ ".webp": "image/webp",
574
+ ".gif": "image/gif"
527
575
  };
528
576
  const ctype = types[ext] ?? "application/octet-stream";
529
577
  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.7",
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.7",
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.7"
14
14
  },
15
15
  "devDependencies": {
16
16
  "@types/node": "^20.0.0",
@@ -47,7 +47,15 @@ function formatDateTimeSeconds(iso) {
47
47
  if (!iso) return "—";
48
48
  const d = new Date(iso);
49
49
  if (Number.isNaN(d.getTime())) return "—";
50
- return d.toLocaleString();
50
+ return d.toLocaleString(undefined, {
51
+ year: "numeric",
52
+ month: "short",
53
+ day: "numeric",
54
+ hour: "2-digit",
55
+ minute: "2-digit",
56
+ second: "2-digit",
57
+ hour12: false
58
+ });
51
59
  }
52
60
 
53
61
  function formatDeviceOnlineLabel(device) {
@@ -217,7 +225,7 @@ async function fetchPlaylists() {
217
225
  showResult({
218
226
  status: "failed",
219
227
  error:
220
- "CMS server is missing /playlists. Restart CMS with @tomorrowos/sdk 0.3.5 or newer."
228
+ "CMS server is missing /playlists. Restart CMS with @tomorrowos/sdk 0.3.7 or newer."
221
229
  });
222
230
  }
223
231
  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;