import type { ModuleUiHostContext } from "@ekklesia/module-ui-sdk"; import { EmptyState, Panel } from "@ekklesia/ui"; import { type ReactElement, useEffect, useRef } from "react"; import { SignageSlate } from "../../browser/slate.js"; import { createDisplayController, type DisplayChannelStateView } from "../../display/controller.js"; import type { SignageApi } from "../api.js"; /** * The live preview. * * It mounts **the display controller itself** — the same scene stage, the same native-image * presentation resolver, the same transition, the same renderer host — against the same channel * state a wall reads. A preview that reimplemented any of that would drift from what the room * actually sees, which makes it worse than no preview at all. * * It differs from a display in exactly two ways, both of them `telemetry: false`: it registers no * viewer, so it never appears in the display roster, and it sends no heartbeat, so it never reports * health for a screen that does not exist. It drives its own refresh instead of subscribing. */ export function PreviewPanel({ api, channelId, context, refreshMs = 2000, }: { readonly api: SignageApi; readonly channelId: string | undefined; readonly context: ModuleUiHostContext; readonly refreshMs?: number; }): ReactElement { const stageRef = useRef(null); useEffect(() => { const root = stageRef.current; if (!root || channelId === undefined) { return; } let stopped = false; const controller = createDisplayController({ context, root, telemetry: false }); let handle: ReturnType | undefined; async function pump(): Promise { if (stopped) { return; } try { const state = await api.get(`channels/${channelId}/display-state`); if (!stopped) { await controller.apply(state); } } catch { // A preview that cannot reach the server simply keeps its last frame, exactly as a wall // does. Nothing about a failed poll belongs in the operator's face. } if (!stopped) { handle = setTimeout(() => void pump(), refreshMs); } } void controller.start().then(() => pump()); return () => { stopped = true; if (handle !== undefined) { clearTimeout(handle); } void controller.stop(); }; }, [api, channelId, context, refreshMs]); return ( {channelId === undefined ? ( ) : (
)} ); }