/** * Admin page templates: overview, cameras, kiosks, account, etc. */ import { js } from "jsx-htmx"; import { Layout } from "./layout.js"; import type { Camera, Kiosk, Label, PairingCode, EventLog } from "../shared/types.js"; // ---- Overview --------------------------------------------------------------- interface OverviewProps { user: string; cameraCount: number; kioskCount: number; onlineKioskCount: number; layoutCount: number; events: EventLog[]; } export function OverviewPage(props: OverviewProps) { return (
Cameras
{String(props.cameraCount)}
Kiosks
{String(props.kioskCount)}
Kiosks Online
{String(props.onlineKioskCount)}
Displays
{String(props.layoutCount)}

Quick Links

Add Camera
RTSP or ONVIF
Pair Kiosk
Enter pairing code
Rule Engine
Node-RED dashboard

Recent Events

{props.events.length === 0 ? ( ) : ( props.events.map((ev) => ( )) )}
Time Topic Source Payload
No events yet
{formatTime(ev.received_at)} {ev.topic} {ev.source_type} {JSON.stringify(ev.payload)}
); } // ---- Cameras ---------------------------------------------------------------- interface CamerasProps { user: string; cameras: Camera[]; streamCounts: Map; } export function CamerasPage(props: CamerasProps) { return (

All Cameras

Add Camera
{props.cameras.length === 0 ? ( ) : ( props.cameras.map((cam) => ( )) )}
Name Type Streams Status
No cameras configured
{cam.name} {cam.type.toUpperCase()} {String(props.streamCounts.get(cam.id) ?? 0)} {cam.enabled ? Enabled : Disabled }
); } // ---- Camera New ------------------------------------------------------------- interface CameraNewProps { user: string; error?: string; values?: Record; } export function CameraNewPage(props: CameraNewProps) { const v = props.values ?? {}; return (
Cancel
); } // ---- Kiosks ----------------------------------------------------------------- interface KiosksProps { user: string; kiosks: Kiosk[]; pendingCodes: PairingCode[]; error?: string; } export function KiosksPage(props: KiosksProps) { return (

Paired Kiosks

{props.kiosks.length === 0 ? ( ) : ( props.kiosks.map((k) => ( )) )}
Name Hardware Last Seen Status
No kiosks paired
{k.name} {k.hardware_model ?? "—"} {k.last_seen_at ? formatTime(k.last_seen_at) : "Never"} {k.enabled ? Active : Disabled }

Pair New Kiosk

8-character code shown on kiosk screen.
Comma-separated label names.
{props.pendingCodes.length > 0 && (
Pending Codes
{props.pendingCodes.map((pc) => (
{pc.code} {formatTime(pc.expires_at)}
))}
)}
); } // ---- Account ---------------------------------------------------------------- interface AccountProps { user: string; totpEnabled: boolean; error?: string; success?: string; } export function AccountPage(props: AccountProps) { return (

Change Password

At least 12 characters.

Two-Factor Authentication

{props.totpEnabled ? (

Enabled {" "}TOTP is active on this account.

) : (

Protect your account with a TOTP authenticator app.

)}
); } // ---- TOTP Enrollment -------------------------------------------------------- interface TotpEnrollProps { user: string; secret: string; provisioningUri: string; recoveryCodes: string[]; } export function TotpEnrollPage(props: TotpEnrollProps) { return (

Step 1: Scan QR Code

Scan this with your authenticator app (Google Authenticator, Authy, etc.).

Can't scan? Enter manually {props.secret}

Step 2: Save Recovery Codes

Save these codes somewhere safe. They will not be shown again.

{props.recoveryCodes.map((code) => (
{code}
))}

Step 3: Verify

Cancel
); } // ---- Simple list page ------------------------------------------------------- interface SimpleListProps { user: string; pageTitle: string; description: string; activeNav: string; items: Array<{ name: string; detail?: string; badge?: string }>; } export function SimpleListPage(props: SimpleListProps) { return (

{props.description}

{props.items.length === 0 ? ( ) : ( props.items.map((item) => ( )) )}
Name Details
None configured yet
{item.name} {item.badge && ( {item.badge} )} {item.detail ?? ""}
); } // ---- Camera Edit ------------------------------------------------------------ interface CameraEditProps { user: string; camera: Camera; labels: Array<{ label_id: number; name: string }>; allLabels: Label[]; streams: Array<{ id: number; role: string; name: string; rtsp_uri: string }>; error?: string; success?: string; } export function CameraEditPage(props: CameraEditProps) { const cam = props.camera; return (

Edit Camera

{cam.type === "rtsp" && (
)} {cam.type === "onvif" && (
)}
Back

Labels

{props.labels.length > 0 ? (
{props.labels.map((l) => (
))}
) : (

No labels attached

)}

Streams

{props.streams.length > 0 ? (
{props.streams.map((s) => ( ))}
RoleNameURI
{s.role} {s.name} {s.rtsp_uri}
) : (

No streams configured

)}
); } // ---- Kiosk Edit ------------------------------------------------------------- interface KioskEditProps { user: string; kiosk: Kiosk; labels: Array<{ label_id: number; name: string; role: string }>; allLabels: Label[]; error?: string; success?: string; } export function KioskEditPage(props: KioskEditProps) { const k = props.kiosk; return (

Edit Kiosk

Back
Hardware: {k.hardware_model ?? "—"}
Paired: {k.paired_at ? formatTime(k.paired_at) : "—"}
Last seen: {k.last_seen_at ? formatTime(k.last_seen_at) : "Never"}

Labels

{props.labels.length > 0 ? (
{props.labels.map((l) => (
))}
) : (

No labels attached

)}
); } // ---- Labels Management ------------------------------------------------------ interface LabelsPageProps { user: string; labels: Label[]; error?: string; } export function LabelsPage(props: LabelsPageProps) { return (

All Labels

{props.labels.length === 0 ? ( ) : ( props.labels.map((l) => ( )) )}
NameColorActions
No labels
{l.name} {l.color ? {l.color} : "—"}
); } // ---- Helpers ---------------------------------------------------------------- function formatTime(iso: string): string { try { const d = new Date(iso); return d.toLocaleString("en-US", { month: "short", day: "numeric", hour: "2-digit", minute: "2-digit", }); } catch { return iso; } }