(function () { const apiState = { stateResponse: null, previewResponse: null, catalog: null, events: [], ws: null, commandTimers: new Map(), controlClient: null, }; const dom = { projectName: document.getElementById("project-name"), topologyLabel: document.getElementById("topology-label"), connectionPill: document.getElementById("connection-pill"), previewUpdated: document.getElementById("preview-updated"), refreshButton: document.getElementById("refresh-button"), controlModePill: document.getElementById("control-mode-pill"), pendingCommitPill: document.getElementById("pending-commit-pill"), pendingSessionSummary: document.getElementById("pending-session-summary"), primitiveErrorBanner: document.getElementById("primitive-error-banner"), triggerTransitionButton: document.getElementById("trigger-transition-button"), clearStagedButton: document.getElementById("clear-staged-button"), patternSelect: document.getElementById("pattern-select"), transitionSlider: document.getElementById("transition-slider"), transitionValue: document.getElementById("transition-value"), transitionStyleSelect: document.getElementById("transition-style-select"), brightnessSlider: document.getElementById("brightness-slider"), brightnessValue: document.getElementById("brightness-value"), blackoutButton: document.getElementById("blackout-button"), presetList: document.getElementById("preset-list"), presetIdInput: document.getElementById("preset-id-input"), presetOverwriteInput: document.getElementById("preset-overwrite-input"), savePresetButton: document.getElementById("save-preset-button"), groupFilterInput: document.getElementById("group-filter-input"), groupList: document.getElementById("group-list"), snapshotIdInput: document.getElementById("snapshot-id-input"), snapshotLabelInput: document.getElementById("snapshot-label-input"), snapshotOverwriteInput: document.getElementById("snapshot-overwrite-input"), saveSnapshotButton: document.getElementById("save-snapshot-button"), snapshotList: document.getElementById("snapshot-list"), sceneParams: document.getElementById("scene-params"), previewGrid: document.getElementById("preview-grid"), summaryCards: document.getElementById("summary-cards"), snapshotJson: document.getElementById("snapshot-json"), eventKindFilter: document.getElementById("event-kind-filter"), eventSearchFilter: document.getElementById("event-search-filter"), eventList: document.getElementById("event-list"), }; function init() { apiState.controlClient = createShowControlClient(); bindControls(); refreshAll(); connectStream(); } function bindControls() { dom.refreshButton.addEventListener("click", () => refreshAll()); dom.patternSelect.addEventListener("change", async (event) => { await handlePrimitive( { primitive: "set_pattern", payload: { pattern_id: event.target.value }, }, { rerenderState: true } ); }); dom.transitionSlider.addEventListener("input", async (event) => { const value = Number(event.target.value); dom.transitionValue.textContent = `${value} ms`; await handlePrimitive( { primitive: "set_transition_style", payload: { style: dom.transitionStyleSelect.value, duration_ms: value, }, }, { announceBuffered: false } ); }); dom.transitionStyleSelect.addEventListener("change", async (event) => { await handlePrimitive( { primitive: "set_transition_style", payload: { style: event.target.value, duration_ms: Number(dom.transitionSlider.value), }, }, { rerenderState: true } ); }); dom.brightnessSlider.addEventListener("input", (event) => { const value = Number(event.target.value); dom.brightnessValue.textContent = `${Math.round(value * 100)}%`; debounceCommand("brightness", async () => { await handlePrimitive({ primitive: "set_master_brightness", payload: { value }, }); }); }); dom.blackoutButton.addEventListener("click", async () => { const enabled = !(apiState.stateResponse?.state?.global?.blackout ?? false); await handlePrimitive({ primitive: "blackout", payload: { enabled }, }); }); dom.triggerTransitionButton.addEventListener("click", async () => { await handlePrimitive({ primitive: "trigger_transition" }, { rerenderState: true }); }); dom.clearStagedButton.addEventListener("click", () => { apiState.controlClient.clearPending(); pushEvent({ at: new Date().toLocaleTimeString(), kind: "info", code: "staged_transition_cleared", message: "Staged transition buffer cleared.", }); renderAll(); }); dom.savePresetButton.addEventListener("click", async () => { const presetId = dom.presetIdInput.value.trim(); if (!presetId) { pushEvent({ at: new Date().toLocaleTimeString(), kind: "warning", code: "preset_id_required", message: "Preset ID is required before saving.", }); return; } try { await sendCommand({ type: "save_preset", payload: { preset_id: presetId, overwrite: dom.presetOverwriteInput.checked, }, }); } catch (error) { handleClientError(error, "save_preset"); } }); dom.saveSnapshotButton.addEventListener("click", async () => { const snapshotId = dom.snapshotIdInput.value.trim(); if (!snapshotId) { pushEvent({ at: new Date().toLocaleTimeString(), kind: "warning", code: "snapshot_id_required", message: "Snapshot ID is required before saving a creative variant.", }); return; } try { await sendCommand({ type: "save_creative_snapshot", payload: { snapshot_id: snapshotId, label: dom.snapshotLabelInput.value.trim() || null, overwrite: dom.snapshotOverwriteInput.checked, }, }); } catch (error) { handleClientError(error, "save_creative_snapshot"); } }); dom.groupFilterInput.addEventListener("input", () => renderGroups(apiState.stateResponse?.state?.global)); dom.eventKindFilter.addEventListener("change", () => renderEvents()); dom.eventSearchFilter.addEventListener("input", () => renderEvents()); } async function refreshAll() { setConnectionState("connecting", "loading"); try { const [stateResponse, previewResponse, catalog] = await Promise.all([ fetchJson("/api/v1/state"), fetchJson("/api/v1/preview"), fetchJson("/api/v1/catalog"), ]); apiState.stateResponse = stateResponse; apiState.previewResponse = previewResponse; apiState.catalog = catalog; renderAll(); setConnectionState("online", "HTTP sync"); } catch (error) { console.error(error); setConnectionState("offline", "snapshot fetch failed"); pushEvent({ at: new Date().toLocaleTimeString(), kind: "error", code: error.code || "http_refresh_failed", message: `HTTP refresh failed: ${error.message}`, }); } } function connectStream() { const protocol = window.location.protocol === "https:" ? "wss:" : "ws:"; const url = `${protocol}//${window.location.host}/api/v1/stream`; const socket = new WebSocket(url); apiState.ws = socket; socket.addEventListener("open", () => { setConnectionState("online", "stream connected"); pushEvent({ at: new Date().toLocaleTimeString(), kind: "info", code: "stream_connected", message: "WebSocket stream connected", }); }); socket.addEventListener("message", (event) => { const envelope = JSON.parse(event.data); handleStreamEnvelope(envelope); }); socket.addEventListener("close", () => { setConnectionState("offline", "stream disconnected"); pushEvent({ at: new Date().toLocaleTimeString(), kind: "warning", code: "stream_reconnect", message: "WebSocket stream closed, retrying", }); window.setTimeout(connectStream, 1500); }); socket.addEventListener("error", () => { setConnectionState("warning", "stream error"); }); } function handleStreamEnvelope(envelope) { const message = envelope.message; if (!message) { return; } if (message.type === "snapshot") { apiState.stateResponse = { api_version: envelope.api_version, generated_at_millis: envelope.generated_at_millis, state: message.payload, }; renderState(); return; } if (message.type === "preview") { apiState.previewResponse = { api_version: envelope.api_version, generated_at_millis: envelope.generated_at_millis, preview: message.payload, }; renderPreview(); renderSnapshotJson(); return; } if (message.type === "event") { pushEvent({ at: `${envelope.generated_at_millis} ms`, kind: message.payload.kind || "info", code: message.payload.code || null, message: message.payload.message, }); } } async function handlePrimitive(primitive, options = {}) { const { announceBuffered = true, rerenderState = false } = options; try { const outcome = await apiState.controlClient.applyPrimitive(primitive); if (outcome.kind === "buffered" && announceBuffered) { pushEvent({ at: new Date().toLocaleTimeString(), kind: "info", code: primitive.primitive, message: outcome.summary, }); } else if (outcome.kind === "command" && outcome.summary) { pushEvent({ at: new Date().toLocaleTimeString(), kind: "info", code: primitive.primitive, message: outcome.summary, }); } if (rerenderState) { renderState(); } else { renderPendingSession(); renderSnapshotJson(); } return outcome; } catch (error) { handleClientError(error, primitive.primitive); renderPendingSession(); renderSnapshotJson(); return null; } } async function sendCommand(command, options = {}) { const { announce = true, refresh = true } = options; const response = await fetchJson("/api/v1/command", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ request_id: `web-${Date.now()}`, command, }), }); if (announce) { pushEvent({ at: new Date().toLocaleTimeString(), kind: "info", code: response.command_type, message: response.summary, }); } if (refresh) { await refreshAll(); } return response; } function debounceCommand(key, callback) { const existing = apiState.commandTimers.get(key); if (existing) { window.clearTimeout(existing); } const timeoutId = window.setTimeout(async () => { try { await callback(); } finally { apiState.commandTimers.delete(key); } }, 120); apiState.commandTimers.set(key, timeoutId); } async function fetchJson(url, options) { const response = await window.fetch(url, options); const body = await response.text(); let payload = null; try { payload = body ? JSON.parse(body) : null; } catch (error) { throw createClientError("invalid_json", `Invalid JSON from ${url}`); } if (!response.ok) { const code = payload?.error?.code || "request_failed"; const message = payload?.error?.message || payload?.error || response.statusText; throw createClientError(code, message); } return payload; } function createShowControlClient() { const urlMode = new URLSearchParams(window.location.search).get("show_control_mode"); const mode = urlMode === "stateless" ? "stateless" : "stateful"; const client = { mode, lastError: null, pending: createEmptyPendingState(), clearPending() { this.pending = createEmptyPendingState(); this.lastError = null; }, hasPending() { return hasPendingState(this.pending); }, effectiveGroupId(liveGroupId) { return this.pending.hasGroupTarget ? this.pending.groupId : liveGroupId; }, stageGroupTarget(groupId) { ensureStatefulSession(this); this.pending.hasGroupTarget = true; this.pending.groupId = groupId; this.lastError = null; return { kind: "buffered", summary: `group target staged: ${groupId || "all_panels"}`, }; }, async applyPrimitive(primitive) { this.lastError = null; try { const outcome = await applyPrimitiveWithClient(this, primitive); this.lastError = null; return outcome; } catch (error) { this.lastError = { code: error.code || "primitive_failed", message: error.message, }; throw error; } }, }; return client; } async function applyPrimitiveWithClient(client, primitive) { switch (primitive.primitive) { case "blackout": return { kind: "direct", response: await sendCommand({ type: "set_blackout", payload: primitive.payload, }), }; case "recall_preset": return { kind: "direct", response: await sendCommand({ type: "recall_preset", payload: primitive.payload, }), }; case "recall_creative_snapshot": return { kind: "direct", response: await sendCommand({ type: "recall_creative_snapshot", payload: primitive.payload, }), }; case "set_master_brightness": return { kind: "direct", response: await sendCommand({ type: "set_master_brightness", payload: primitive.payload, }), }; case "upsert_group": return { kind: "direct", response: await sendCommand({ type: "upsert_group", payload: primitive.payload, }), }; case "request_snapshot": return { kind: "snapshot", snapshot: await fetchJson("/api/v1/snapshot"), }; case "set_pattern": ensureStatefulSession(client); if (!primitive.payload.pattern_id || !primitive.payload.pattern_id.trim()) { throw createClientError("invalid_pattern_id", "pattern_id must not be empty"); } client.pending.patternId = primitive.payload.pattern_id; return { kind: "buffered", summary: `pattern staged: ${primitive.payload.pattern_id}`, }; case "set_group_parameter": ensureStatefulSession(client); if (!primitive.payload.key || !primitive.payload.key.trim()) { throw createClientError( "invalid_group_parameter_key", "group parameter key must not be empty" ); } client.pending.hasGroupTarget = true; client.pending.groupId = primitive.payload.group_id ?? null; client.pending.parameters[primitive.payload.key] = primitive.payload.value; return { kind: "buffered", summary: `group parameter staged: ${primitive.payload.key} for ${primitive.payload.group_id || "all_panels"}`, }; case "set_transition_style": ensureStatefulSession(client); client.pending.transitionStyle = primitive.payload.style; if (typeof primitive.payload.duration_ms === "number") { client.pending.transitionDurationMs = primitive.payload.duration_ms; } return { kind: "buffered", summary: `transition style staged: ${primitive.payload.style}`, }; case "trigger_transition": ensureStatefulSession(client); if (!client.pending.patternId) { throw createClientError( "transition_pattern_required", "trigger_transition requires a staged pattern" ); } if (client.pending.hasGroupTarget) { await sendCommand( { type: "select_group", payload: { group_id: client.pending.groupId }, }, { announce: false, refresh: false } ); } if (client.pending.transitionDurationMs !== null) { await sendCommand( { type: "set_transition_duration_ms", payload: { duration_ms: client.pending.transitionDurationMs }, }, { announce: false, refresh: false } ); } if (client.pending.transitionStyle) { await sendCommand( { type: "set_transition_style", payload: { style: client.pending.transitionStyle }, }, { announce: false, refresh: false } ); } await sendCommand( { type: "select_pattern", payload: { pattern_id: client.pending.patternId }, }, { announce: false, refresh: false } ); const parameterEntries = Object.entries(client.pending.parameters); for (const [key, value] of parameterEntries) { await sendCommand( { type: "set_scene_parameter", payload: { key, value }, }, { announce: false, refresh: false } ); } const summary = client.pending.hasGroupTarget ? `transition triggered: ${client.pending.patternId} on ${client.pending.groupId || "all_panels"}` : `transition triggered: ${client.pending.patternId}`; client.clearPending(); await refreshAll(); return { kind: "command", summary, }; default: throw createClientError("unknown_primitive", `unknown primitive '${primitive.primitive}'`); } } function ensureStatefulSession(client) { if (client.mode !== "stateful") { throw createClientError( "show_control_session_required", "staged show-control primitives require a stateful session" ); } } function createEmptyPendingState() { return { patternId: null, hasGroupTarget: false, groupId: null, parameters: {}, transitionStyle: null, transitionDurationMs: null, }; } function hasPendingState(pending) { return Boolean( pending.patternId || pending.hasGroupTarget || Object.keys(pending.parameters).length || pending.transitionStyle || pending.transitionDurationMs !== null ); } function createClientError(code, message) { const error = new Error(message); error.code = code; return error; } function handleClientError(error, fallbackCode) { pushEvent({ at: new Date().toLocaleTimeString(), kind: "error", code: error.code || fallbackCode || "primitive_failed", message: error.message, }); } function renderAll() { renderState(); renderPreview(); renderPendingSession(); renderEvents(); } function renderState() { const state = apiState.stateResponse?.state; if (!state) { renderPendingSession(); return; } const global = state.global; const scene = state.active_scene; const pending = apiState.controlClient.pending; const displayedPatternId = pending.patternId || global.selected_pattern; const displayedTransitionStyle = pending.transitionStyle || global.transition_style; const displayedTransitionDuration = pending.transitionDurationMs !== null ? pending.transitionDurationMs : global.transition_duration_ms; dom.projectName.textContent = state.system.project_name; dom.topologyLabel.textContent = `${state.system.topology_label} / API ${apiState.stateResponse.api_version}`; dom.patternSelect.innerHTML = ""; (apiState.catalog?.patterns || []).forEach((pattern) => { const option = document.createElement("option"); option.value = pattern.pattern_id; option.textContent = `${pattern.display_name} (${pattern.pattern_id})`; option.selected = pattern.pattern_id === displayedPatternId; dom.patternSelect.appendChild(option); }); dom.patternSelect.value = displayedPatternId; dom.transitionSlider.value = String(displayedTransitionDuration); dom.transitionValue.textContent = `${displayedTransitionDuration} ms`; dom.transitionStyleSelect.value = displayedTransitionStyle; dom.brightnessSlider.value = String(global.master_brightness); dom.brightnessValue.textContent = `${Math.round(global.master_brightness * 100)}%`; dom.blackoutButton.textContent = global.blackout ? "Release blackout" : "Enable blackout"; dom.blackoutButton.classList.toggle("is-active", global.blackout); renderPresets(scene); renderGroups(global); renderCreativeSnapshots(); renderSceneParameters(scene, global); renderSummaryCards(state); renderPendingSession(); renderSnapshotJson(); } function renderPendingSession() { const client = apiState.controlClient; const pending = client.pending; dom.controlModePill.textContent = client.mode; dom.controlModePill.className = client.mode === "stateful" ? "pill pill-online" : "pill pill-warning"; dom.pendingCommitPill.textContent = client.hasPending() ? "staged" : "idle"; dom.pendingCommitPill.className = client.hasPending() ? "pill pill-warning" : "pill pill-offline"; const cards = []; if (pending.patternId) { cards.push(renderPendingCard("Pattern", pending.patternId)); } if (pending.hasGroupTarget) { cards.push(renderPendingCard("Target Group", pending.groupId || "all_panels")); } if (pending.transitionStyle || pending.transitionDurationMs !== null) { const detail = [ pending.transitionStyle || "inherit", pending.transitionDurationMs !== null ? `${pending.transitionDurationMs} ms` : "duration unchanged", ].join(" / "); cards.push(renderPendingCard("Transition", detail)); } const parameterKeys = Object.keys(pending.parameters); if (parameterKeys.length) { cards.push(renderPendingCard("Parameters", `${parameterKeys.length} staged: ${parameterKeys.join(", ")}`)); } dom.pendingSessionSummary.innerHTML = cards.length ? cards.join("") : '
No staged transition yet. Stage pattern, group target, parameters or transition config, then commit explicitly.
'; if (client.lastError) { dom.primitiveErrorBanner.classList.remove("hidden"); dom.primitiveErrorBanner.innerHTML = ` ${escapeHtml(client.lastError.code)} ${escapeHtml(client.lastError.message)} `; } else { dom.primitiveErrorBanner.classList.add("hidden"); dom.primitiveErrorBanner.innerHTML = ""; } dom.triggerTransitionButton.classList.toggle("staged", client.hasPending()); dom.clearStagedButton.disabled = !client.hasPending(); } function renderPendingCard(label, detail) { return `
${escapeHtml(label)} ${escapeHtml(detail)}
`; } function renderPresets(scene) { dom.presetList.innerHTML = ""; const presets = apiState.catalog?.presets || []; if (!presets.length) { dom.presetList.innerHTML = '
No presets available.
'; return; } presets.forEach((preset) => { const button = document.createElement("button"); button.type = "button"; button.className = "preset-button"; button.classList.toggle("active", scene.preset_id === preset.preset_id); button.innerHTML = ` ${preset.preset_id}
${preset.pattern_id} / ${preset.transition_style} / ${preset.source}
`; button.addEventListener("click", async () => { await handlePrimitive({ primitive: "recall_preset", payload: { preset_id: preset.preset_id }, }); }); dom.presetList.appendChild(button); }); } function renderGroups(global) { dom.groupList.innerHTML = ""; if (!global) { return; } const filterValue = dom.groupFilterInput.value.trim().toLowerCase(); const stagedGroupId = apiState.controlClient.pending.hasGroupTarget ? apiState.controlClient.pending.groupId : undefined; const groups = (apiState.catalog?.groups || []).filter((group) => { if (!filterValue) { return true; } return ( group.group_id.toLowerCase().includes(filterValue) || (group.tags || []).some((tag) => tag.toLowerCase().includes(filterValue)) ); }); const allButton = document.createElement("button"); allButton.type = "button"; allButton.className = "group-button"; allButton.classList.toggle( "active", stagedGroupId !== undefined ? stagedGroupId === null : !global.selected_group ); allButton.classList.toggle("staged", stagedGroupId === null); allButton.innerHTML = "all_panels
target group for next commit
"; allButton.addEventListener("click", () => { try { const outcome = apiState.controlClient.stageGroupTarget(null); pushEvent({ at: new Date().toLocaleTimeString(), kind: "info", code: "stage_group_target", message: outcome.summary, }); renderGroups(global); renderPendingSession(); renderSnapshotJson(); } catch (error) { apiState.controlClient.lastError = { code: error.code || "stage_group_target_failed", message: error.message, }; handleClientError(error, "stage_group_target"); renderPendingSession(); } }); dom.groupList.appendChild(allButton); if (!groups.length) { const empty = document.createElement("div"); empty.className = "empty-state"; empty.textContent = "No groups match the current filter."; dom.groupList.appendChild(empty); return; } groups.forEach((group) => { const button = document.createElement("button"); button.type = "button"; button.className = "group-button"; button.classList.toggle( "active", stagedGroupId !== undefined ? group.group_id === stagedGroupId : group.group_id === global.selected_group ); button.classList.toggle("staged", group.group_id === stagedGroupId); button.innerHTML = ` ${group.group_id}
${group.member_count} members / ${group.source}
`; button.addEventListener("click", () => { try { const outcome = apiState.controlClient.stageGroupTarget(group.group_id); pushEvent({ at: new Date().toLocaleTimeString(), kind: "info", code: "stage_group_target", message: outcome.summary, }); renderGroups(global); renderPendingSession(); renderSnapshotJson(); } catch (error) { apiState.controlClient.lastError = { code: error.code || "stage_group_target_failed", message: error.message, }; handleClientError(error, "stage_group_target"); renderPendingSession(); } }); dom.groupList.appendChild(button); }); } function renderCreativeSnapshots() { dom.snapshotList.innerHTML = ""; const snapshots = apiState.catalog?.creative_snapshots || []; if (!snapshots.length) { dom.snapshotList.innerHTML = '
No creative snapshots saved yet.
'; return; } snapshots.forEach((snapshot) => { const card = document.createElement("article"); card.className = "snapshot-card"; card.innerHTML = `
${snapshot.label || snapshot.snapshot_id}
${snapshot.snapshot_id}
${snapshot.pattern_id} ${snapshot.transition_style} ${snapshot.transition_duration_ms} ms ${snapshot.target_group || "all_panels"}
`; card.querySelector("button").addEventListener("click", async () => { await handlePrimitive({ primitive: "recall_creative_snapshot", payload: { snapshot_id: snapshot.snapshot_id }, }); }); dom.snapshotList.appendChild(card); }); } function renderSceneParameters(scene, global) { dom.sceneParams.innerHTML = ""; const parameters = scene.parameters || []; if (!parameters.length) { dom.sceneParams.innerHTML = '
This pattern has no exposed scene parameters.
'; return; } parameters.forEach((parameter) => { const stagedValue = apiState.controlClient.pending.parameters[parameter.key]; const displayValue = stagedValue || parameter.value; const card = document.createElement("div"); card.className = "parameter-card"; card.classList.toggle("staged", Boolean(stagedValue)); if (parameter.kind === "scalar") { const currentValue = Number(displayValue.value || 0); card.innerHTML = ` `; const slider = card.querySelector("input"); const readout = card.querySelector("span:last-of-type"); slider.addEventListener("input", async (event) => { const value = Number(event.target.value); readout.textContent = value.toFixed(2); await handlePrimitive( { primitive: "set_group_parameter", payload: { group_id: apiState.controlClient.effectiveGroupId(global.selected_group), key: parameter.key, value: { kind: "scalar", value }, }, }, { announceBuffered: false } ); }); } else if (parameter.kind === "toggle") { const checked = Boolean(displayValue.value); card.innerHTML = ` `; const checkbox = card.querySelector("input"); checkbox.addEventListener("change", async (event) => { await handlePrimitive( { primitive: "set_group_parameter", payload: { group_id: apiState.controlClient.effectiveGroupId(global.selected_group), key: parameter.key, value: { kind: "toggle", value: event.target.checked }, }, }, { announceBuffered: false } ); }); } else { const currentValue = displayValue.value || ""; card.innerHTML = ` `; const input = card.querySelector("input"); input.addEventListener("change", async (event) => { await handlePrimitive( { primitive: "set_group_parameter", payload: { group_id: apiState.controlClient.effectiveGroupId(global.selected_group), key: parameter.key, value: { kind: "text", value: event.target.value }, }, }, { announceBuffered: false } ); }); } dom.sceneParams.appendChild(card); }); } function renderPreview() { const preview = apiState.previewResponse?.preview; dom.previewGrid.innerHTML = ""; if (!preview?.panels?.length) { dom.previewGrid.innerHTML = '
Preview stream is waiting for panel snapshots.
'; return; } dom.previewUpdated.textContent = `${apiState.previewResponse.generated_at_millis} ms`; const panels = [...preview.panels].sort(comparePreviewPanels); panels.forEach((panel) => { const card = document.createElement("article"); card.className = "preview-card"; card.style.setProperty("--preview-color", panel.representative_color_hex); card.innerHTML = `

${panel.node_id}

${panel.panel_position} / ${panel.source}
${panel.energy_percent}%
${panel.sample_led_hex .map( (hex) => `` ) .join("")}
`; dom.previewGrid.appendChild(card); }); } function renderSummaryCards(state) { const scene = state.active_scene; const global = state.global; const engine = state.engine; const nodeStats = summarizeNodes(state.nodes || []); const creativeSnapshotCount = (apiState.catalog?.creative_snapshots || []).length; const cards = [ { label: "Active Pattern", value: scene.pattern_id, detail: scene.preset_id ? `Preset ${scene.preset_id}` : "live scene", }, { label: "Group Target", value: scene.target_group || "all_panels", detail: `${(apiState.catalog?.groups || []).length} groups available`, }, { label: "Transition", value: `${global.transition_style} / ${global.transition_duration_ms} ms`, detail: engine.active_transition ? `${engine.active_transition.style} ${Math.round(engine.active_transition.progress * 100)}%` : "idle", }, { label: "Brightness", value: `${Math.round(global.master_brightness * 100)}%`, detail: global.blackout ? "blackout active" : "output live", }, { label: "Engine", value: `${engine.frame_hz} fps target`, detail: `${engine.logic_hz} hz logic / frame ${engine.frame_index}`, }, { label: "Nodes", value: `${nodeStats.online}/${state.nodes.length} online`, detail: `${nodeStats.degraded} degraded / ${nodeStats.offline} offline`, }, { label: "Creative Snapshots", value: `${creativeSnapshotCount}`, detail: `${(apiState.catalog?.presets || []).length} presets in library`, }, ]; dom.summaryCards.innerHTML = cards .map( (card) => `
${card.value} ${card.label}
${card.detail}
` ) .join(""); } function renderSnapshotJson() { dom.snapshotJson.textContent = JSON.stringify(buildComposedSnapshot(), null, 2); } function buildComposedSnapshot() { return { api_version: apiState.stateResponse?.api_version || apiState.previewResponse?.api_version || "v1", generated_at_millis: apiState.previewResponse?.generated_at_millis || apiState.stateResponse?.generated_at_millis || 0, state: apiState.stateResponse?.state || null, preview: apiState.previewResponse?.preview || null, catalog: apiState.catalog || null, show_control_client: { mode: apiState.controlClient.mode, pending: apiState.controlClient.pending, last_error: apiState.controlClient.lastError, }, }; } function pushEvent(entry) { apiState.events.unshift({ kind: entry.kind || "info", code: entry.code || null, ...entry, }); apiState.events = apiState.events.slice(0, 50); renderEvents(); } function renderEvents() { const kindFilter = dom.eventKindFilter.value; const searchFilter = dom.eventSearchFilter.value.trim().toLowerCase(); const filtered = apiState.events.filter((entry) => { const kindMatches = kindFilter === "all" || entry.kind === kindFilter; const searchMatches = !searchFilter || (entry.message || "").toLowerCase().includes(searchFilter) || (entry.code || "").toLowerCase().includes(searchFilter); return kindMatches && searchMatches; }); if (!filtered.length) { dom.eventList.innerHTML = '
No events match the current filter.
'; return; } dom.eventList.innerHTML = filtered .map( (entry) => `
${entry.at}
${entry.code ? `${entry.code}` : ""} ${entry.message}
` ) .join(""); } function setConnectionState(kind, message) { dom.connectionPill.textContent = message; dom.connectionPill.className = kind === "online" ? "pill pill-online" : kind === "warning" ? "pill pill-warning" : "pill pill-offline"; } function summarizeNodes(nodes) { return nodes.reduce( (summary, node) => { summary[node.connection] += 1; return summary; }, { online: 0, degraded: 0, offline: 0 } ); } function comparePreviewPanels(left, right) { const leftNode = left.node_id.localeCompare(right.node_id); if (leftNode !== 0) { return leftNode; } return panelPositionRank(left.panel_position) - panelPositionRank(right.panel_position); } function panelPositionRank(position) { if (position === "top") { return 0; } if (position === "middle") { return 1; } return 2; } function escapeHtml(value) { return String(value) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """); } init(); })();