import type { ModuleUiHostContext } from "@ekklesia/module-ui-sdk"; import { EkklesiaUiStyles, EmptyState, Tabs } from "@ekklesia/ui"; import { type ReactElement, useMemo, useState } from "react"; import { DesktopOnly } from "../browser/desktop-gate.js"; import { createSignageApi } from "./api.js"; import { useResource } from "./hooks.js"; import { DisplaysPanel } from "./panels/DisplaysPanel.js"; import { ImagesPanel } from "./panels/ImagesPanel.js"; import { PlaybackPanel } from "./panels/PlaybackPanel.js"; import { ProvidersPanel } from "./panels/ProvidersPanel.js"; import { renderLoadable } from "./panels/shared.js"; import { TimelinesPanel } from "./panels/TimelinesPanel.js"; import type { ChannelGroupView, SettingsResponse } from "./types.js"; /** * The Signage management screen. * * Host-neutral by construction: every value comes from the host context, and nothing here hardcodes * a URL, an origin, or a global. Presentation is entirely the platform's shared elements plus * Signage's own stylesheet, so this looks like the rest of Ekklesia without copying a colour. * * Permission checks here decide what an operator is *offered*. The server decides what happens: a * hidden button is a courtesy, and every route is enforced regardless of what this believes. */ export function SignageDashboard({ context, }: { readonly context: ModuleUiHostContext; }): ReactElement { const api = useMemo(() => createSignageApi(context.api), [context.api]); // Read before the resources below, so a viewer who may not see Signage at all asks for nothing. // A hidden screen is a usability choice; the server is what actually refuses, and making a // request it will certainly refuse is noise in a log rather than security. const mayView = context.permissions.has("signage.view"); const settings = useResource( () => (mayView ? api.get("settings") : Promise.resolve(undefined)), [mayView], ); const groups = useResource( () => mayView ? api.get<{ groups: readonly ChannelGroupView[] }>("channel-groups") : Promise.resolve(undefined), [mayView], ); const [tab, setTab] = useState("playback"); const [groupId, setGroupId] = useState(undefined); const mayManageAssets = context.permissions.has("signage.assets.manage"); const mayManageTimelines = context.permissions.has("signage.timelines.manage"); const mayControl = context.permissions.has("signage.playback.control"); const mayManageDisplays = context.permissions.has("signage.displays.manage"); if (!mayView) { return ( ); } const selectedGroupId = groupId ?? (groups.state.status === "ready" ? groups.state.value?.groups[0]?.id : undefined); const selectedGroup = groups.state.status === "ready" ? groups.state.value?.groups.find((candidate) => candidate.id === selectedGroupId) : undefined; return ( <> {renderLoadable( settings.state, (loaded) => loaded === undefined ? undefined : ( ), id: "playback", label: "Playback", }, { content: ( { await api.send("POST", `playback/${selectedGroupId}/activate`, { timelineId, when: "now", }); groups.reload(); setTab("playback"); } : undefined } settings={loaded} /> ), id: "timelines", label: "Timelines", }, { content: ( ), id: "images", label: "Images", }, { content: , id: "displays", label: "Displays", }, { content: , id: "providers", label: "Providers", }, ]} /> ), { label: "Loading Signage…", onRetry: settings.reload }, )} ); }