import type { ModuleUiImporter } from "@ekklesia/module-ui-host"; import { type ReactElement, useEffect, useState } from "react"; import { bootstrapDisplayHost, type DisplayHostBootstrap, type EkklesiaHostConfig, HostProtocolError, } from "../host-protocol.js"; import { DisplayUiFrame } from "./DisplayUiFrame.js"; type DisplayState = | { readonly status: "loading"; readonly generation: number } | { readonly status: "ready"; readonly bootstrap: DisplayHostBootstrap & { status: "ready" } } | { readonly status: "signed-out" } | { readonly status: "unavailable" } | { readonly status: "failed" }; export interface DisplayAppProps { readonly config: EkklesiaHostConfig; readonly moduleId: string; /** Test/composition seam only. Production uses the generic loader's native dynamic import. */ readonly importModule?: ModuleUiImporter; } const displayStyles = { host: { height: "100%", minHeight: "100dvh", minWidth: 0, overflow: "hidden", width: "100%", }, message: { alignItems: "center", color: "#57534e", display: "flex", flexDirection: "column", fontFamily: "system-ui, sans-serif", gap: "0.75rem", height: "100%", justifyContent: "center", margin: 0, minHeight: "100dvh", padding: "1rem", textAlign: "center", }, retry: { background: "transparent", border: "1px solid currentColor", borderRadius: "0.25rem", color: "inherit", cursor: "pointer", font: "inherit", padding: "0.375rem 0.75rem", }, } as const; function DisplayMessage(props: { readonly children: string; readonly testId: string; readonly onRetry?: () => void; }): ReactElement { return (

{props.children}

{props.onRetry ? ( ) : undefined}
); } /** * The host for one display-capable module, with no Dashboard application mounted around it. * * Bootstrap is principal-first, so an anonymous browser never probes a module descriptor. The * descriptor itself is permission-protected server-side; only after it succeeds can an artifact URL * reach the generic loader. A changed `moduleId` or retry aborts the old fetch and keys a new frame, * so neither a late descriptor nor a late import can replace the current display. */ export function DisplayApp(props: DisplayAppProps): ReactElement { const [state, setState] = useState({ generation: 0, status: "loading" }); const [attempt, setAttempt] = useState(0); const { config, importModule, moduleId } = props; useEffect(() => { const abort = new AbortController(); setState({ generation: attempt, status: "loading" }); void bootstrapDisplayHost(config, moduleId, abort.signal) .then((bootstrap) => { if (abort.signal.aborted) { return; } setState( bootstrap.status === "signed-out" ? { status: "signed-out" } : { bootstrap, status: "ready" }, ); }) .catch((error: unknown) => { if (abort.signal.aborted) { return; } console.error("[ekklesia] display host bootstrap failed", error); setState( error instanceof HostProtocolError && (error.code === "DISPLAY_ACCESS_DENIED" || error.code === "DISPLAY_NOT_AVAILABLE") ? { status: "unavailable" } : error instanceof HostProtocolError && error.code === "HOST_UNAUTHENTICATED" ? { status: "signed-out" } : { status: "failed" }, ); }); return () => abort.abort(); }, [attempt, config, moduleId]); const retry = (): void => setAttempt((current) => current + 1); return (
{state.status === "loading" ? ( Loading display… ) : undefined} {state.status === "signed-out" ? ( Sign in to use this display. ) : undefined} {state.status === "unavailable" ? ( This display is unavailable. ) : undefined} {state.status === "failed" ? ( This display could not be reached. ) : undefined} {state.status === "ready" ? ( ) : undefined}
); }