BetterFrame/server/src/shared/bundle.ts

159 lines
4.2 KiB
TypeScript
Raw Normal View History

/**
* Label-scoped bundle generation shared module.
*
* Queries cameras/layouts/templates for a kiosk's label set,
* encrypts ONVIF passwords with cluster key, returns versioned bundle.
*/
import { createHash } from "node:crypto";
import type { Repository } from "../plugins/service-store/repository.js";
import type { SecretsApi } from "./secrets.js";
export interface BundleCamera {
id: number;
name: string;
type: string;
rtsp_url: string | null;
onvif_host: string | null;
onvif_port: number | null;
onvif_username: string | null;
onvif_password_encrypted: string | null;
streams: Array<{
id: number;
role: string;
name: string;
rtsp_uri: string;
width: number | null;
height: number | null;
encoding: string | null;
framerate: number | null;
}>;
}
export interface BundleLayout {
id: number;
name: string;
template_id: number | null;
display_id: number | null;
priority: string;
cooling_timeout_seconds: number | null;
preload_camera_ids: number[];
is_default: boolean;
resets_idle_timer: boolean;
cells: Array<{
region_name: string;
content_type: string;
camera_id: number | null;
stream_selector: string | null;
web_url: string | null;
html_content: string | null;
}>;
}
export interface KioskBundle {
kiosk_id: number;
kiosk_name: string;
labels: string[];
operate_labels: string[];
cameras: BundleCamera[];
layouts: BundleLayout[];
templates: Array<{
id: number;
name: string;
regions: unknown;
grid_cols: number;
grid_rows: number;
}>;
version: string;
}
export function generateBundle(
repo: Repository,
secrets: SecretsApi,
kioskId: number,
clusterKey: string | undefined,
): KioskBundle | null {
const kiosk = repo.getKioskById(kioskId);
if (!kiosk) return null;
const scope = repo.bundleScope(kioskId);
const cameras = repo.camerasForLabelIds(scope.labelIds);
const layouts = repo.layoutsForLabelIds(scope.labelIds);
const bundleCameras: BundleCamera[] = cameras.map((cam) => {
const streams = repo.listCameraStreams(cam.id);
let onvifPwEncrypted: string | null = null;
if (cam.onvif_password && clusterKey) {
onvifPwEncrypted = secrets.encryptForCluster(cam.onvif_password, clusterKey);
}
return {
id: cam.id,
name: cam.name,
type: cam.type,
rtsp_url: cam.rtsp_url,
onvif_host: cam.onvif_host,
onvif_port: cam.onvif_port,
onvif_username: cam.onvif_username,
onvif_password_encrypted: onvifPwEncrypted,
streams: streams.map((s) => ({
id: s.id,
role: s.role,
name: s.name,
rtsp_uri: s.rtsp_uri,
width: s.width,
height: s.height,
encoding: s.encoding,
framerate: s.framerate,
})),
};
});
const templateIds = [...new Set(layouts.map((l) => l.template_id).filter((id): id is number => id !== null))];
const templates = templateIds.length > 0 ? repo.layoutTemplates(templateIds) : [];
const bundleLayouts: BundleLayout[] = layouts.map((l) => {
const cells = repo.layoutCells(l.id);
return {
id: l.id,
name: l.name,
template_id: l.template_id,
display_id: l.display_id,
priority: l.priority,
cooling_timeout_seconds: l.cooling_timeout_seconds,
preload_camera_ids: l.preload_camera_ids,
is_default: l.is_default,
resets_idle_timer: l.resets_idle_timer,
cells: cells.map((c) => ({
region_name: c.region_name,
content_type: c.content_type,
camera_id: c.camera_id,
stream_selector: c.stream_selector,
web_url: c.web_url,
html_content: c.html_content,
})),
};
});
const bundle: KioskBundle = {
kiosk_id: kioskId,
kiosk_name: kiosk.name,
labels: scope.labelNames,
operate_labels: scope.operateLabelNames,
cameras: bundleCameras,
layouts: bundleLayouts,
templates: templates.map((t) => ({
id: t.id,
name: t.name,
regions: t.regions,
grid_cols: t.grid_cols,
grid_rows: t.grid_rows,
})),
version: "",
};
bundle.version = createHash("sha256")
.update(JSON.stringify(bundle))
.digest("hex");
return bundle;
}