@tomorrowos/sdk 0.2.4 → 0.2.5
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/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/pairing-code.d.ts +8 -0
- package/dist/pairing-code.d.ts.map +1 -0
- package/dist/pairing-code.js +23 -0
- package/dist/store/memory-store.d.ts +9 -1
- package/dist/store/memory-store.d.ts.map +1 -1
- package/dist/store/memory-store.js +22 -0
- package/dist/store/types.d.ts +14 -0
- package/dist/store/types.d.ts.map +1 -1
- package/dist/tomorrowos.d.ts +5 -1
- package/dist/tomorrowos.d.ts.map +1 -1
- package/dist/tomorrowos.js +101 -38
- package/package.json +1 -1
- package/templates/cms-starter/policy.example.json +30 -30
- package/templates/cms-starter/public/index.html +2 -2
- package/templates/cms-starter/public/methods.js +10 -2
- package/templates/cms-starter/public/panel.css +329 -329
- package/templates/cms-starter/public/uploads/.gitkeep +1 -0
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
export { TomorrowOS } from "./tomorrowos.js";
|
|
2
2
|
export type { DeviceListItem, ListenOptions, TomorrowOSBrand, TomorrowOSOptions } from "./tomorrowos.js";
|
|
3
|
-
export type { PairedDeviceRecord, PendingCodeRecord, TomorrowOSStore } from "./store/types.js";
|
|
3
|
+
export type { DeviceRegistryRecord, PairedDeviceRecord, PendingCodeRecord, TomorrowOSStore } from "./store/types.js";
|
|
4
|
+
export { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode, PAIRING_CODE_ALPHABET, PAIRING_CODE_LENGTH } from "./pairing-code.js";
|
|
4
5
|
export { MemoryStore } from "./store/memory-store.js";
|
|
5
6
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC7C,YAAY,EACV,cAAc,EACd,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,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,aAAa,EACb,eAAe,EACf,iBAAiB,EAClB,MAAM,iBAAiB,CAAC;AAEzB,YAAY,EACV,oBAAoB,EACpB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,kBAAkB,CAAC;AAC1B,OAAO,EACL,yBAAyB,EACzB,wBAAwB,EACxB,oBAAoB,EACpB,qBAAqB,EACrB,mBAAmB,EACpB,MAAM,mBAAmB,CAAC;AAC3B,OAAO,EAAE,WAAW,EAAE,MAAM,yBAAyB,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/** 0-9 and A-Z — 36 characters per digit position. */
|
|
2
|
+
export declare const PAIRING_CODE_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
3
|
+
export declare const PAIRING_CODE_LENGTH = 6;
|
|
4
|
+
export declare function normalizePairingCode(raw: string): string;
|
|
5
|
+
export declare function isValidPairingCodeFormat(code: string): boolean;
|
|
6
|
+
/** Random 6-character code; each position is digit or uppercase letter. */
|
|
7
|
+
export declare function generateRandomPairingCode(): string;
|
|
8
|
+
//# sourceMappingURL=pairing-code.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pairing-code.d.ts","sourceRoot":"","sources":["../src/pairing-code.ts"],"names":[],"mappings":"AAEA,sDAAsD;AACtD,eAAO,MAAM,qBAAqB,yCAAyC,CAAC;AAE5E,eAAO,MAAM,mBAAmB,IAAI,CAAC;AAErC,wBAAgB,oBAAoB,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAKxD;AAED,wBAAgB,wBAAwB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAK9D;AAED,2EAA2E;AAC3E,wBAAgB,yBAAyB,IAAI,MAAM,CAOlD"}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { randomBytes } from "crypto";
|
|
2
|
+
/** 0-9 and A-Z — 36 characters per digit position. */
|
|
3
|
+
export const PAIRING_CODE_ALPHABET = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
|
4
|
+
export const PAIRING_CODE_LENGTH = 6;
|
|
5
|
+
export function normalizePairingCode(raw) {
|
|
6
|
+
return String(raw || "")
|
|
7
|
+
.trim()
|
|
8
|
+
.toUpperCase()
|
|
9
|
+
.replace(/[^0-9A-Z]/g, "");
|
|
10
|
+
}
|
|
11
|
+
export function isValidPairingCodeFormat(code) {
|
|
12
|
+
return (code.length === PAIRING_CODE_LENGTH &&
|
|
13
|
+
[...code].every((ch) => PAIRING_CODE_ALPHABET.includes(ch)));
|
|
14
|
+
}
|
|
15
|
+
/** Random 6-character code; each position is digit or uppercase letter. */
|
|
16
|
+
export function generateRandomPairingCode() {
|
|
17
|
+
const bytes = randomBytes(PAIRING_CODE_LENGTH);
|
|
18
|
+
let out = "";
|
|
19
|
+
for (let i = 0; i < PAIRING_CODE_LENGTH; i += 1) {
|
|
20
|
+
out += PAIRING_CODE_ALPHABET[bytes[i] % PAIRING_CODE_ALPHABET.length];
|
|
21
|
+
}
|
|
22
|
+
return out;
|
|
23
|
+
}
|
|
@@ -1,14 +1,22 @@
|
|
|
1
|
-
import type { PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, TomorrowOSStore } from "./types.js";
|
|
1
|
+
import type { DeviceRegistryRecord, PairedDeviceEntry, PairedDeviceRecord, PendingCodeRecord, TomorrowOSStore } from "./types.js";
|
|
2
2
|
/**
|
|
3
3
|
* Default store for development / single-node demos.
|
|
4
4
|
* Data is lost on process restart — not for multi-instance production.
|
|
5
5
|
*/
|
|
6
6
|
export declare class MemoryStore implements TomorrowOSStore {
|
|
7
7
|
private readonly pendingCodes;
|
|
8
|
+
private readonly deviceRegistry;
|
|
9
|
+
private readonly codeToDeviceId;
|
|
8
10
|
private readonly pairedDevices;
|
|
9
11
|
setPendingCode(code: string, record: PendingCodeRecord): Promise<void>;
|
|
10
12
|
getPendingCode(code: string): Promise<PendingCodeRecord | undefined>;
|
|
11
13
|
deletePendingCode(code: string): Promise<void>;
|
|
14
|
+
getDeviceRegistry(deviceId: string): Promise<DeviceRegistryRecord | undefined>;
|
|
15
|
+
setDeviceRegistry(deviceId: string, record: DeviceRegistryRecord): Promise<void>;
|
|
16
|
+
getDeviceRegistryByCode(code: string): Promise<{
|
|
17
|
+
deviceId: string;
|
|
18
|
+
record: DeviceRegistryRecord;
|
|
19
|
+
} | undefined>;
|
|
12
20
|
setPairedDevice(deviceId: string, record: PairedDeviceRecord): Promise<void>;
|
|
13
21
|
getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
|
|
14
22
|
deletePairedDevice(deviceId: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IAEjE,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAMxD"}
|
|
1
|
+
{"version":3,"file":"memory-store.d.ts","sourceRoot":"","sources":["../../src/store/memory-store.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EACV,oBAAoB,EACpB,iBAAiB,EACjB,kBAAkB,EAClB,iBAAiB,EACjB,eAAe,EAChB,MAAM,YAAY,CAAC;AAEpB;;;GAGG;AACH,qBAAa,WAAY,YAAW,eAAe;IACjD,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAwC;IACrE,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA2C;IAC1E,OAAO,CAAC,QAAQ,CAAC,cAAc,CAA6B;IAC5D,OAAO,CAAC,QAAQ,CAAC,aAAa,CAAyC;IAEjE,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC;IAItE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC;IAIpE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAI9C,iBAAiB,CACrB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC;IAItC,iBAAiB,CACrB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC;IASV,uBAAuB,CAC3B,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC;IAQpE,eAAe,CACnB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,kBAAkB,GACzB,OAAO,CAAC,IAAI,CAAC;IAIV,eAAe,CACnB,QAAQ,EAAE,MAAM,GACf,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC;IAIpC,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAInD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC;CAMxD"}
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
*/
|
|
5
5
|
export class MemoryStore {
|
|
6
6
|
pendingCodes = new Map();
|
|
7
|
+
deviceRegistry = new Map();
|
|
8
|
+
codeToDeviceId = new Map();
|
|
7
9
|
pairedDevices = new Map();
|
|
8
10
|
async setPendingCode(code, record) {
|
|
9
11
|
this.pendingCodes.set(code, record);
|
|
@@ -14,6 +16,26 @@ export class MemoryStore {
|
|
|
14
16
|
async deletePendingCode(code) {
|
|
15
17
|
this.pendingCodes.delete(code);
|
|
16
18
|
}
|
|
19
|
+
async getDeviceRegistry(deviceId) {
|
|
20
|
+
return this.deviceRegistry.get(deviceId);
|
|
21
|
+
}
|
|
22
|
+
async setDeviceRegistry(deviceId, record) {
|
|
23
|
+
const existing = this.deviceRegistry.get(deviceId);
|
|
24
|
+
if (existing?.permanentPairingCode) {
|
|
25
|
+
this.codeToDeviceId.delete(existing.permanentPairingCode);
|
|
26
|
+
}
|
|
27
|
+
this.deviceRegistry.set(deviceId, record);
|
|
28
|
+
this.codeToDeviceId.set(record.permanentPairingCode, deviceId);
|
|
29
|
+
}
|
|
30
|
+
async getDeviceRegistryByCode(code) {
|
|
31
|
+
const deviceId = this.codeToDeviceId.get(code);
|
|
32
|
+
if (!deviceId)
|
|
33
|
+
return undefined;
|
|
34
|
+
const record = this.deviceRegistry.get(deviceId);
|
|
35
|
+
if (!record)
|
|
36
|
+
return undefined;
|
|
37
|
+
return { deviceId, record };
|
|
38
|
+
}
|
|
17
39
|
async setPairedDevice(deviceId, record) {
|
|
18
40
|
this.pairedDevices.set(deviceId, record);
|
|
19
41
|
}
|
package/dist/store/types.d.ts
CHANGED
|
@@ -6,6 +6,14 @@ export interface PendingCodeRecord {
|
|
|
6
6
|
deviceId: string;
|
|
7
7
|
createdAt: number;
|
|
8
8
|
}
|
|
9
|
+
/** Permanent pairing code bound to a device (serial) on first hello — never rotated. */
|
|
10
|
+
export interface DeviceRegistryRecord {
|
|
11
|
+
permanentPairingCode: string;
|
|
12
|
+
codeCreatedAt: number;
|
|
13
|
+
serialNumber?: string;
|
|
14
|
+
firstSeenAt?: number;
|
|
15
|
+
lastHelloAt?: number;
|
|
16
|
+
}
|
|
9
17
|
export interface PairedDeviceRecord {
|
|
10
18
|
pairingToken: string;
|
|
11
19
|
pairedAt: string;
|
|
@@ -29,6 +37,12 @@ export interface TomorrowOSStore {
|
|
|
29
37
|
setPendingCode(code: string, record: PendingCodeRecord): Promise<void>;
|
|
30
38
|
getPendingCode(code: string): Promise<PendingCodeRecord | undefined>;
|
|
31
39
|
deletePendingCode(code: string): Promise<void>;
|
|
40
|
+
getDeviceRegistry(deviceId: string): Promise<DeviceRegistryRecord | undefined>;
|
|
41
|
+
setDeviceRegistry(deviceId: string, record: DeviceRegistryRecord): Promise<void>;
|
|
42
|
+
getDeviceRegistryByCode(code: string): Promise<{
|
|
43
|
+
deviceId: string;
|
|
44
|
+
record: DeviceRegistryRecord;
|
|
45
|
+
} | undefined>;
|
|
32
46
|
setPairedDevice(deviceId: string, record: PairedDeviceRecord): Promise<void>;
|
|
33
47
|
getPairedDevice(deviceId: string): Promise<PairedDeviceRecord | undefined>;
|
|
34
48
|
deletePairedDevice(deviceId: string): Promise<void>;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACrE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC3E,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;CACnD"}
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/store/types.ts"],"names":[],"mappings":"AAAA;;;GAGG;AAEH,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,SAAS,EAAE,MAAM,CAAC;CACnB;AAED,wFAAwF;AACxF,MAAM,WAAW,oBAAoB;IACnC,oBAAoB,EAAE,MAAM,CAAC;IAC7B,aAAa,EAAE,MAAM,CAAC;IACtB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,MAAM,WAAW,kBAAkB;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,UAAU,CAAC,EAAE,MAAM,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,gBAAgB,CAAC,EAAE,MAAM,CAAC;CAC3B;AAED,MAAM,WAAW,iBAAiB;IAChC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,kBAAkB,CAAC;CAC5B;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B,cAAc,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,EAAE,iBAAiB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACvE,cAAc,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,GAAG,SAAS,CAAC,CAAC;IACrE,iBAAiB,CAAC,IAAI,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC/C,iBAAiB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,oBAAoB,GAAG,SAAS,CAAC,CAAC;IAC/E,iBAAiB,CACf,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,oBAAoB,GAC3B,OAAO,CAAC,IAAI,CAAC,CAAC;IACjB,uBAAuB,CACrB,IAAI,EAAE,MAAM,GACX,OAAO,CAAC;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,oBAAoB,CAAA;KAAE,GAAG,SAAS,CAAC,CAAC;IAC3E,eAAe,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,kBAAkB,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC7E,eAAe,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,GAAG,SAAS,CAAC,CAAC;IAC3E,kBAAkB,CAAC,QAAQ,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACpD,iBAAiB,IAAI,OAAO,CAAC,iBAAiB,EAAE,CAAC,CAAC;CACnD"}
|
package/dist/tomorrowos.d.ts
CHANGED
|
@@ -29,10 +29,13 @@ export interface ListenOptions {
|
|
|
29
29
|
}
|
|
30
30
|
export interface DeviceListItem {
|
|
31
31
|
deviceId: string;
|
|
32
|
+
/** Permanent 6-character alphanumeric pairing code (same after unpair). */
|
|
33
|
+
pairingCode: string | null;
|
|
32
34
|
connected: boolean;
|
|
33
35
|
deviceName: string | null;
|
|
34
36
|
platform: string | null;
|
|
35
37
|
system: string | null;
|
|
38
|
+
serialNumber: string | null;
|
|
36
39
|
pairedAt: string;
|
|
37
40
|
lastBootAt: string | null;
|
|
38
41
|
lastOnlineAt: string | null;
|
|
@@ -53,7 +56,7 @@ export declare class TomorrowOS extends EventEmitter {
|
|
|
53
56
|
private staticRoot;
|
|
54
57
|
private staticIndexFile;
|
|
55
58
|
constructor(options: TomorrowOSOptions);
|
|
56
|
-
/** Verify a 6-
|
|
59
|
+
/** Verify a 6-character alphanumeric pairing code (POST /pairing/verify). */
|
|
57
60
|
pairingVerify(code: string): Promise<{
|
|
58
61
|
deviceId: string;
|
|
59
62
|
}>;
|
|
@@ -75,6 +78,7 @@ export declare class TomorrowOS extends EventEmitter {
|
|
|
75
78
|
listDevices(): Promise<DeviceListItem[]>;
|
|
76
79
|
private isDeviceConnected;
|
|
77
80
|
private captureHelloMeta;
|
|
81
|
+
private getOrCreatePermanentPairingCode;
|
|
78
82
|
private mergePairedRecord;
|
|
79
83
|
private touchPairedOnline;
|
|
80
84
|
private touchPairedOffline;
|
package/dist/tomorrowos.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,YAAY,EAAE,MAAM,QAAQ,CAAC;AAEtC,OAAO,IAAI,MAAM,MAAM,CAAC;
|
|
1
|
+
{"version":3,"file":"tomorrowos.d.ts","sourceRoot":"","sources":["../src/tomorrowos.ts"],"names":[],"mappings":"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"}
|
package/dist/tomorrowos.js
CHANGED
|
@@ -4,6 +4,7 @@ import fs from "fs/promises";
|
|
|
4
4
|
import http from "http";
|
|
5
5
|
import path from "path";
|
|
6
6
|
import { WebSocket, WebSocketServer } from "ws";
|
|
7
|
+
import { generateRandomPairingCode, isValidPairingCodeFormat, normalizePairingCode } from "./pairing-code.js";
|
|
7
8
|
import { MemoryStore } from "./store/memory-store.js";
|
|
8
9
|
function formatDurationMs(ms) {
|
|
9
10
|
if (!Number.isFinite(ms) || ms < 0)
|
|
@@ -23,8 +24,16 @@ function formatDurationMs(ms) {
|
|
|
23
24
|
parts.push(`${seconds}s`);
|
|
24
25
|
return parts.join(" ");
|
|
25
26
|
}
|
|
26
|
-
function
|
|
27
|
-
|
|
27
|
+
function resolveHelloDeviceId(msg) {
|
|
28
|
+
const serial = typeof msg.serialNumber === "string" && msg.serialNumber.trim()
|
|
29
|
+
? msg.serialNumber.trim()
|
|
30
|
+
: null;
|
|
31
|
+
if (serial)
|
|
32
|
+
return serial;
|
|
33
|
+
const deviceId = typeof msg.deviceId === "string" && msg.deviceId.trim()
|
|
34
|
+
? msg.deviceId.trim()
|
|
35
|
+
: null;
|
|
36
|
+
return deviceId;
|
|
28
37
|
}
|
|
29
38
|
const MAX_MEDIA_UPLOAD_BYTES = 100 * 1024 * 1024;
|
|
30
39
|
async function readJsonBody(req) {
|
|
@@ -82,41 +91,55 @@ export class TomorrowOS extends EventEmitter {
|
|
|
82
91
|
this.brand = options.brand;
|
|
83
92
|
this.store = options.store ?? new MemoryStore();
|
|
84
93
|
}
|
|
85
|
-
/** Verify a 6-
|
|
94
|
+
/** Verify a 6-character alphanumeric pairing code (POST /pairing/verify). */
|
|
86
95
|
async pairingVerify(code) {
|
|
87
|
-
const
|
|
88
|
-
if (!
|
|
89
|
-
const err = new Error("Invalid
|
|
96
|
+
const normalized = normalizePairingCode(code);
|
|
97
|
+
if (!isValidPairingCodeFormat(normalized)) {
|
|
98
|
+
const err = new Error("Invalid pairing code format");
|
|
99
|
+
err.code = "PAIRING_INVALID";
|
|
100
|
+
throw err;
|
|
101
|
+
}
|
|
102
|
+
let deviceId;
|
|
103
|
+
const registry = await this.store.getDeviceRegistryByCode(normalized);
|
|
104
|
+
if (registry) {
|
|
105
|
+
deviceId = registry.deviceId;
|
|
106
|
+
}
|
|
107
|
+
else {
|
|
108
|
+
const pending = await this.store.getPendingCode(normalized);
|
|
109
|
+
deviceId = pending?.deviceId;
|
|
110
|
+
}
|
|
111
|
+
if (!deviceId) {
|
|
112
|
+
const err = new Error("Invalid or unknown pairing code");
|
|
90
113
|
err.code = "PAIRING_INVALID";
|
|
91
114
|
throw err;
|
|
92
115
|
}
|
|
93
116
|
const pairingToken = randomBytes(32).toString("hex");
|
|
94
117
|
const pairedAt = new Date().toISOString();
|
|
95
|
-
const meta = this.pendingDeviceMeta.get(
|
|
118
|
+
const meta = this.pendingDeviceMeta.get(deviceId);
|
|
96
119
|
const now = pairedAt;
|
|
97
|
-
await this.store.setPairedDevice(
|
|
120
|
+
await this.store.setPairedDevice(deviceId, {
|
|
98
121
|
pairingToken,
|
|
99
122
|
pairedAt,
|
|
100
123
|
deviceName: meta?.deviceName,
|
|
101
124
|
platform: meta?.platform,
|
|
102
125
|
system: meta?.system,
|
|
103
126
|
lastBootAt: meta?.bootedAt ?? now,
|
|
104
|
-
lastOnlineAt: this.isDeviceConnected(
|
|
105
|
-
lastOfflineAt: this.isDeviceConnected(
|
|
127
|
+
lastOnlineAt: this.isDeviceConnected(deviceId) ? now : undefined,
|
|
128
|
+
lastOfflineAt: this.isDeviceConnected(deviceId) ? undefined : now
|
|
106
129
|
});
|
|
107
|
-
await this.store.deletePendingCode(
|
|
108
|
-
const ws = this.devices.get(
|
|
130
|
+
await this.store.deletePendingCode(normalized);
|
|
131
|
+
const ws = this.devices.get(deviceId);
|
|
109
132
|
if (ws && ws.readyState === WebSocket.OPEN) {
|
|
110
133
|
ws.send(JSON.stringify({
|
|
111
134
|
type: "pairing.verified",
|
|
112
135
|
method: "tomorrowos.pairing.verify",
|
|
113
|
-
deviceId
|
|
136
|
+
deviceId,
|
|
114
137
|
pairingToken
|
|
115
138
|
}));
|
|
116
|
-
void this.refreshPairedDeviceInfo(
|
|
139
|
+
void this.refreshPairedDeviceInfo(deviceId);
|
|
117
140
|
}
|
|
118
|
-
this.emit("device.paired", { deviceId
|
|
119
|
-
return { deviceId
|
|
141
|
+
this.emit("device.paired", { deviceId });
|
|
142
|
+
return { deviceId };
|
|
120
143
|
}
|
|
121
144
|
/** Remove pairing for a device and notify it over WebSocket if connected. */
|
|
122
145
|
async pairingUnpair(deviceId) {
|
|
@@ -149,8 +172,9 @@ export class TomorrowOS extends EventEmitter {
|
|
|
149
172
|
async listDevices() {
|
|
150
173
|
const entries = await this.store.listPairedDevices();
|
|
151
174
|
const now = Date.now();
|
|
152
|
-
return entries.map(({ deviceId, record }) => {
|
|
175
|
+
return Promise.all(entries.map(async ({ deviceId, record }) => {
|
|
153
176
|
const connected = this.isDeviceConnected(deviceId);
|
|
177
|
+
const reg = await this.store.getDeviceRegistry(deviceId);
|
|
154
178
|
let screenOnlineActive = false;
|
|
155
179
|
let screenOnlineLabel = "Not active";
|
|
156
180
|
if (connected && record.lastBootAt) {
|
|
@@ -163,10 +187,12 @@ export class TomorrowOS extends EventEmitter {
|
|
|
163
187
|
const screenOnlineSince = connected && record.lastBootAt ? record.lastBootAt : null;
|
|
164
188
|
return {
|
|
165
189
|
deviceId,
|
|
190
|
+
pairingCode: reg?.permanentPairingCode ?? null,
|
|
166
191
|
connected,
|
|
167
192
|
deviceName: record.deviceName ?? null,
|
|
168
193
|
platform: record.platform ?? null,
|
|
169
194
|
system: record.system ?? null,
|
|
195
|
+
serialNumber: reg?.serialNumber ?? deviceId,
|
|
170
196
|
pairedAt: record.pairedAt,
|
|
171
197
|
lastBootAt: record.lastBootAt ?? null,
|
|
172
198
|
lastOnlineAt: record.lastOnlineAt ?? null,
|
|
@@ -176,7 +202,7 @@ export class TomorrowOS extends EventEmitter {
|
|
|
176
202
|
screenOnlineLabel,
|
|
177
203
|
screenOnlineSince
|
|
178
204
|
};
|
|
179
|
-
});
|
|
205
|
+
}));
|
|
180
206
|
}
|
|
181
207
|
isDeviceConnected(deviceId) {
|
|
182
208
|
const ws = this.devices.get(deviceId);
|
|
@@ -188,9 +214,37 @@ export class TomorrowOS extends EventEmitter {
|
|
|
188
214
|
deviceName: typeof msg.deviceName === "string" ? msg.deviceName : undefined,
|
|
189
215
|
system: typeof msg.system === "string" ? msg.system : undefined,
|
|
190
216
|
bootedAt: typeof msg.bootedAt === "string" ? msg.bootedAt : undefined,
|
|
191
|
-
playerVersion: typeof msg.playerVersion === "string" ? msg.playerVersion : undefined
|
|
217
|
+
playerVersion: typeof msg.playerVersion === "string" ? msg.playerVersion : undefined,
|
|
218
|
+
serialNumber: typeof msg.serialNumber === "string" ? msg.serialNumber : deviceId
|
|
192
219
|
});
|
|
193
220
|
}
|
|
221
|
+
async getOrCreatePermanentPairingCode(deviceId, serialNumber) {
|
|
222
|
+
const existing = await this.store.getDeviceRegistry(deviceId);
|
|
223
|
+
if (existing?.permanentPairingCode) {
|
|
224
|
+
await this.store.setDeviceRegistry(deviceId, {
|
|
225
|
+
...existing,
|
|
226
|
+
serialNumber: serialNumber ?? existing.serialNumber ?? deviceId,
|
|
227
|
+
lastHelloAt: Date.now()
|
|
228
|
+
});
|
|
229
|
+
return existing.permanentPairingCode;
|
|
230
|
+
}
|
|
231
|
+
for (let attempt = 0; attempt < 32; attempt += 1) {
|
|
232
|
+
const code = generateRandomPairingCode();
|
|
233
|
+
const collision = await this.store.getDeviceRegistryByCode(code);
|
|
234
|
+
if (collision && collision.deviceId !== deviceId)
|
|
235
|
+
continue;
|
|
236
|
+
const now = Date.now();
|
|
237
|
+
await this.store.setDeviceRegistry(deviceId, {
|
|
238
|
+
permanentPairingCode: code,
|
|
239
|
+
codeCreatedAt: now,
|
|
240
|
+
serialNumber: serialNumber ?? deviceId,
|
|
241
|
+
firstSeenAt: now,
|
|
242
|
+
lastHelloAt: now
|
|
243
|
+
});
|
|
244
|
+
return code;
|
|
245
|
+
}
|
|
246
|
+
throw new Error("Failed to allocate unique pairing code");
|
|
247
|
+
}
|
|
194
248
|
async mergePairedRecord(deviceId, patch) {
|
|
195
249
|
const existing = await this.store.getPairedDevice(deviceId);
|
|
196
250
|
if (!existing)
|
|
@@ -593,25 +647,34 @@ export class TomorrowOS extends EventEmitter {
|
|
|
593
647
|
}
|
|
594
648
|
const type = msg.type;
|
|
595
649
|
if (type === "device.hello") {
|
|
596
|
-
const deviceId =
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
650
|
+
const deviceId = resolveHelloDeviceId(msg) ?? randomUUID();
|
|
651
|
+
const serialNumber = typeof msg.serialNumber === "string" && msg.serialNumber.trim()
|
|
652
|
+
? msg.serialNumber.trim()
|
|
653
|
+
: deviceId;
|
|
654
|
+
void (async () => {
|
|
655
|
+
try {
|
|
656
|
+
const code = await this.getOrCreatePermanentPairingCode(deviceId, serialNumber);
|
|
657
|
+
this.captureHelloMeta(deviceId, msg);
|
|
658
|
+
this.devices.set(deviceId, ws);
|
|
659
|
+
void this.store.setPendingCode(code, {
|
|
660
|
+
deviceId,
|
|
661
|
+
createdAt: Date.now()
|
|
662
|
+
});
|
|
663
|
+
ws.deviceId = deviceId;
|
|
664
|
+
ws.send(JSON.stringify({
|
|
665
|
+
type: "pairing.code",
|
|
666
|
+
method: "tomorrowos.pairing.createCode",
|
|
667
|
+
code,
|
|
668
|
+
deviceId,
|
|
669
|
+
serialNumber
|
|
670
|
+
}));
|
|
671
|
+
this.sendBrandSnapshot(ws);
|
|
672
|
+
this.emit("device.online", { deviceId });
|
|
673
|
+
}
|
|
674
|
+
catch (err) {
|
|
675
|
+
console.error("[TomorrowOS] device.hello failed:", err);
|
|
676
|
+
}
|
|
677
|
+
})();
|
|
615
678
|
return;
|
|
616
679
|
}
|
|
617
680
|
if (type === "device.resume") {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tomorrowos/sdk",
|
|
3
|
-
"version": "0.2.
|
|
3
|
+
"version": "0.2.5",
|
|
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,30 +1,30 @@
|
|
|
1
|
-
{
|
|
2
|
-
"policy": {
|
|
3
|
-
"playlists": [
|
|
4
|
-
{
|
|
5
|
-
"id": "weekday-promo",
|
|
6
|
-
"name": "Weekday promo",
|
|
7
|
-
"schedule": {
|
|
8
|
-
"startDate": "2026-05-01",
|
|
9
|
-
"endDate": "2026-05-31",
|
|
10
|
-
"daysOfWeek": [1, 2, 3, 4, 5],
|
|
11
|
-
"start": "09:00",
|
|
12
|
-
"end": "17:00"
|
|
13
|
-
},
|
|
14
|
-
"items": [
|
|
15
|
-
{
|
|
16
|
-
"url": "https://example.com/hero.jpg",
|
|
17
|
-
"type": "image",
|
|
18
|
-
"durationMs": 10000
|
|
19
|
-
},
|
|
20
|
-
{
|
|
21
|
-
"url": "https://example.com/spot.mp4",
|
|
22
|
-
"type": "video",
|
|
23
|
-
"durationMs": 30000
|
|
24
|
-
}
|
|
25
|
-
]
|
|
26
|
-
}
|
|
27
|
-
],
|
|
28
|
-
"fallback": { "type": "brand" }
|
|
29
|
-
}
|
|
30
|
-
}
|
|
1
|
+
{
|
|
2
|
+
"policy": {
|
|
3
|
+
"playlists": [
|
|
4
|
+
{
|
|
5
|
+
"id": "weekday-promo",
|
|
6
|
+
"name": "Weekday promo",
|
|
7
|
+
"schedule": {
|
|
8
|
+
"startDate": "2026-05-01",
|
|
9
|
+
"endDate": "2026-05-31",
|
|
10
|
+
"daysOfWeek": [1, 2, 3, 4, 5],
|
|
11
|
+
"start": "09:00",
|
|
12
|
+
"end": "17:00"
|
|
13
|
+
},
|
|
14
|
+
"items": [
|
|
15
|
+
{
|
|
16
|
+
"url": "https://example.com/hero.jpg",
|
|
17
|
+
"type": "image",
|
|
18
|
+
"durationMs": 10000
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
"url": "https://example.com/spot.mp4",
|
|
22
|
+
"type": "video",
|
|
23
|
+
"durationMs": 30000
|
|
24
|
+
}
|
|
25
|
+
]
|
|
26
|
+
}
|
|
27
|
+
],
|
|
28
|
+
"fallback": { "type": "brand" }
|
|
29
|
+
}
|
|
30
|
+
}
|
|
@@ -16,9 +16,9 @@
|
|
|
16
16
|
<main class="panel-main">
|
|
17
17
|
<section class="card">
|
|
18
18
|
<h2>Pair device</h2>
|
|
19
|
-
<p class="hint">Enter the 6-
|
|
19
|
+
<p class="hint">Enter the 6-character code on the screen (A–Z or 0–9). Each device keeps the same code after unpair.</p>
|
|
20
20
|
<div class="row">
|
|
21
|
-
<input id="code" maxlength="6" placeholder="
|
|
21
|
+
<input id="code" maxlength="6" placeholder="e.g. A3K9Z1" autocapitalize="characters" />
|
|
22
22
|
<button type="button" onclick="verify()">Verify</button>
|
|
23
23
|
</div>
|
|
24
24
|
</section>
|
|
@@ -486,7 +486,15 @@ function buildPolicyPayload() {
|
|
|
486
486
|
}
|
|
487
487
|
|
|
488
488
|
async function verify() {
|
|
489
|
-
const code = document.getElementById("code").value
|
|
489
|
+
const code = String(document.getElementById("code").value || "")
|
|
490
|
+
.trim()
|
|
491
|
+
.toUpperCase()
|
|
492
|
+
.replace(/[^0-9A-Z]/g, "");
|
|
493
|
+
|
|
494
|
+
if (code.length !== 6) {
|
|
495
|
+
showResult({ status: "failed", error: "Enter the 6-character code from the screen." });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
490
498
|
|
|
491
499
|
const res = await fetch("/pairing/verify", {
|
|
492
500
|
method: "POST",
|
|
@@ -507,7 +515,7 @@ async function verify() {
|
|
|
507
515
|
async function unpairDevice(deviceId) {
|
|
508
516
|
if (
|
|
509
517
|
!confirm(
|
|
510
|
-
"Unpair this device? Its card will be removed
|
|
518
|
+
"Unpair this device? Its card will be removed; the screen keeps the same permanent pairing code."
|
|
511
519
|
)
|
|
512
520
|
) {
|
|
513
521
|
return;
|
|
@@ -1,329 +1,329 @@
|
|
|
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 {
|
|
43
|
-
width: 320px;
|
|
44
|
-
flex-shrink: 0;
|
|
45
|
-
background: #fff;
|
|
46
|
-
border-left: 1px solid #e4e1dc;
|
|
47
|
-
display: flex;
|
|
48
|
-
flex-direction: column;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
.card {
|
|
52
|
-
background: #fff;
|
|
53
|
-
border: 1px solid #e4e1dc;
|
|
54
|
-
border-radius: 10px;
|
|
55
|
-
padding: 1rem 1.1rem;
|
|
56
|
-
margin-bottom: 1rem;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
.card h2 {
|
|
60
|
-
margin: 0 0 0.75rem;
|
|
61
|
-
font-size: 0.95rem;
|
|
62
|
-
font-weight: 600;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
.hint {
|
|
66
|
-
margin: 0 0 0.75rem;
|
|
67
|
-
font-size: 0.8rem;
|
|
68
|
-
color: #666;
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
.devices-grid {
|
|
72
|
-
display: grid;
|
|
73
|
-
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
74
|
-
gap: 0.85rem;
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
.devices-empty {
|
|
78
|
-
margin: 0;
|
|
79
|
-
color: #666;
|
|
80
|
-
font-size: 0.875rem;
|
|
81
|
-
grid-column: 1 / -1;
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
.device-card {
|
|
85
|
-
border: 1px solid #e4e1dc;
|
|
86
|
-
border-radius: 8px;
|
|
87
|
-
padding: 0.85rem 0.95rem;
|
|
88
|
-
background: #fafaf9;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
.device-card-header {
|
|
92
|
-
display: flex;
|
|
93
|
-
align-items: center;
|
|
94
|
-
gap: 0.5rem;
|
|
95
|
-
margin-bottom: 0.65rem;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
.device-card-title {
|
|
99
|
-
margin: 0;
|
|
100
|
-
font-size: 0.95rem;
|
|
101
|
-
font-weight: 600;
|
|
102
|
-
line-height: 1.2;
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
.status-led {
|
|
106
|
-
width: 11px;
|
|
107
|
-
height: 11px;
|
|
108
|
-
border-radius: 50%;
|
|
109
|
-
flex-shrink: 0;
|
|
110
|
-
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06);
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
.status-led--online {
|
|
114
|
-
background: #12b76a;
|
|
115
|
-
box-shadow: 0 0 8px rgba(18, 183, 106, 0.55);
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
.status-led--offline {
|
|
119
|
-
background: #f04438;
|
|
120
|
-
box-shadow: 0 0 8px rgba(240, 68, 56, 0.45);
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
.device-meta {
|
|
124
|
-
display: grid;
|
|
125
|
-
gap: 0.35rem;
|
|
126
|
-
font-size: 0.78rem;
|
|
127
|
-
color: #444;
|
|
128
|
-
margin-bottom: 0.75rem;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
.device-meta dt {
|
|
132
|
-
font-weight: 600;
|
|
133
|
-
color: #666;
|
|
134
|
-
display: inline;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
.device-meta dt::after {
|
|
138
|
-
content: ": ";
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
.device-meta dd {
|
|
142
|
-
display: inline;
|
|
143
|
-
margin: 0;
|
|
144
|
-
word-break: break-all;
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
.device-meta-row {
|
|
148
|
-
display: block;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
.device-card-actions {
|
|
152
|
-
display: flex;
|
|
153
|
-
flex-wrap: wrap;
|
|
154
|
-
gap: 0.4rem;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
.device-card-actions button {
|
|
158
|
-
font-size: 0.78rem;
|
|
159
|
-
padding: 0.35rem 0.6rem;
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
.row {
|
|
163
|
-
display: flex;
|
|
164
|
-
flex-wrap: wrap;
|
|
165
|
-
gap: 0.5rem;
|
|
166
|
-
align-items: center;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
input[type="text"],
|
|
170
|
-
input[type="date"],
|
|
171
|
-
input[type="time"],
|
|
172
|
-
select {
|
|
173
|
-
font: inherit;
|
|
174
|
-
padding: 0.45rem 0.6rem;
|
|
175
|
-
border: 1px solid #d6d2cb;
|
|
176
|
-
border-radius: 6px;
|
|
177
|
-
background: #fff;
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
button {
|
|
181
|
-
font: inherit;
|
|
182
|
-
padding: 0.45rem 0.85rem;
|
|
183
|
-
border-radius: 6px;
|
|
184
|
-
border: 1px solid #d6d2cb;
|
|
185
|
-
background: #fff;
|
|
186
|
-
cursor: pointer;
|
|
187
|
-
}
|
|
188
|
-
|
|
189
|
-
button:hover {
|
|
190
|
-
background: #f5f3ef;
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
button.primary {
|
|
194
|
-
background: #ff8a3d;
|
|
195
|
-
border-color: #e67320;
|
|
196
|
-
color: #0a0908;
|
|
197
|
-
font-weight: 600;
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
button.primary:hover {
|
|
201
|
-
background: #ff9a57;
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
button.danger {
|
|
205
|
-
color: #b42318;
|
|
206
|
-
border-color: #fecdca;
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
.schedule-grid {
|
|
210
|
-
display: grid;
|
|
211
|
-
grid-template-columns: 1fr 1fr;
|
|
212
|
-
gap: 0.65rem 1rem;
|
|
213
|
-
}
|
|
214
|
-
|
|
215
|
-
.schedule-grid label {
|
|
216
|
-
display: flex;
|
|
217
|
-
flex-direction: column;
|
|
218
|
-
gap: 0.25rem;
|
|
219
|
-
font-size: 0.8rem;
|
|
220
|
-
color: #444;
|
|
221
|
-
}
|
|
222
|
-
|
|
223
|
-
.days-row {
|
|
224
|
-
grid-column: 1 / -1;
|
|
225
|
-
display: flex;
|
|
226
|
-
flex-wrap: wrap;
|
|
227
|
-
gap: 0.5rem 0.75rem;
|
|
228
|
-
font-size: 0.8rem;
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
.days-row label {
|
|
232
|
-
flex-direction: row;
|
|
233
|
-
align-items: center;
|
|
234
|
-
gap: 0.35rem;
|
|
235
|
-
color: #0a0908;
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
.playlist-header {
|
|
239
|
-
display: flex;
|
|
240
|
-
align-items: center;
|
|
241
|
-
justify-content: space-between;
|
|
242
|
-
padding: 1rem 1rem 0.75rem;
|
|
243
|
-
border-bottom: 1px solid #e4e1dc;
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
.playlist-header h2 {
|
|
247
|
-
margin: 0;
|
|
248
|
-
font-size: 0.95rem;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
.playlist-list {
|
|
252
|
-
list-style: none;
|
|
253
|
-
margin: 0;
|
|
254
|
-
padding: 0.5rem;
|
|
255
|
-
flex: 1;
|
|
256
|
-
overflow-y: auto;
|
|
257
|
-
}
|
|
258
|
-
|
|
259
|
-
.playlist-empty {
|
|
260
|
-
padding: 1.5rem 1rem;
|
|
261
|
-
text-align: center;
|
|
262
|
-
color: #888;
|
|
263
|
-
font-size: 0.875rem;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
.playlist-item {
|
|
267
|
-
border: 1px solid #e4e1dc;
|
|
268
|
-
border-radius: 8px;
|
|
269
|
-
padding: 0.65rem;
|
|
270
|
-
margin-bottom: 0.5rem;
|
|
271
|
-
background: #fafaf9;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
.playlist-item-thumb {
|
|
275
|
-
width: 100%;
|
|
276
|
-
aspect-ratio: 16 / 9;
|
|
277
|
-
object-fit: cover;
|
|
278
|
-
border-radius: 4px;
|
|
279
|
-
background: #e4e1dc;
|
|
280
|
-
display: block;
|
|
281
|
-
margin-bottom: 0.5rem;
|
|
282
|
-
}
|
|
283
|
-
|
|
284
|
-
.playlist-item-name {
|
|
285
|
-
font-size: 0.8rem;
|
|
286
|
-
font-weight: 600;
|
|
287
|
-
word-break: break-all;
|
|
288
|
-
margin-bottom: 0.35rem;
|
|
289
|
-
}
|
|
290
|
-
|
|
291
|
-
.playlist-item-meta {
|
|
292
|
-
font-size: 0.75rem;
|
|
293
|
-
color: #666;
|
|
294
|
-
margin-bottom: 0.45rem;
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
.playlist-item-actions {
|
|
298
|
-
display: flex;
|
|
299
|
-
gap: 0.35rem;
|
|
300
|
-
align-items: center;
|
|
301
|
-
}
|
|
302
|
-
|
|
303
|
-
.playlist-item-actions input[type="number"] {
|
|
304
|
-
width: 5rem;
|
|
305
|
-
font: inherit;
|
|
306
|
-
padding: 0.25rem 0.4rem;
|
|
307
|
-
border: 1px solid #d6d2cb;
|
|
308
|
-
border-radius: 4px;
|
|
309
|
-
}
|
|
310
|
-
|
|
311
|
-
.publish-bar {
|
|
312
|
-
padding: 1rem 0 0;
|
|
313
|
-
}
|
|
314
|
-
|
|
315
|
-
#result {
|
|
316
|
-
margin: 0;
|
|
317
|
-
padding: 0.75rem;
|
|
318
|
-
background: #0a0908;
|
|
319
|
-
color: #e8e6e3;
|
|
320
|
-
border-radius: 8px;
|
|
321
|
-
font-size: 0.75rem;
|
|
322
|
-
max-height: 10rem;
|
|
323
|
-
overflow: auto;
|
|
324
|
-
white-space: pre-wrap;
|
|
325
|
-
}
|
|
326
|
-
|
|
327
|
-
.hidden {
|
|
328
|
-
display: none !important;
|
|
329
|
-
}
|
|
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 {
|
|
43
|
+
width: 320px;
|
|
44
|
+
flex-shrink: 0;
|
|
45
|
+
background: #fff;
|
|
46
|
+
border-left: 1px solid #e4e1dc;
|
|
47
|
+
display: flex;
|
|
48
|
+
flex-direction: column;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
.card {
|
|
52
|
+
background: #fff;
|
|
53
|
+
border: 1px solid #e4e1dc;
|
|
54
|
+
border-radius: 10px;
|
|
55
|
+
padding: 1rem 1.1rem;
|
|
56
|
+
margin-bottom: 1rem;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
.card h2 {
|
|
60
|
+
margin: 0 0 0.75rem;
|
|
61
|
+
font-size: 0.95rem;
|
|
62
|
+
font-weight: 600;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
.hint {
|
|
66
|
+
margin: 0 0 0.75rem;
|
|
67
|
+
font-size: 0.8rem;
|
|
68
|
+
color: #666;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
.devices-grid {
|
|
72
|
+
display: grid;
|
|
73
|
+
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
|
74
|
+
gap: 0.85rem;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
.devices-empty {
|
|
78
|
+
margin: 0;
|
|
79
|
+
color: #666;
|
|
80
|
+
font-size: 0.875rem;
|
|
81
|
+
grid-column: 1 / -1;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
.device-card {
|
|
85
|
+
border: 1px solid #e4e1dc;
|
|
86
|
+
border-radius: 8px;
|
|
87
|
+
padding: 0.85rem 0.95rem;
|
|
88
|
+
background: #fafaf9;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
.device-card-header {
|
|
92
|
+
display: flex;
|
|
93
|
+
align-items: center;
|
|
94
|
+
gap: 0.5rem;
|
|
95
|
+
margin-bottom: 0.65rem;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
.device-card-title {
|
|
99
|
+
margin: 0;
|
|
100
|
+
font-size: 0.95rem;
|
|
101
|
+
font-weight: 600;
|
|
102
|
+
line-height: 1.2;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
.status-led {
|
|
106
|
+
width: 11px;
|
|
107
|
+
height: 11px;
|
|
108
|
+
border-radius: 50%;
|
|
109
|
+
flex-shrink: 0;
|
|
110
|
+
box-shadow: 0 0 0 2px rgba(0, 0, 0, 0.06);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
.status-led--online {
|
|
114
|
+
background: #12b76a;
|
|
115
|
+
box-shadow: 0 0 8px rgba(18, 183, 106, 0.55);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
.status-led--offline {
|
|
119
|
+
background: #f04438;
|
|
120
|
+
box-shadow: 0 0 8px rgba(240, 68, 56, 0.45);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
.device-meta {
|
|
124
|
+
display: grid;
|
|
125
|
+
gap: 0.35rem;
|
|
126
|
+
font-size: 0.78rem;
|
|
127
|
+
color: #444;
|
|
128
|
+
margin-bottom: 0.75rem;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
.device-meta dt {
|
|
132
|
+
font-weight: 600;
|
|
133
|
+
color: #666;
|
|
134
|
+
display: inline;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
.device-meta dt::after {
|
|
138
|
+
content: ": ";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
.device-meta dd {
|
|
142
|
+
display: inline;
|
|
143
|
+
margin: 0;
|
|
144
|
+
word-break: break-all;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
.device-meta-row {
|
|
148
|
+
display: block;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
.device-card-actions {
|
|
152
|
+
display: flex;
|
|
153
|
+
flex-wrap: wrap;
|
|
154
|
+
gap: 0.4rem;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
.device-card-actions button {
|
|
158
|
+
font-size: 0.78rem;
|
|
159
|
+
padding: 0.35rem 0.6rem;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
.row {
|
|
163
|
+
display: flex;
|
|
164
|
+
flex-wrap: wrap;
|
|
165
|
+
gap: 0.5rem;
|
|
166
|
+
align-items: center;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
input[type="text"],
|
|
170
|
+
input[type="date"],
|
|
171
|
+
input[type="time"],
|
|
172
|
+
select {
|
|
173
|
+
font: inherit;
|
|
174
|
+
padding: 0.45rem 0.6rem;
|
|
175
|
+
border: 1px solid #d6d2cb;
|
|
176
|
+
border-radius: 6px;
|
|
177
|
+
background: #fff;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
button {
|
|
181
|
+
font: inherit;
|
|
182
|
+
padding: 0.45rem 0.85rem;
|
|
183
|
+
border-radius: 6px;
|
|
184
|
+
border: 1px solid #d6d2cb;
|
|
185
|
+
background: #fff;
|
|
186
|
+
cursor: pointer;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
button:hover {
|
|
190
|
+
background: #f5f3ef;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
button.primary {
|
|
194
|
+
background: #ff8a3d;
|
|
195
|
+
border-color: #e67320;
|
|
196
|
+
color: #0a0908;
|
|
197
|
+
font-weight: 600;
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
button.primary:hover {
|
|
201
|
+
background: #ff9a57;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
button.danger {
|
|
205
|
+
color: #b42318;
|
|
206
|
+
border-color: #fecdca;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
.schedule-grid {
|
|
210
|
+
display: grid;
|
|
211
|
+
grid-template-columns: 1fr 1fr;
|
|
212
|
+
gap: 0.65rem 1rem;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
.schedule-grid label {
|
|
216
|
+
display: flex;
|
|
217
|
+
flex-direction: column;
|
|
218
|
+
gap: 0.25rem;
|
|
219
|
+
font-size: 0.8rem;
|
|
220
|
+
color: #444;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
.days-row {
|
|
224
|
+
grid-column: 1 / -1;
|
|
225
|
+
display: flex;
|
|
226
|
+
flex-wrap: wrap;
|
|
227
|
+
gap: 0.5rem 0.75rem;
|
|
228
|
+
font-size: 0.8rem;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.days-row label {
|
|
232
|
+
flex-direction: row;
|
|
233
|
+
align-items: center;
|
|
234
|
+
gap: 0.35rem;
|
|
235
|
+
color: #0a0908;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
.playlist-header {
|
|
239
|
+
display: flex;
|
|
240
|
+
align-items: center;
|
|
241
|
+
justify-content: space-between;
|
|
242
|
+
padding: 1rem 1rem 0.75rem;
|
|
243
|
+
border-bottom: 1px solid #e4e1dc;
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
.playlist-header h2 {
|
|
247
|
+
margin: 0;
|
|
248
|
+
font-size: 0.95rem;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
.playlist-list {
|
|
252
|
+
list-style: none;
|
|
253
|
+
margin: 0;
|
|
254
|
+
padding: 0.5rem;
|
|
255
|
+
flex: 1;
|
|
256
|
+
overflow-y: auto;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
.playlist-empty {
|
|
260
|
+
padding: 1.5rem 1rem;
|
|
261
|
+
text-align: center;
|
|
262
|
+
color: #888;
|
|
263
|
+
font-size: 0.875rem;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
.playlist-item {
|
|
267
|
+
border: 1px solid #e4e1dc;
|
|
268
|
+
border-radius: 8px;
|
|
269
|
+
padding: 0.65rem;
|
|
270
|
+
margin-bottom: 0.5rem;
|
|
271
|
+
background: #fafaf9;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.playlist-item-thumb {
|
|
275
|
+
width: 100%;
|
|
276
|
+
aspect-ratio: 16 / 9;
|
|
277
|
+
object-fit: cover;
|
|
278
|
+
border-radius: 4px;
|
|
279
|
+
background: #e4e1dc;
|
|
280
|
+
display: block;
|
|
281
|
+
margin-bottom: 0.5rem;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
.playlist-item-name {
|
|
285
|
+
font-size: 0.8rem;
|
|
286
|
+
font-weight: 600;
|
|
287
|
+
word-break: break-all;
|
|
288
|
+
margin-bottom: 0.35rem;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
.playlist-item-meta {
|
|
292
|
+
font-size: 0.75rem;
|
|
293
|
+
color: #666;
|
|
294
|
+
margin-bottom: 0.45rem;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
.playlist-item-actions {
|
|
298
|
+
display: flex;
|
|
299
|
+
gap: 0.35rem;
|
|
300
|
+
align-items: center;
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
.playlist-item-actions input[type="number"] {
|
|
304
|
+
width: 5rem;
|
|
305
|
+
font: inherit;
|
|
306
|
+
padding: 0.25rem 0.4rem;
|
|
307
|
+
border: 1px solid #d6d2cb;
|
|
308
|
+
border-radius: 4px;
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
.publish-bar {
|
|
312
|
+
padding: 1rem 0 0;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
#result {
|
|
316
|
+
margin: 0;
|
|
317
|
+
padding: 0.75rem;
|
|
318
|
+
background: #0a0908;
|
|
319
|
+
color: #e8e6e3;
|
|
320
|
+
border-radius: 8px;
|
|
321
|
+
font-size: 0.75rem;
|
|
322
|
+
max-height: 10rem;
|
|
323
|
+
overflow: auto;
|
|
324
|
+
white-space: pre-wrap;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
.hidden {
|
|
328
|
+
display: none !important;
|
|
329
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|