// LightSync frontend — Phase 1 shell // WebSocket stub + device list loader // Phase 2 expands audio, transport, and WS protocol const WS_URL = `ws://${location.host}/ws`; async function apiPost(path, data) { const res = await fetch(path, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify(data), }); if (!res.ok) throw new Error(`POST ${path}: HTTP ${res.status}`); return res.json(); } async function apiDelete(path) { const res = await fetch(path, { method: "DELETE" }); if (!res.ok && res.status !== 204) throw new Error(`DELETE ${path}: HTTP ${res.status}`); } class LightSyncClient { constructor() { this.ws = null; this.reconnectDelay = 2000; this._reconnectTimer = null; } connect() { if (this._reconnectTimer) { clearTimeout(this._reconnectTimer); this._reconnectTimer = null; } this.ws = new WebSocket(WS_URL); this.ws.onopen = () => { document.getElementById("status-dot").className = "status-dot connected"; document.getElementById("status-text").textContent = "CONNECTED"; }; this.ws.onclose = () => { document.getElementById("status-dot").className = "status-dot"; document.getElementById("status-text").textContent = "OFFLINE"; this._reconnectTimer = setTimeout(() => this.connect(), this.reconnectDelay); }; this.ws.onerror = () => { document.getElementById("status-dot").className = "status-dot error"; }; this.ws.onmessage = (event) => { const msg = JSON.parse(event.data); this.handleMessage(msg); }; } handleMessage(msg) { // Phase 2 will dispatch on msg.type console.debug("[ws]", msg); } send(msg) { if (this.ws?.readyState === WebSocket.OPEN) { this.ws.send(JSON.stringify(msg)); } } } const client = new LightSyncClient(); client.connect(); // Load device list on startup async function loadDevices() { try { const res = await fetch("/api/devices"); if (!res.ok) throw new Error(`HTTP ${res.status}`); const devices = await res.json(); const list = document.getElementById("device-list"); if (devices.length === 0) { list.innerHTML = 'no devices registered'; return; } list.innerHTML = devices.map(d => `
${escapeHtml(d.name)} ${escapeHtml(d.strip_type)} / ${d.led_count} LEDs / ${escapeHtml(d.ip)}:${d.port}
` ).join(""); } catch (err) { const list = document.getElementById("device-list"); list.innerHTML = `error loading devices`; console.error("[devices]", err); } } async function removeDevice(id) { try { await apiDelete(`/api/devices/${id}`); await loadDevices(); } catch (err) { console.error("[devices] remove failed:", err); } } window.removeDevice = removeDevice; function escapeHtml(str) { const div = document.createElement("div"); div.appendChild(document.createTextNode(String(str))); return div.innerHTML; } loadDevices(); document.getElementById("devices-manage-toggle").addEventListener("click", () => { const panel = document.getElementById("devices-panel"); const btn = document.getElementById("devices-manage-toggle"); const active = panel.classList.toggle("manage-active"); btn.classList.toggle("active", active); }); document.getElementById("add-device-form").addEventListener("submit", async (e) => { e.preventDefault(); const form = e.target; const data = { name: form.elements["name"].value.trim(), strip_type: form.strip_type.value, led_count: parseInt(form.led_count.value, 10), ip: form.ip.value.trim(), port: parseInt(form.port.value, 10), }; try { await apiPost("/api/devices", data); form.reset(); form.port.value = "21324"; await loadDevices(); } catch (err) { console.error("[devices] add failed:", err); } });