@tomorrowos/sdk 0.9.5 → 0.9.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.
package/REPLIT_SETUP.md CHANGED
@@ -230,7 +230,7 @@ Allowed extras (optional): `"build-player": "tomorrowos build --platform tizen"`
230
230
  "node": ">=20"
231
231
  },
232
232
  "dependencies": {
233
- "@tomorrowos/sdk": "^0.9.5",
233
+ "@tomorrowos/sdk": "^0.9.7",
234
234
  "dotenv": "^17.2.3",
235
235
  "tsx": "^4.19.0"
236
236
  },
@@ -17,4 +17,6 @@ export declare function uploadBufferToCloudinary(body: Buffer, options: {
17
17
  filename: string;
18
18
  }): Promise<CloudinaryUploadResult>;
19
19
  export declare function deleteCloudinaryAsset(config: CloudinaryStorageConfig, publicId: string, resourceType?: string): Promise<void>;
20
+ /** Lightweight credential / reachability check for CMS status UI. */
21
+ export declare function pingCloudinary(config: CloudinaryStorageConfig): Promise<void>;
20
22
  //# sourceMappingURL=cloudinary-storage.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"cloudinary-storage.d.ts","sourceRoot":"","sources":["../src/cloudinary-storage.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,uBAAuB,GAAG,IAAI,CAoBhC;AAWD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,MAAM,EAAE,uBAAuB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,sBAAsB,CAAC,CAsCjC;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,MAAM,EAChB,YAAY,SAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAKf"}
1
+ {"version":3,"file":"cloudinary-storage.d.ts","sourceRoot":"","sources":["../src/cloudinary-storage.ts"],"names":[],"mappings":"AAEA,MAAM,WAAW,uBAAuB;IACtC,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,EAAE,MAAM,CAAC;IACf,SAAS,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,sBAAsB;IACrC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;IAClB,YAAY,EAAE,MAAM,CAAC;IACrB,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB;AAOD,wBAAgB,uBAAuB,CACrC,GAAG,GAAE,MAAM,CAAC,UAAwB,GACnC,uBAAuB,GAAG,IAAI,CAoBhC;AAWD,wBAAsB,wBAAwB,CAC5C,IAAI,EAAE,MAAM,EACZ,OAAO,EAAE;IACP,MAAM,EAAE,uBAAuB,CAAC;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,QAAQ,EAAE,MAAM,CAAC;CAClB,GACA,OAAO,CAAC,sBAAsB,CAAC,CAsCjC;AAED,wBAAsB,qBAAqB,CACzC,MAAM,EAAE,uBAAuB,EAC/B,QAAQ,EAAE,MAAM,EAChB,YAAY,SAAU,GACrB,OAAO,CAAC,IAAI,CAAC,CAKf;AAED,qEAAqE;AACrE,wBAAsB,cAAc,CAClC,MAAM,EAAE,uBAAuB,GAC9B,OAAO,CAAC,IAAI,CAAC,CAGf"}
@@ -68,3 +68,8 @@ export async function deleteCloudinaryAsset(config, publicId, resourceType = "im
68
68
  resource_type: resourceType
69
69
  });
70
70
  }
71
+ /** Lightweight credential / reachability check for CMS status UI. */
72
+ export async function pingCloudinary(config) {
73
+ configureCloudinary(config);
74
+ await cloudinary.api.ping();
75
+ }
package/dist/index.d.ts CHANGED
@@ -1,5 +1,7 @@
1
1
  export { TomorrowOS } from "./tomorrowos.js";
2
2
  export type { DeviceListItem, DeviceScreenshotInfo, ListenOptions, TomorrowOSBrand, TomorrowOSOptions } from "./tomorrowos.js";
3
+ export type { ConnectorState, ConnectorStatus, ServerStatusReport, StatusBlocker } from "./server-status.js";
4
+ export { buildServerStatus } from "./server-status.js";
3
5
  export type { DevicePlaylistAssignment, DeviceRegistryRecord, PairedDeviceRecord, PendingCodeEntry, PendingCodeRecord, DeviceRegistryEntry, PlaylistItemRecord, PlaylistSchedule, PublishedPlaylistSnapshot, StoredPlaylist, TomorrowOSDataSnapshot, TomorrowOSMigratableStore, TomorrowOSStore } from "./store/types.js";
4
6
  export { PlaylistCatalog } from "./playlist-catalog.js";
5
7
  export type { BuiltDevicePolicy, SavePlaylistInput } from "./playlist-catalog.js";
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,2BAA2B,EAC3B,gCAAgC,EAChC,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,oBAAoB,EACpB,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AACzB,YAAY,EACV,cAAc,EACd,eAAe,EACf,kBAAkB,EAClB,aAAa,EACd,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EAAE,iBAAiB,EAAE,MAAM,oBAAoB,CAAC;AAEvD,YAAY,EACV,wBAAwB,EACxB,oBAAoB,EACpB,kBAAkB,EAClB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,EACnB,kBAAkB,EAClB,gBAAgB,EAChB,yBAAyB,EACzB,cAAc,EACd,sBAAsB,EACtB,yBAAyB,EACzB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,eAAe,EAAE,MAAM,uBAAuB,CAAC;AACxD,YAAY,EAAE,iBAAiB,EAAE,iBAAiB,EAAE,MAAM,uBAAuB,CAAC;AAClF,OAAO,EACL,2BAA2B,EAC3B,gCAAgC,EAChC,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACnB,wBAAwB,EACzB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EAAE,aAAa,EAAE,MAAM,2BAA2B,CAAC;AAC1D,YAAY,EAAE,oBAAoB,EAAE,MAAM,2BAA2B,CAAC;AACtE,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC;AACtD,OAAO,EACL,qBAAqB,EACrB,iBAAiB,EAClB,MAAM,oBAAoB,CAAC;AAC5B,YAAY,EACV,4BAA4B,EAC5B,qBAAqB,EACtB,MAAM,oBAAoB,CAAC;AAC5B,OAAO,EACL,oBAAoB,EACpB,oBAAoB,EACpB,qBAAqB,EACtB,MAAM,sBAAsB,CAAC;AAC9B,YAAY,EAAE,eAAe,EAAE,MAAM,sBAAsB,CAAC"}
package/dist/index.js CHANGED
@@ -1,4 +1,5 @@
1
1
  export { TomorrowOS } from "./tomorrowos.js";
2
+ export { buildServerStatus } from "./server-status.js";
2
3
  export { PlaylistCatalog } from "./playlist-catalog.js";
3
4
  export { DEFAULT_PAIRING_CODE_SECRET, generateDeterministicPairingCode, generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH, resolvePairingCodeSecret } from "./pairing-code.js";
4
5
  export { MemoryStore } from "./store/memory-store.js";
@@ -0,0 +1,30 @@
1
+ import type { TomorrowOSStore } from "./store/types.js";
2
+ export type ConnectorState = "ok" | "warn" | "error" | "missing";
3
+ export interface ConnectorStatus {
4
+ id: string;
5
+ label: string;
6
+ provider?: string;
7
+ state: ConnectorState;
8
+ detail: string;
9
+ }
10
+ export interface StatusBlocker {
11
+ connectorId: string;
12
+ title: string;
13
+ message: string;
14
+ fixHint: string;
15
+ }
16
+ export interface ServerStatusReport {
17
+ status: "success";
18
+ overall: "ok" | "degraded" | "blocked";
19
+ checkedAt: string;
20
+ connectors: ConnectorStatus[];
21
+ blockers: StatusBlocker[];
22
+ }
23
+ export interface BuildServerStatusOptions {
24
+ store: TomorrowOSStore;
25
+ staticRoot?: string | null;
26
+ env?: NodeJS.ProcessEnv;
27
+ probeTimeoutMs?: number;
28
+ }
29
+ export declare function buildServerStatus(options: BuildServerStatusOptions): Promise<ServerStatusReport>;
30
+ //# sourceMappingURL=server-status.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"server-status.d.ts","sourceRoot":"","sources":["../src/server-status.ts"],"names":[],"mappings":"AAOA,OAAO,KAAK,EAAE,eAAe,EAAE,MAAM,kBAAkB,CAAC;AAExD,MAAM,MAAM,cAAc,GAAG,IAAI,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAEjE,MAAM,WAAW,eAAe;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,cAAc,CAAC;IACtB,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,aAAa;IAC5B,WAAW,EAAE,MAAM,CAAC;IACpB,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,kBAAkB;IACjC,MAAM,EAAE,SAAS,CAAC;IAClB,OAAO,EAAE,IAAI,GAAG,UAAU,GAAG,SAAS,CAAC;IACvC,SAAS,EAAE,MAAM,CAAC;IAClB,UAAU,EAAE,eAAe,EAAE,CAAC;IAC9B,QAAQ,EAAE,aAAa,EAAE,CAAC;CAC3B;AAED,MAAM,WAAW,wBAAwB;IACvC,KAAK,EAAE,eAAe,CAAC;IACvB,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACxB,cAAc,CAAC,EAAE,MAAM,CAAC;CACzB;AAoSD,wBAAsB,iBAAiB,CACrC,OAAO,EAAE,wBAAwB,GAChC,OAAO,CAAC,kBAAkB,CAAC,CA4B7B"}
@@ -0,0 +1,265 @@
1
+ import fs from "fs/promises";
2
+ import path from "path";
3
+ import { pingCloudinary, resolveCloudinaryConfig } from "./cloudinary-storage.js";
4
+ function isReplitHost(env) {
5
+ return !!(env.REPL_ID ||
6
+ env.REPLIT_DEV_DOMAIN ||
7
+ env.REPLIT_DEPLOYMENT ||
8
+ env.REPL_SLUG);
9
+ }
10
+ /** Replit Preview/dev often cannot resolve Supabase hosts — not a config error. */
11
+ function isReplitPreviewDnsFailure(message) {
12
+ const m = String(message || "").toLowerCase();
13
+ return (m.includes("enotfound") ||
14
+ m.includes("getaddrinfo") ||
15
+ m.includes("eai_again") ||
16
+ m.includes("can't be resolved") ||
17
+ m.includes("cannot resolve") ||
18
+ m.includes("name or service not known"));
19
+ }
20
+ function resolveDatabaseProvider(env) {
21
+ const driver = String(env.TOMORROWOS_STORE || "").trim().toLowerCase();
22
+ const hasUrl = !!(env.SUPABASE_URL || env.DATABASE_URL);
23
+ if (driver === "supabase" || (!driver && env.SUPABASE_URL)) {
24
+ return {
25
+ provider: "supabase",
26
+ label: "Supabase",
27
+ configured: !!env.SUPABASE_URL || !!env.DATABASE_URL
28
+ };
29
+ }
30
+ if (driver === "postgres" || (!driver && hasUrl && !env.SUPABASE_URL)) {
31
+ return {
32
+ provider: "postgres",
33
+ label: "Postgres",
34
+ configured: hasUrl
35
+ };
36
+ }
37
+ if (driver === "memory") {
38
+ return { provider: "memory", label: "Memory Store", configured: true };
39
+ }
40
+ if (driver === "sqlite" || !driver) {
41
+ return { provider: "sqlite", label: "SQLite", configured: true };
42
+ }
43
+ return {
44
+ provider: driver || "unknown",
45
+ label: driver ? `Store (${driver})` : "Database",
46
+ configured: hasUrl || driver === "sqlite" || driver === "memory"
47
+ };
48
+ }
49
+ async function withTimeout(promise, timeoutMs, label) {
50
+ let timer;
51
+ try {
52
+ return await Promise.race([
53
+ promise,
54
+ new Promise((_, reject) => {
55
+ timer = setTimeout(() => reject(new Error(`${label} probe timed out after ${timeoutMs}ms`)), timeoutMs);
56
+ })
57
+ ]);
58
+ }
59
+ finally {
60
+ if (timer)
61
+ clearTimeout(timer);
62
+ }
63
+ }
64
+ async function probeDatabase(store, env, timeoutMs) {
65
+ const meta = resolveDatabaseProvider(env);
66
+ const wantsRemote = meta.provider === "supabase" || meta.provider === "postgres";
67
+ if (wantsRemote && !meta.configured) {
68
+ return {
69
+ id: "database",
70
+ label: meta.label,
71
+ provider: meta.provider,
72
+ state: "missing",
73
+ detail: "Connection string not set. Add SUPABASE_URL (preferred) or DATABASE_URL."
74
+ };
75
+ }
76
+ try {
77
+ await withTimeout(store.listPairedDevices(), timeoutMs, meta.label);
78
+ if (meta.provider === "memory") {
79
+ return {
80
+ id: "database",
81
+ label: meta.label,
82
+ provider: meta.provider,
83
+ state: "warn",
84
+ detail: "In-memory only — data is lost on restart."
85
+ };
86
+ }
87
+ if (meta.provider === "sqlite" && isReplitHost(env)) {
88
+ return {
89
+ id: "database",
90
+ label: meta.label,
91
+ provider: meta.provider,
92
+ state: "warn",
93
+ detail: "SQLite works for demos; pairings may reset on rebuild. Prefer Supabase for production."
94
+ };
95
+ }
96
+ return {
97
+ id: "database",
98
+ label: meta.label,
99
+ provider: meta.provider,
100
+ state: "ok",
101
+ detail: `${meta.label} reachable`
102
+ };
103
+ }
104
+ catch (err) {
105
+ const message = err instanceof Error ? err.message : String(err);
106
+ if (isReplitHost(env) &&
107
+ wantsRemote &&
108
+ isReplitPreviewDnsFailure(message)) {
109
+ return {
110
+ id: "database",
111
+ label: meta.label,
112
+ provider: meta.provider,
113
+ state: "warn",
114
+ detail: "Replit Preview cannot verify this connection (DNS ENOTFOUND to Supabase is common in preview). Secrets are configured — confirm on the published live URL after Publish, not in Preview."
115
+ };
116
+ }
117
+ return {
118
+ id: "database",
119
+ label: meta.label,
120
+ provider: meta.provider,
121
+ state: "error",
122
+ detail: message
123
+ };
124
+ }
125
+ }
126
+ async function probeMedia(staticRoot, env, timeoutMs) {
127
+ const cloudinary = resolveCloudinaryConfig(env);
128
+ if (cloudinary) {
129
+ try {
130
+ await withTimeout(pingCloudinary(cloudinary), timeoutMs, "Cloudinary");
131
+ return {
132
+ id: "media",
133
+ label: "Media Server",
134
+ provider: "cloudinary",
135
+ state: "ok",
136
+ detail: `Cloudinary (${cloudinary.cloudName}) reachable`
137
+ };
138
+ }
139
+ catch (err) {
140
+ const message = err instanceof Error ? err.message : String(err);
141
+ return {
142
+ id: "media",
143
+ label: "Media Server",
144
+ provider: "cloudinary",
145
+ state: "error",
146
+ detail: message
147
+ };
148
+ }
149
+ }
150
+ const hasPartialCloudinary = !!(env.CLOUDINARY_CLOUD_NAME ||
151
+ env.CLOUDINARY_API_KEY ||
152
+ env.CLOUDINARY_API_SECRET);
153
+ if (hasPartialCloudinary) {
154
+ return {
155
+ id: "media",
156
+ label: "Media Server",
157
+ provider: "cloudinary",
158
+ state: "error",
159
+ detail: "Cloudinary config incomplete — set CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, and CLOUDINARY_API_SECRET."
160
+ };
161
+ }
162
+ const provider = isReplitHost(env) ? "replit-object-storage" : "local";
163
+ const providerLabel = provider === "replit-object-storage"
164
+ ? "Replit Object Storage / local uploads"
165
+ : "Local uploads";
166
+ if (!staticRoot) {
167
+ return {
168
+ id: "media",
169
+ label: "Media Server",
170
+ provider,
171
+ state: "missing",
172
+ detail: "No Cloudinary and staticRoot is unset — media uploads are unavailable."
173
+ };
174
+ }
175
+ const uploadsDir = path.join(path.resolve(staticRoot), "uploads");
176
+ try {
177
+ await fs.mkdir(uploadsDir, { recursive: true });
178
+ await fs.access(uploadsDir);
179
+ return {
180
+ id: "media",
181
+ label: "Media Server",
182
+ provider,
183
+ state: "warn",
184
+ detail: `${providerLabel} writable. Uploads may not survive rebuilds — Cloudinary recommended.`
185
+ };
186
+ }
187
+ catch (err) {
188
+ const message = err instanceof Error ? err.message : String(err);
189
+ return {
190
+ id: "media",
191
+ label: "Media Server",
192
+ provider,
193
+ state: "error",
194
+ detail: `Uploads directory not writable (${uploadsDir}): ${message}`
195
+ };
196
+ }
197
+ }
198
+ function buildBlockers(connectors) {
199
+ const blockers = [];
200
+ for (const c of connectors) {
201
+ if (c.id === "database" && (c.state === "missing" || c.state === "error")) {
202
+ blockers.push({
203
+ connectorId: c.id,
204
+ title: `${c.label} not connected`,
205
+ message: c.detail,
206
+ fixHint: c.state === "missing"
207
+ ? "Paste your Supabase Postgres connection string as Secret SUPABASE_URL, set TOMORROWOS_STORE=supabase, then restart the CMS."
208
+ : "Check SUPABASE_URL / network / SSL (DATABASE_SSL=true). After Publish, confirm the live URL can reach Supabase."
209
+ });
210
+ }
211
+ if (c.id === "media" && c.state === "error") {
212
+ blockers.push({
213
+ connectorId: c.id,
214
+ title: "Media storage not ready",
215
+ message: c.detail,
216
+ fixHint: c.provider === "cloudinary"
217
+ ? "Fix Cloudinary Secrets (CLOUDINARY_CLOUD_NAME, CLOUDINARY_API_KEY, CLOUDINARY_API_SECRET) and restart."
218
+ : "Enable Object Storage for public/uploads, or configure Cloudinary for durable media URLs."
219
+ });
220
+ }
221
+ if (c.id === "media" && c.state === "missing") {
222
+ blockers.push({
223
+ connectorId: c.id,
224
+ title: "Media storage not configured",
225
+ message: c.detail,
226
+ fixHint: "Set Cloudinary Secrets, or ensure the CMS listens with staticRoot pointing at public/ (uploads folder)."
227
+ });
228
+ }
229
+ }
230
+ return blockers;
231
+ }
232
+ function overallFrom(connectors) {
233
+ const hardFail = connectors.some((c) => (c.id === "database" || c.id === "media") &&
234
+ (c.state === "error" || c.state === "missing"));
235
+ if (hardFail)
236
+ return "blocked";
237
+ if (connectors.some((c) => c.state === "warn"))
238
+ return "degraded";
239
+ return "ok";
240
+ }
241
+ export async function buildServerStatus(options) {
242
+ const env = options.env ?? process.env;
243
+ const timeoutMs = options.probeTimeoutMs ?? 5000;
244
+ const server = {
245
+ id: "server",
246
+ label: "Server",
247
+ provider: "tomorrowos",
248
+ state: "ok",
249
+ detail: "CMS process is running"
250
+ };
251
+ const [database, media] = await Promise.all([
252
+ probeDatabase(options.store, env, timeoutMs),
253
+ probeMedia(options.staticRoot, env, timeoutMs)
254
+ ]);
255
+ const connectors = [server, database, media];
256
+ const blockers = buildBlockers(connectors);
257
+ const overall = overallFrom(connectors);
258
+ return {
259
+ status: "success",
260
+ overall,
261
+ checkedAt: new Date().toISOString(),
262
+ connectors,
263
+ blockers
264
+ };
265
+ }
@@ -2,6 +2,7 @@ import { EventEmitter } from "events";
2
2
  import http from "http";
3
3
  import { PlaylistCatalog, type BuiltDevicePolicy } from "./playlist-catalog.js";
4
4
  import type { TomorrowOSStore } from "./store/types.js";
5
+ export type { ConnectorState, ConnectorStatus, ServerStatusReport, StatusBlocker } from "./server-status.js";
5
6
  export interface TomorrowOSBrand {
6
7
  name?: string;
7
8
  cmsEndpoint?: string;
@@ -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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"}
@@ -11,6 +11,7 @@ import { resolveBrandLogoPath, syncProjectAssetsToStaticRoot } from "./brand-ass
11
11
  import { deleteCloudinaryAsset, resolveCloudinaryConfig, uploadBufferToCloudinary } from "./cloudinary-storage.js";
12
12
  import { probeVideoDurationMs } from "./media-probe.js";
13
13
  import { contentHashHex, storeUploadIfNeeded } from "./upload-storage.js";
14
+ import { buildServerStatus } from "./server-status.js";
14
15
  function normalizeDevicePlatform(platform, system) {
15
16
  const explicit = typeof platform === "string" ? platform.trim().toLowerCase() : "";
16
17
  if (explicit)
@@ -1024,6 +1025,14 @@ export class TomorrowOS extends EventEmitter {
1024
1025
  res.end(JSON.stringify(this.brand));
1025
1026
  return;
1026
1027
  }
1028
+ if (req.method === "GET" && pathname === "/status") {
1029
+ const report = await buildServerStatus({
1030
+ store: this.store,
1031
+ staticRoot: this.staticRoot
1032
+ });
1033
+ sendJson(res, 200, report);
1034
+ return;
1035
+ }
1027
1036
  if (req.method === "POST" && pathname === "/media/upload") {
1028
1037
  await this.handleMediaUpload(req, res, url);
1029
1038
  return;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tomorrowos/sdk",
3
- "version": "0.9.5",
3
+ "version": "0.9.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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "my-cms",
3
- "version": "0.9.5",
3
+ "version": "0.9.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",
@@ -13,7 +13,7 @@
13
13
  "build-player": "tomorrowos build --platform tizen"
14
14
  },
15
15
  "dependencies": {
16
- "@tomorrowos/sdk": "^0.9.5",
16
+ "@tomorrowos/sdk": "^0.9.7",
17
17
  "dotenv": "^17.2.3",
18
18
  "tsx": "^4.19.0"
19
19
  },
@@ -27,6 +27,16 @@
27
27
  </aside>
28
28
 
29
29
  <main class="panel-main">
30
+ <section class="card server-status-card" id="serverStatusSection" aria-live="polite">
31
+ <div class="server-status-header">
32
+ <h2>Server status</h2>
33
+ <button type="button" id="serverStatusRefreshBtn" title="Refresh status">Refresh</button>
34
+ </div>
35
+ <p class="hint server-status-overall" id="serverStatusOverall">Checking connectors…</p>
36
+ <ul class="server-status-list" id="serverStatusList"></ul>
37
+ <div id="serverStatusBlockers" class="server-status-blockers hidden"></div>
38
+ </section>
39
+
30
40
  <section class="card">
31
41
  <h2>Pair device</h2>
32
42
  <p class="hint">Enter the 6-character code on the screen (A–Z or 0–9).</p>
@@ -27,6 +27,7 @@ let editingDeviceNameId = null;
27
27
  let editingDeviceNameValue = "";
28
28
 
29
29
  let devicePollTimer = null;
30
+ let serverStatusTimer = null;
30
31
  /** @type {number|null} CMS server boot time (ms) from GET /devices. */
31
32
  let serverStartedAtMs = null;
32
33
  /** @type {ReturnType<typeof setTimeout>|null} */
@@ -1857,12 +1858,169 @@ async function downloadMediaAsset(item) {
1857
1858
  openDownloadFailedModal(lastError);
1858
1859
  }
1859
1860
 
1861
+ function connectorStateLabel(state) {
1862
+ if (state === "ok") return "OK";
1863
+ if (state === "warn") return "Warning";
1864
+ if (state === "missing") return "Not set";
1865
+ return "Error";
1866
+ }
1867
+
1868
+ function overallStatusCopy(overall) {
1869
+ if (overall === "ok") return "All connectors look healthy.";
1870
+ if (overall === "degraded") {
1871
+ return "CMS is running with warnings — review media or database durability below.";
1872
+ }
1873
+ if (overall === "blocked") {
1874
+ return "Setup incomplete — fix the blockers below so pairing and media work reliably.";
1875
+ }
1876
+ return "Checking connectors…";
1877
+ }
1878
+
1879
+ function renderServerStatus(report) {
1880
+ const section = document.getElementById("serverStatusSection");
1881
+ const list = document.getElementById("serverStatusList");
1882
+ const overallEl = document.getElementById("serverStatusOverall");
1883
+ const blockersEl = document.getElementById("serverStatusBlockers");
1884
+ if (!section || !list || !overallEl || !blockersEl) return;
1885
+
1886
+ const overall = report?.overall || "blocked";
1887
+ section.classList.remove(
1888
+ "server-status-card--ok",
1889
+ "server-status-card--degraded",
1890
+ "server-status-card--blocked"
1891
+ );
1892
+ section.classList.add(`server-status-card--${overall}`);
1893
+ overallEl.textContent = overallStatusCopy(overall);
1894
+
1895
+ list.innerHTML = "";
1896
+ const connectors = Array.isArray(report?.connectors) ? report.connectors : [];
1897
+ for (const connector of connectors) {
1898
+ const li = document.createElement("li");
1899
+ li.className = "server-status-row";
1900
+
1901
+ const main = document.createElement("div");
1902
+ main.className = "server-status-row-main";
1903
+
1904
+ const label = document.createElement("span");
1905
+ label.className = "server-status-label";
1906
+ label.textContent = connector.label || connector.id || "Connector";
1907
+ main.appendChild(label);
1908
+
1909
+ if (connector.provider) {
1910
+ const provider = document.createElement("span");
1911
+ provider.className = "server-status-provider";
1912
+ provider.textContent = connector.provider;
1913
+ main.appendChild(provider);
1914
+ }
1915
+
1916
+ const badge = document.createElement("span");
1917
+ const state = connector.state || "error";
1918
+ badge.className = `server-status-badge server-status-badge--${state}`;
1919
+ badge.textContent = connectorStateLabel(state);
1920
+
1921
+ const detail = document.createElement("p");
1922
+ detail.className = "server-status-detail";
1923
+ detail.textContent = connector.detail || "";
1924
+
1925
+ li.appendChild(main);
1926
+ li.appendChild(badge);
1927
+ li.appendChild(detail);
1928
+ list.appendChild(li);
1929
+ }
1930
+
1931
+ const blockers = Array.isArray(report?.blockers) ? report.blockers : [];
1932
+ blockersEl.innerHTML = "";
1933
+ if (blockers.length === 0) {
1934
+ blockersEl.classList.add("hidden");
1935
+ return;
1936
+ }
1937
+
1938
+ blockersEl.classList.remove("hidden");
1939
+ for (const blocker of blockers) {
1940
+ const card = document.createElement("div");
1941
+ card.className = "server-status-blocker";
1942
+
1943
+ const title = document.createElement("h3");
1944
+ title.textContent = blocker.title || "Connector issue";
1945
+
1946
+ const message = document.createElement("p");
1947
+ message.textContent = blocker.message || "";
1948
+
1949
+ const fix = document.createElement("p");
1950
+ fix.className = "server-status-blocker-fix";
1951
+ fix.textContent = blocker.fixHint
1952
+ ? `How to fix: ${blocker.fixHint}`
1953
+ : "How to fix: update Secrets / env and restart the CMS.";
1954
+
1955
+ card.appendChild(title);
1956
+ card.appendChild(message);
1957
+ card.appendChild(fix);
1958
+ blockersEl.appendChild(card);
1959
+ }
1960
+ }
1961
+
1962
+ async function fetchServerStatus() {
1963
+ try {
1964
+ const res = await fetch("/status", { cache: "no-store" });
1965
+ const data = await res.json();
1966
+ if (!res.ok) {
1967
+ renderServerStatus({
1968
+ overall: "blocked",
1969
+ connectors: [
1970
+ {
1971
+ id: "server",
1972
+ label: "Server",
1973
+ state: "error",
1974
+ detail: data?.error || `HTTP ${res.status}`
1975
+ }
1976
+ ],
1977
+ blockers: [
1978
+ {
1979
+ connectorId: "server",
1980
+ title: "Could not load server status",
1981
+ message: data?.error || `HTTP ${res.status}`,
1982
+ fixHint: "Confirm the CMS is running with @tomorrowos/sdk that includes GET /status."
1983
+ }
1984
+ ]
1985
+ });
1986
+ return;
1987
+ }
1988
+ renderServerStatus(data);
1989
+ } catch (err) {
1990
+ renderServerStatus({
1991
+ overall: "blocked",
1992
+ connectors: [
1993
+ {
1994
+ id: "server",
1995
+ label: "Server",
1996
+ state: "error",
1997
+ detail: err?.message || "Network error"
1998
+ }
1999
+ ],
2000
+ blockers: [
2001
+ {
2002
+ connectorId: "server",
2003
+ title: "CMS unreachable",
2004
+ message: err?.message || "Network error",
2005
+ fixHint: "Start the CMS (`npm run start`) and reload this page."
2006
+ }
2007
+ ]
2008
+ });
2009
+ }
2010
+ }
2011
+
1860
2012
  function startDevicePolling() {
1861
2013
  if (devicePollTimer) clearInterval(devicePollTimer);
1862
2014
  void fetchDevices();
1863
2015
  devicePollTimer = setInterval(() => void fetchDevices(), 8000);
1864
2016
  }
1865
2017
 
2018
+ function startServerStatusPolling() {
2019
+ if (serverStatusTimer) clearInterval(serverStatusTimer);
2020
+ void fetchServerStatus();
2021
+ serverStatusTimer = setInterval(() => void fetchServerStatus(), 30000);
2022
+ }
2023
+
1866
2024
  document.addEventListener("DOMContentLoaded", () => {
1867
2025
  const cmsUrlSection = document.getElementById("cmsUrlSection");
1868
2026
  if (cmsUrlSection && !isLocalPanelHost(window.location.hostname)) {
@@ -1876,6 +2034,11 @@ document.addEventListener("DOMContentLoaded", () => {
1876
2034
  updatePlaylistEditorVisibility();
1877
2035
  void fetchPlaylists();
1878
2036
  startDevicePolling();
2037
+ startServerStatusPolling();
2038
+
2039
+ document
2040
+ .getElementById("serverStatusRefreshBtn")
2041
+ ?.addEventListener("click", () => void fetchServerStatus());
1879
2042
 
1880
2043
  document.getElementById("newPlaylistBtn")?.addEventListener("click", newPlaylistDraft);
1881
2044
  document.getElementById("savePlaylistBtn")?.addEventListener("click", () => void saveCurrentPlaylist());
@@ -118,6 +118,153 @@ body {
118
118
  color: #666;
119
119
  }
120
120
 
121
+ .server-status-card {
122
+ border-color: #e4e1dc;
123
+ }
124
+
125
+ .server-status-header {
126
+ display: flex;
127
+ align-items: center;
128
+ justify-content: space-between;
129
+ gap: 0.75rem;
130
+ margin-bottom: 0.35rem;
131
+ }
132
+
133
+ .server-status-header h2 {
134
+ margin: 0;
135
+ }
136
+
137
+ .server-status-header button {
138
+ font-size: 0.78rem;
139
+ padding: 0.35rem 0.65rem;
140
+ }
141
+
142
+ .server-status-overall {
143
+ margin-bottom: 0.65rem;
144
+ }
145
+
146
+ .server-status-list {
147
+ list-style: none;
148
+ margin: 0;
149
+ padding: 0;
150
+ display: grid;
151
+ gap: 0.45rem;
152
+ }
153
+
154
+ .server-status-row {
155
+ display: grid;
156
+ grid-template-columns: minmax(0, 1fr) auto;
157
+ gap: 0.35rem 0.75rem;
158
+ align-items: baseline;
159
+ padding: 0.55rem 0.65rem;
160
+ border: 1px solid #ece8e2;
161
+ border-radius: 8px;
162
+ background: #fafaf9;
163
+ }
164
+
165
+ .server-status-row-main {
166
+ display: flex;
167
+ flex-wrap: wrap;
168
+ align-items: baseline;
169
+ gap: 0.35rem 0.55rem;
170
+ min-width: 0;
171
+ }
172
+
173
+ .server-status-label {
174
+ font-size: 0.85rem;
175
+ font-weight: 600;
176
+ color: #0a0908;
177
+ }
178
+
179
+ .server-status-provider {
180
+ font-size: 0.72rem;
181
+ color: #888;
182
+ }
183
+
184
+ .server-status-detail {
185
+ grid-column: 1 / -1;
186
+ margin: 0;
187
+ font-size: 0.75rem;
188
+ color: #555;
189
+ line-height: 1.4;
190
+ }
191
+
192
+ .server-status-badge {
193
+ font-size: 0.72rem;
194
+ font-weight: 600;
195
+ letter-spacing: 0.02em;
196
+ text-transform: uppercase;
197
+ padding: 0.18rem 0.45rem;
198
+ border-radius: 999px;
199
+ border: 1px solid transparent;
200
+ white-space: nowrap;
201
+ }
202
+
203
+ .server-status-badge--ok {
204
+ color: #067647;
205
+ background: #ecfdf3;
206
+ border-color: #abefc6;
207
+ }
208
+
209
+ .server-status-badge--warn {
210
+ color: #b54708;
211
+ background: #fffaeb;
212
+ border-color: #fedf89;
213
+ }
214
+
215
+ .server-status-badge--error,
216
+ .server-status-badge--missing {
217
+ color: #b42318;
218
+ background: #fff5f5;
219
+ border-color: #fecdca;
220
+ }
221
+
222
+ .server-status-blockers {
223
+ margin-top: 0.75rem;
224
+ display: grid;
225
+ gap: 0.55rem;
226
+ }
227
+
228
+ .server-status-blocker {
229
+ padding: 0.7rem 0.8rem;
230
+ border-radius: 8px;
231
+ border: 1px solid #fecdca;
232
+ background: #fff8f7;
233
+ }
234
+
235
+ .server-status-blocker h3 {
236
+ margin: 0 0 0.3rem;
237
+ font-size: 0.85rem;
238
+ color: #b42318;
239
+ }
240
+
241
+ .server-status-blocker p {
242
+ margin: 0;
243
+ font-size: 0.78rem;
244
+ color: #444;
245
+ line-height: 1.45;
246
+ }
247
+
248
+ .server-status-blocker p + p {
249
+ margin-top: 0.35rem;
250
+ }
251
+
252
+ .server-status-blocker-fix {
253
+ color: #0a0908;
254
+ }
255
+
256
+ .server-status-card--ok {
257
+ border-color: #abefc6;
258
+ }
259
+
260
+ .server-status-card--degraded {
261
+ border-color: #fedf89;
262
+ }
263
+
264
+ .server-status-card--blocked {
265
+ border-color: #fecdca;
266
+ }
267
+
121
268
  .devices-grid {
122
269
  display: grid;
123
270
  grid-template-columns: repeat(auto-fill, minmax(308px, 1fr));