mirror of
https://github.com/BetterCorp/BetterFrame.git
synced 2026-05-26 23:26:34 +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
5163051218
6 changed files with 46 additions and 8 deletions
|
|
@ -96,15 +96,34 @@ export function registerAbleSignRoutes(app: H3, deps: AdminDeps): void {
|
||||||
try {
|
try {
|
||||||
const apiKey = deps.secrets.decryptString(account.api_key_encrypted, "ablesign-key");
|
const apiKey = deps.secrets.decryptString(account.api_key_encrypted, "ablesign-key");
|
||||||
const opts = { apiKey, workspaceId: account.workspace_id || undefined };
|
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,
|
account_id: accountId,
|
||||||
ablesign_screen_id: String(screen.id),
|
ablesign_screen_id: String(screen.id),
|
||||||
|
ablesign_screen_token_encrypted: screenToken
|
||||||
|
? deps.secrets.encryptString(screenToken, "ablesign-token")
|
||||||
|
: undefined,
|
||||||
title: screen.title,
|
title: screen.title,
|
||||||
orientation: screen.orientation,
|
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, {
|
await deps.repo.updateAbleSignAccount(accountId, {
|
||||||
screen_count: (account.screen_count ?? 0) + 1,
|
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 id = (getRouterParam(event, "id") ?? "");
|
||||||
const ent = await deps.repo.getEntityById(id);
|
const ent = await deps.repo.getEntityById(id);
|
||||||
if (!ent) return new Response(null, { status: 302, headers: { location: "/admin/entities" } });
|
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 body = await readBody<Record<string, string>>(event);
|
||||||
const patch: {
|
const patch: {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
|
|
||||||
|
|
@ -258,6 +258,8 @@ export function rowToEntity(r: Row): Entity {
|
||||||
html_content: sn(r["html_content"]),
|
html_content: sn(r["html_content"]),
|
||||||
web_url: sn(r["web_url"]),
|
web_url: sn(r["web_url"]),
|
||||||
dashboard_id: sn(r["dashboard_id"]),
|
dashboard_id: sn(r["dashboard_id"]),
|
||||||
|
ablesign_screen_id: sn(r["ablesign_screen_id"]),
|
||||||
|
managed: !!r["managed"],
|
||||||
created_at: s(r["created_at"]),
|
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_account ON ablesign_screens(account_id)`,
|
||||||
`CREATE INDEX IF NOT EXISTS idx_ablesign_screens_kiosk ON ablesign_screens(kiosk_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 ----
|
// ---- UUIDv7 PK migration for existing databases ----
|
||||||
// Databases created before UUIDv7 migration have INTEGER PKs.
|
// Databases created before UUIDv7 migration have INTEGER PKs.
|
||||||
// This migration converts them to TEXT in-place. Safe to run on
|
// This migration converts them to TEXT in-place. Safe to run on
|
||||||
|
|
|
||||||
|
|
@ -2307,11 +2307,13 @@ export class Repository {
|
||||||
html_content?: string | null;
|
html_content?: string | null;
|
||||||
web_url?: string | null;
|
web_url?: string | null;
|
||||||
dashboard_id?: string | null;
|
dashboard_id?: string | null;
|
||||||
|
ablesign_screen_id?: string | null;
|
||||||
|
managed?: boolean;
|
||||||
}): Promise<Entity> {
|
}): Promise<Entity> {
|
||||||
const id = uuidv7();
|
const id = uuidv7();
|
||||||
await this._run(
|
await this._run(
|
||||||
`INSERT INTO entities (id, name, type, description, camera_id, html_content, web_url, dashboard_id)
|
`INSERT INTO entities (id, name, type, description, camera_id, html_content, web_url, dashboard_id, ablesign_screen_id, managed)
|
||||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`,
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
[
|
[
|
||||||
id,
|
id,
|
||||||
input.name,
|
input.name,
|
||||||
|
|
@ -2319,8 +2321,10 @@ export class Repository {
|
||||||
input.description ?? null,
|
input.description ?? null,
|
||||||
input.type === "camera" ? (input.camera_id ?? null) : null,
|
input.type === "camera" ? (input.camera_id ?? null) : null,
|
||||||
input.type === "html" ? (input.html_content ?? 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 === "dashboard" ? (input.dashboard_id ?? null) : null,
|
||||||
|
input.type === "ablesign" ? (input.ablesign_screen_id ?? null) : null,
|
||||||
|
input.managed ?? false,
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
void this.notify("entities", "create", id);
|
void this.notify("entities", "create", id);
|
||||||
|
|
@ -2405,7 +2409,7 @@ export class Repository {
|
||||||
if (await this.getEntityByName(name)) {
|
if (await this.getEntityByName(name)) {
|
||||||
name = `${camera.name} (cam ${camera.id.slice(0, 8)})`;
|
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> {
|
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 StreamSelector = "auto" | "main" | "sub";
|
||||||
export type StreamPolicy = "auto" | "always_main" | "always_sub";
|
export type StreamPolicy = "auto" | "always_main" | "always_sub";
|
||||||
export type LayoutPriority = "hot" | "normal" | "cold";
|
export type LayoutPriority = "hot" | "normal" | "cold";
|
||||||
export type CellContentType = "none" | "camera" | "web" | "html";
|
export type CellContentType = "none" | "camera" | "web" | "html" | "ablesign";
|
||||||
export type EntityType = "camera" | "html" | "web" | "dashboard";
|
export type EntityType = "camera" | "html" | "web" | "dashboard" | "ablesign";
|
||||||
|
|
||||||
export interface Entity {
|
export interface Entity {
|
||||||
id: string;
|
id: string;
|
||||||
|
|
@ -25,6 +25,10 @@ export interface Entity {
|
||||||
web_url: string | null;
|
web_url: string | null;
|
||||||
/** Node-RED dashboard tab id; populated when type === "dashboard". */
|
/** Node-RED dashboard tab id; populated when type === "dashboard". */
|
||||||
dashboard_id: string | null;
|
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;
|
created_at: string;
|
||||||
}
|
}
|
||||||
export type DesiredPowerState = "follow_layout" | "on" | "standby";
|
export type DesiredPowerState = "follow_layout" | "on" | "standby";
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue