feat(01-01): create device registry, show store, FastAPI app, API routes, and frontend shell
- DeviceRegistry with load/save/add/remove/instantiate and _DEVICE_CLASSES dict
- ShowStore with save/load/list_ids using aiofiles for async I/O
- FastAPI app with lifespan hook (startup: load registry/store, shutdown: save registry)
- API routes: GET/POST /api/devices/, DELETE /api/devices/{id}, GET/POST /api/shows/, GET /api/shows/{id}
- WebSocket stub at /ws with ConnectionManager and ack echo
- Frontend shell: DAW layout with header, sidebar (DEVICES + ANIMATIONS), TIMELINE, TRANSPORT
- Terminal aesthetic: dark theme, cyan accents, monospace fonts, no drop shadows
- .gitignore for runtime data (devices.json, shows/, __pycache__, .venv)
This commit is contained in:
88
lightsync/frontend/app.js
Normal file
88
lightsync/frontend/app.js
Normal file
@@ -0,0 +1,88 @@
|
||||
// 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`;
|
||||
|
||||
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 = '<span class="text-dim">no devices registered</span>';
|
||||
return;
|
||||
}
|
||||
list.innerHTML = devices.map(d =>
|
||||
`<div class="device-row">
|
||||
<span class="text-accent">${escapeHtml(d.name)}</span>
|
||||
<span class="text-dim">${escapeHtml(d.strip_type)} / ${d.led_count} LEDs / ${escapeHtml(d.ip)}:${d.port}</span>
|
||||
</div>`
|
||||
).join("");
|
||||
} catch (err) {
|
||||
const list = document.getElementById("device-list");
|
||||
list.innerHTML = `<span class="text-dim">error loading devices</span>`;
|
||||
console.error("[devices]", err);
|
||||
}
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
const div = document.createElement("div");
|
||||
div.appendChild(document.createTextNode(String(str)));
|
||||
return div.innerHTML;
|
||||
}
|
||||
|
||||
loadDevices();
|
||||
Reference in New Issue
Block a user