const state = { activeColor: null, alertActive: false, demoActive: false, busy: false, }; const lamps = [...document.querySelectorAll(".lamp")]; const alertToggle = document.getElementById("alertToggle"); const demoToggle = document.getElementById("demoToggle"); const allOffButton = document.getElementById("allOffButton"); const activeColorText = document.getElementById("activeColorText"); const alertToggleLabel = document.getElementById("alertToggleLabel"); const demoToggleLabel = document.getElementById("demoToggleLabel"); const connectionBadge = document.getElementById("connectionBadge"); const message = document.getElementById("message"); function setMessage(text, isError = false) { message.textContent = text; message.classList.toggle("is-error", isError); } function setConnection(online) { connectionBadge.textContent = online ? "API online" : "API offline"; connectionBadge.className = `badge ${online ? "badge--online" : "badge--offline"}`; } function render() { lamps.forEach((lamp) => { const active = !state.demoActive && lamp.dataset.color === state.activeColor; lamp.classList.toggle("is-active", active); lamp.setAttribute("aria-pressed", String(active)); lamp.disabled = state.busy; }); activeColorText.textContent = state.demoActive ? "Demo running" : (state.activeColor || "Off"); alertToggle.setAttribute("aria-checked", String(state.alertActive)); alertToggleLabel.textContent = state.alertActive ? "On" : "Off"; alertToggle.disabled = state.busy; demoToggle.setAttribute("aria-checked", String(state.demoActive)); demoToggleLabel.textContent = state.demoActive ? "On" : "Off"; demoToggle.disabled = state.busy; allOffButton.disabled = state.busy; } function applyApiState(payload) { if (Object.hasOwn(payload, "active_color")) state.activeColor = payload.active_color; if (Object.hasOwn(payload, "alert_active")) state.alertActive = Boolean(payload.alert_active); if (Object.hasOwn(payload, "demo_active")) state.demoActive = Boolean(payload.demo_active); } async function apiCall(path, options = {}) { const result = await fetch(path, { method: options.method || "GET", headers: { "Accept": "application/json" }, }); const payload = await result.json().catch(() => ({})); if (!result.ok) { throw new Error(payload.error || payload.detail || `Request failed (${result.status})`); } return payload; } async function runAction(action, successMessage) { if (state.busy) return; state.busy = true; render(); setMessage("Sending command…"); try { const payload = await action(); applyApiState(payload); setConnection(true); setMessage(successMessage); } catch (error) { setConnection(false); setMessage(error.message, true); } finally { state.busy = false; render(); } } lamps.forEach((lamp) => { lamp.addEventListener("click", () => { const color = lamp.dataset.color; runAction( () => apiCall(`/ui-api/color/${color}`, { method: "POST" }), `${color[0].toUpperCase()}${color.slice(1)} light activated.`, ); }); }); alertToggle.addEventListener("click", () => { const nextState = state.alertActive ? "off" : "on"; runAction( () => apiCall(`/ui-api/alert/${nextState}`, { method: "POST" }), `Alert turned ${nextState}.`, ); }); demoToggle.addEventListener("click", () => { const nextState = state.demoActive ? "off" : "on"; runAction( () => apiCall(`/ui-api/demo/${nextState}`, { method: "POST" }), `Demo mode turned ${nextState}.`, ); }); allOffButton.addEventListener("click", () => { runAction( () => apiCall("/ui-api/off", { method: "POST" }), "All outputs turned off.", ); }); async function refreshStatus({ quiet = false } = {}) { if (state.busy) return; try { const payload = await apiCall("/ui-api/status"); applyApiState(payload); setConnection(true); if (!quiet) setMessage("Status synchronized."); render(); } catch (error) { setConnection(false); if (!quiet) setMessage(error.message, true); } } refreshStatus(); setInterval(() => refreshStatus({ quiet: true }), 1500);