mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 16:56:33 +00:00
feat: managed entities (read-only) + AbleSign auto-creates entity
- Entity type: add 'ablesign' to EntityType + CellContentType - Entity.managed boolean: true for auto-created entities (camera sync, cloud cams, AbleSign). UI blocks editing managed entities. - Entity.ablesign_screen_id: links to ablesign_screens row - ensureCameraEntity now sets managed=true - AbleSign screen creation auto-creates managed entity with web_url=player.ablesign.tv and ablesign_screen_id FK - PG migration: alter entities CHECK constraint + add columns - Entity edit route rejects POST for managed entities Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
c3bdcbce4c
commit
73dbd9b6bf
6 changed files with 46 additions and 8 deletions
|
|
@ -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,
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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<Record<string, string>>(event);
|
||||
const patch: {
|
||||
name?: string;
|
||||
|
|
|
|||
|
|
@ -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"]),
|
||||
};
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<Entity> {
|
||||
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<Kiosk>): Promise<void> {
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
|
|
|||
Loading…
Reference in a new issue