import { designToken } from "@ekklesia/design-tokens"; import { Button, Field, TextInput } from "@ekklesia/ui"; import { type FormEvent, type ReactElement, useCallback, useEffect, useState } from "react"; import { approveDevicePairing, type EkklesiaHostConfig, type HostDeviceSummary, listHostDevices, lookupDevicePairing, revokeHostDevice, } from "../host-protocol.js"; /** * The administrator's whole device surface: type the code from a screen, name it, approve it, and * later take it away again. * * Deliberately small. Device identity belongs to the platform rather than to any module, so it has * to live in the host — but this is infrastructure plumbing, not a fleet-management product, and * there is no grouping, filtering, bulk action, targeting, health, or remote control here on * purpose (ADR 0036). It renders only for an administrator who actually holds `core.devices.manage`: * a viewer without it is told nothing about which devices exist. */ const panelStyles = { meta: { color: designToken("text.muted"), fontSize: designToken("typography.size-xs"), margin: 0, }, row: { alignItems: "center", borderTop: `${designToken("border.width")} solid ${designToken("border.divider")}`, display: "flex", gap: designToken("spacing.sm"), justifyContent: "space-between", padding: `${designToken("spacing.sm")} 0`, }, section: { display: "grid", gap: designToken("spacing.sm") }, title: { color: designToken("text.secondary"), fontSize: designToken("typography.size-xs"), fontWeight: designToken("typography.weight-bold"), letterSpacing: "0.05em", margin: 0, }, } as const; type PairingStep = | { readonly step: "idle" } | { readonly step: "busy" } | { readonly step: "found"; readonly moduleId: string; readonly permissionId: string } | { readonly step: "error"; readonly message: string }; export interface DevicePanelProps { readonly config: EkklesiaHostConfig; } export function DevicePanel(props: DevicePanelProps): ReactElement | undefined { const { config } = props; const [devices, setDevices] = useState(undefined); const [permitted, setPermitted] = useState(true); const [code, setCode] = useState(""); const [name, setName] = useState(""); const [pairing, setPairing] = useState({ step: "idle" }); const refresh = useCallback(async (): Promise => { try { setDevices(await listHostDevices(config)); } catch { // The panel simply does not render for someone who may not manage devices; it never explains // what it would have shown. setPermitted(false); } }, [config]); useEffect(() => { void refresh(); }, [refresh]); if (!permitted) { return undefined; } const lookUp = async (event: FormEvent): Promise => { event.preventDefault(); setPairing({ step: "busy" }); try { const found = await lookupDevicePairing(config, code.trim()); setPairing({ moduleId: found.moduleId, permissionId: found.permissionId, step: "found" }); } catch { // One message for expired, consumed, mistyped, and never-existed alike. setPairing({ message: "That code is not waiting for approval.", step: "error" }); } }; const approve = async (): Promise => { setPairing({ step: "busy" }); try { await approveDevicePairing(config, code.trim(), name.trim()); setCode(""); setName(""); setPairing({ step: "idle" }); await refresh(); } catch { setPairing({ message: "That code could not be approved.", step: "error" }); } }; const revoke = async (deviceId: string): Promise => { try { await revokeHostDevice(config, deviceId); await refresh(); } catch { // Left to the next refresh rather than asserting a failure the operator cannot act on. } await refresh(); }; return (

DISPLAY DEVICES

void lookUp(event)} style={{ display: "grid", gap: designToken("spacing.sm") }} > {(control) => ( setCode(event.target.value)} value={code} /> )}
{pairing.step === "found" ? (

A display is asking to show {pairing.moduleId}.

{(control) => ( setName(event.target.value)} placeholder="Foyer TV" value={name} /> )}
) : undefined} {pairing.step === "error" ? (

{pairing.message}

) : undefined} {devices !== undefined && devices.length === 0 ? (

No displays are paired.

) : undefined} {devices?.map((device) => (
{device.name} {device.revoked ? " (revoked)" : ""} {device.revoked ? undefined : ( )}
))}
); }