2026-05-09 23:09:13 +00:00
|
|
|
/**
|
|
|
|
|
* Admin page routes — overview, cameras, kiosks, labels, etc.
|
|
|
|
|
*/
|
|
|
|
|
import { type H3, html, readBody } from "h3";
|
|
|
|
|
import type { AdminDeps } from "./index.js";
|
|
|
|
|
import {
|
|
|
|
|
OverviewPage,
|
|
|
|
|
CamerasPage,
|
|
|
|
|
CameraNewPage,
|
|
|
|
|
KiosksPage,
|
|
|
|
|
SimpleListPage,
|
|
|
|
|
} from "../../web-templates/admin-pages.js";
|
|
|
|
|
|
|
|
|
|
export function registerAdminRoutes(app: H3, deps: AdminDeps): void {
|
|
|
|
|
// ---- Overview -------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
app.get("/admin/", (event) => {
|
|
|
|
|
const user = event.context.user!;
|
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
|
|
|
const cameras = deps.repo.listCameras();
|
|
|
|
|
const kiosks = deps.repo.listKiosks();
|
|
|
|
|
const layouts = deps.repo.listDisplays(); // for count
|
|
|
|
|
const events = deps.repo.recentEvents(10);
|
2026-05-09 23:09:13 +00:00
|
|
|
const onlineKiosks = kiosks.filter((k) => {
|
|
|
|
|
if (!k.last_seen_at) return false;
|
|
|
|
|
return Date.now() - new Date(k.last_seen_at).getTime() < 5 * 60 * 1000;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return html(OverviewPage({
|
|
|
|
|
user: user.username,
|
|
|
|
|
cameraCount: cameras.length,
|
|
|
|
|
kioskCount: kiosks.length,
|
|
|
|
|
onlineKioskCount: onlineKiosks.length,
|
|
|
|
|
layoutCount: layouts.length,
|
|
|
|
|
events,
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Redirect /admin to /admin/
|
|
|
|
|
app.get("/admin", () => {
|
|
|
|
|
return new Response(null, { status: 301, headers: { location: "/admin/" } });
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- Cameras --------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
app.get("/admin/cameras", (event) => {
|
|
|
|
|
const user = event.context.user!;
|
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
|
|
|
const cameras = deps.repo.listCameras();
|
2026-05-09 23:09:13 +00:00
|
|
|
const streamCounts = new Map<number, number>();
|
|
|
|
|
for (const cam of cameras) {
|
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
|
|
|
streamCounts.set(cam.id, deps.repo.listCameraStreams(cam.id).length);
|
2026-05-09 23:09:13 +00:00
|
|
|
}
|
|
|
|
|
return html(CamerasPage({ user: user.username, cameras, streamCounts }));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.get("/admin/cameras/new", (event) => {
|
|
|
|
|
const user = event.context.user!;
|
|
|
|
|
return html(CameraNewPage({ user: user.username }));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.post("/admin/cameras/new", async (event) => {
|
|
|
|
|
const user = event.context.user!;
|
|
|
|
|
const body = await readBody<Record<string, string>>(event);
|
|
|
|
|
const name = (body?.["name"] ?? "").trim();
|
|
|
|
|
const type = body?.["type"] as "rtsp" | "onvif" | undefined;
|
|
|
|
|
const errors: string[] = [];
|
|
|
|
|
|
|
|
|
|
if (!name || name.length > 128) {
|
|
|
|
|
errors.push("Name required (max 128 chars).");
|
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
|
|
|
} else if (deps.repo.getCameraByName(name)) {
|
2026-05-09 23:09:13 +00:00
|
|
|
errors.push("Camera name already in use.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (type !== "rtsp" && type !== "onvif") {
|
|
|
|
|
errors.push("Select camera type.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let rtspUrl: string | undefined;
|
|
|
|
|
let onvifHost: string | undefined;
|
|
|
|
|
let onvifPort: number | undefined;
|
|
|
|
|
let onvifUser: string | undefined;
|
|
|
|
|
let onvifPass: string | undefined;
|
|
|
|
|
|
|
|
|
|
if (type === "rtsp") {
|
|
|
|
|
rtspUrl = (body?.["rtsp_url"] ?? "").trim();
|
|
|
|
|
if (!rtspUrl) errors.push("RTSP URL required.");
|
|
|
|
|
} else if (type === "onvif") {
|
|
|
|
|
onvifHost = (body?.["onvif_host"] ?? "").trim();
|
|
|
|
|
onvifPort = parseInt(body?.["onvif_port"] ?? "80", 10);
|
|
|
|
|
onvifUser = (body?.["onvif_username"] ?? "").trim();
|
|
|
|
|
onvifPass = body?.["onvif_password"] ?? "";
|
|
|
|
|
if (!onvifHost) errors.push("ONVIF host required.");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (errors.length > 0) {
|
|
|
|
|
return html(CameraNewPage({
|
|
|
|
|
user: user.username,
|
|
|
|
|
error: errors.join(" "),
|
|
|
|
|
values: body,
|
|
|
|
|
}));
|
|
|
|
|
}
|
|
|
|
|
|
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
|
|
|
const cam = deps.repo.createCamera({
|
2026-05-09 23:09:13 +00:00
|
|
|
name,
|
|
|
|
|
type: type!,
|
|
|
|
|
rtsp_url: rtspUrl ?? null,
|
|
|
|
|
onvif_host: onvifHost ?? null,
|
|
|
|
|
onvif_port: onvifPort ?? null,
|
|
|
|
|
onvif_username: onvifUser ?? null,
|
|
|
|
|
onvif_password: onvifPass ?? null,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// Create default main stream for RTSP cameras
|
|
|
|
|
if (type === "rtsp" && rtspUrl) {
|
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
|
|
|
deps.repo.createCameraStream({
|
2026-05-09 23:09:13 +00:00
|
|
|
camera_id: cam.id,
|
|
|
|
|
role: "main",
|
|
|
|
|
name: "Main",
|
|
|
|
|
rtsp_uri: rtspUrl,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return new Response(null, {
|
|
|
|
|
status: 302,
|
|
|
|
|
headers: { location: "/admin/cameras" },
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- Kiosks ---------------------------------------------------------------
|
|
|
|
|
|
|
|
|
|
app.get("/admin/kiosks", (event) => {
|
|
|
|
|
const user = event.context.user!;
|
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
|
|
|
const kiosks = deps.repo.listKiosks();
|
|
|
|
|
const pending = deps.repo.listPendingPairingCodes();
|
2026-05-09 23:09:13 +00:00
|
|
|
return html(KiosksPage({ user: user.username, kiosks, pendingCodes: pending }));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
// ---- Simple list pages (templates, layouts, displays, labels) -------------
|
|
|
|
|
|
|
|
|
|
app.get("/admin/templates", (event) => {
|
|
|
|
|
const user = event.context.user!;
|
|
|
|
|
return html(SimpleListPage({
|
|
|
|
|
user: user.username,
|
|
|
|
|
pageTitle: "Layout Templates",
|
|
|
|
|
description: "Templates define named regions on a 12x12 grid. A visual template designer is coming.",
|
|
|
|
|
activeNav: "templates",
|
|
|
|
|
items: [], // TODO: list templates
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.get("/admin/layouts", (event) => {
|
|
|
|
|
const user = event.context.user!;
|
|
|
|
|
return html(SimpleListPage({
|
|
|
|
|
user: user.username,
|
|
|
|
|
pageTitle: "Layouts",
|
|
|
|
|
description: "A layout binds cameras and other content into a template's regions for one display.",
|
|
|
|
|
activeNav: "layouts",
|
|
|
|
|
items: [], // TODO: list layouts
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.get("/admin/displays", (event) => {
|
|
|
|
|
const user = event.context.user!;
|
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
|
|
|
const displays = deps.repo.listDisplays();
|
2026-05-09 23:09:13 +00:00
|
|
|
return html(SimpleListPage({
|
|
|
|
|
user: user.username,
|
|
|
|
|
pageTitle: "Displays",
|
|
|
|
|
description: "Physical HDMI displays. Primary display created during setup.",
|
|
|
|
|
activeNav: "displays",
|
|
|
|
|
items: displays.map((d) => ({
|
|
|
|
|
name: d.name,
|
|
|
|
|
detail: `${d.width_px}x${d.height_px} — index ${d.index}`,
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
app.get("/admin/labels", (event) => {
|
|
|
|
|
const user = event.context.user!;
|
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
|
|
|
const labels = deps.repo.listLabels();
|
2026-05-09 23:09:13 +00:00
|
|
|
return html(SimpleListPage({
|
|
|
|
|
user: user.username,
|
|
|
|
|
pageTitle: "Labels",
|
|
|
|
|
description: "Labels route cameras, layouts, and kiosks to each other across sites.",
|
|
|
|
|
activeNav: "labels",
|
|
|
|
|
items: labels.map((l) => ({
|
|
|
|
|
name: l.name,
|
|
|
|
|
detail: l.description ?? "",
|
|
|
|
|
badge: l.color ?? undefined,
|
|
|
|
|
})),
|
|
|
|
|
}));
|
|
|
|
|
});
|
|
|
|
|
}
|