refactor: collapse 6 non-service plugins into shared modules
BSB plugins should be actual services (own port, lifecycle, resource
ownership). Moved secrets, auth, pairing, bundle, nodered-bridge, and
cec-relay from plugin folders to shared modules under server/src/shared/.
4 BSB plugins remain: service-store, service-admin-http,
service-api-http, service-coordinator-ws.
service-admin-http now initializes secrets + auth as plain modules in
init() using the store repo from the plugin-registry singleton. No
more setSiblings() hack or inter-plugin wiring.
sec-config.yaml updated: secrets/auth config moved into
service-admin-http, pairing config into service-api-http, nodered
config into service-coordinator-ws.
2026-05-10 00:29:25 +00:00
|
|
|
/**
|
2026-05-10 01:12:07 +00:00
|
|
|
* 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.
|
refactor: collapse 6 non-service plugins into shared modules
BSB plugins should be actual services (own port, lifecycle, resource
ownership). Moved secrets, auth, pairing, bundle, nodered-bridge, and
cec-relay from plugin folders to shared modules under server/src/shared/.
4 BSB plugins remain: service-store, service-admin-http,
service-api-http, service-coordinator-ws.
service-admin-http now initializes secrets + auth as plain modules in
init() using the store repo from the plugin-registry singleton. No
more setSiblings() hack or inter-plugin wiring.
sec-config.yaml updated: secrets/auth config moved into
service-admin-http, pairing config into service-api-http, nodered
config into service-coordinator-ws.
2026-05-10 00:29:25 +00:00
|
|
|
*/
|
2026-05-10 01:12:07 +00:00
|
|
|
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;
|
|
|
|
|
}
|