diff --git a/server/src/plugins/service-admin-http/routes-ablesign.ts b/server/src/plugins/service-admin-http/routes-ablesign.ts index 880109f..14077db 100644 --- a/server/src/plugins/service-admin-http/routes-ablesign.ts +++ b/server/src/plugins/service-admin-http/routes-ablesign.ts @@ -96,15 +96,34 @@ export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void { try { const apiKey = deps.secrets.decryptString(account.api_key_encrypted, "ablesign-key"); const opts = { apiKey, workspaceId: account.workspace_id || undefined }; - const { screen } = await ablesign.headlessPairScreen(opts, title); + const { screen, registrationCode } = await ablesign.headlessPairScreen(opts, title); - await deps.repo.createAbleSignScreen({ + // Poll once for token (may not be available immediately). + let screenToken: string | undefined; + try { + const poll = await ablesign.pollRegistration(registrationCode); + screenToken = poll.screenToken; + } catch { /* token may not be ready yet — kiosk can work without it initially */ } + + const screenRowId = await deps.repo.createAbleSignScreen({ account_id: accountId, ablesign_screen_id: String(screen.id), + ablesign_screen_token_encrypted: screenToken + ? deps.secrets.encryptString(screenToken, "ablesign-token") + : undefined, title: screen.title, orientation: screen.orientation, }); + await deps.repo.createEntity({ + name: `AbleSign: ${screen.title}`, + type: "ablesign", + description: `AbleSign screen (ID: ${String(screen.id)})`, + web_url: "https://player.ablesign.tv", + ablesign_screen_id: screenRowId, + managed: true, + }); + await deps.repo.updateAbleSignAccount(accountId, { screen_count: (account.screen_count ?? 0) + 1, }); diff --git a/server/src/plugins/service-admin-http/routes-admin.ts b/server/src/plugins/service-admin-http/routes-admin.ts index e4ac423..a321cad 100644 --- a/server/src/plugins/service-admin-http/routes-admin.ts +++ b/server/src/plugins/service-admin-http/routes-admin.ts @@ -790,6 +790,9 @@ export function registerAdminRoutes(app: H3, deps: AdminDeps): void { const id = (getRouterParam(event, "id") ?? ""); const ent = await deps.repo.getEntityById(id); if (!ent) return new Response(null, { status: 302, headers: { location: "/admin/entities" } }); + if ((ent as any).managed) { + return new Response(null, { status: 302, headers: { location: `/admin/entities/${String(id)}` } }); + } const body = await readBody>(event); const patch: { name?: string; diff --git a/server/src/shared/db/mappers.ts b/server/src/shared/db/mappers.ts index c01919e..0659676 100644 --- a/server/src/shared/db/mappers.ts +++ b/server/src/shared/db/mappers.ts @@ -258,6 +258,8 @@ export function rowToEntity(r: Row): Entity { html_content: sn(r["html_content"]), web_url: sn(r["web_url"]), dashboard_id: sn(r["dashboard_id"]), + ablesign_screen_id: sn(r["ablesign_screen_id"]), + managed: !!r["managed"], created_at: s(r["created_at"]), }; } diff --git a/server/src/shared/db/migrations-pg.ts b/server/src/shared/db/migrations-pg.ts index af3b308..80af2d0 100644 --- a/server/src/shared/db/migrations-pg.ts +++ b/server/src/shared/db/migrations-pg.ts @@ -514,6 +514,12 @@ export const TENANT_MIGRATIONS: readonly string[] = [ `CREATE INDEX IF NOT EXISTS idx_ablesign_screens_account ON ablesign_screens(account_id)`, `CREATE INDEX IF NOT EXISTS idx_ablesign_screens_kiosk ON ablesign_screens(kiosk_id)`, + // Add ablesign entity type + ablesign_screen_id column to entities + `ALTER TABLE entities DROP CONSTRAINT IF EXISTS entities_type_check`, + `ALTER TABLE entities ADD CONSTRAINT entities_type_check CHECK(type IN ('camera', 'html', 'web', 'dashboard', 'ablesign'))`, + `ALTER TABLE entities ADD COLUMN IF NOT EXISTS ablesign_screen_id TEXT REFERENCES ablesign_screens(id) ON DELETE CASCADE`, + `ALTER TABLE entities ADD COLUMN IF NOT EXISTS managed BOOLEAN NOT NULL DEFAULT false`, + // ---- UUIDv7 PK migration for existing databases ---- // Databases created before UUIDv7 migration have INTEGER PKs. // This migration converts them to TEXT in-place. Safe to run on diff --git a/server/src/shared/db/repository.ts b/server/src/shared/db/repository.ts index 0269ef9..d22d20c 100644 --- a/server/src/shared/db/repository.ts +++ b/server/src/shared/db/repository.ts @@ -2307,11 +2307,13 @@ export class Repository { html_content?: string | null; web_url?: string | null; dashboard_id?: string | null; + ablesign_screen_id?: string | null; + managed?: boolean; }): Promise { const id = uuidv7(); await this._run( - `INSERT INTO entities (id, name, type, description, camera_id, html_content, web_url, dashboard_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, + `INSERT INTO entities (id, name, type, description, camera_id, html_content, web_url, dashboard_id, ablesign_screen_id, managed) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, [ id, input.name, @@ -2319,8 +2321,10 @@ export class Repository { input.description ?? null, input.type === "camera" ? (input.camera_id ?? null) : null, input.type === "html" ? (input.html_content ?? null) : null, - input.type === "web" ? (input.web_url ?? null) : null, + input.type === "web" || input.type === "ablesign" ? (input.web_url ?? null) : null, input.type === "dashboard" ? (input.dashboard_id ?? null) : null, + input.type === "ablesign" ? (input.ablesign_screen_id ?? null) : null, + input.managed ?? false, ], ); void this.notify("entities", "create", id); @@ -2405,7 +2409,7 @@ export class Repository { if (await this.getEntityByName(name)) { name = `${camera.name} (cam ${camera.id.slice(0, 8)})`; } - return this.createEntity({ name, type: "camera", camera_id: camera.id }); + return this.createEntity({ name, type: "camera", camera_id: camera.id, managed: true }); } async updateKiosk(id: string, patch: Partial): Promise { diff --git a/server/src/shared/types.ts b/server/src/shared/types.ts index e1955ce..53d7afd 100644 --- a/server/src/shared/types.ts +++ b/server/src/shared/types.ts @@ -12,8 +12,8 @@ export type StreamRole = "main" | "sub" | "other"; export type StreamSelector = "auto" | "main" | "sub"; export type StreamPolicy = "auto" | "always_main" | "always_sub"; export type LayoutPriority = "hot" | "normal" | "cold"; -export type CellContentType = "none" | "camera" | "web" | "html"; -export type EntityType = "camera" | "html" | "web" | "dashboard"; +export type CellContentType = "none" | "camera" | "web" | "html" | "ablesign"; +export type EntityType = "camera" | "html" | "web" | "dashboard" | "ablesign"; export interface Entity { id: string; @@ -25,6 +25,10 @@ export interface Entity { web_url: string | null; /** Node-RED dashboard tab id; populated when type === "dashboard". */ dashboard_id: string | null; + /** AbleSign screen row id; populated when type === "ablesign". */ + ablesign_screen_id: string | null; + /** True for entities auto-created by camera sync, cloud cams, AbleSign. Read-only in UI. */ + managed: boolean; created_at: string; } export type DesiredPowerState = "follow_layout" | "on" | "standby";