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:
0
lightsync/api/__init__.py
Normal file
0
lightsync/api/__init__.py
Normal file
24
lightsync/api/devices.py
Normal file
24
lightsync/api/devices.py
Normal file
@@ -0,0 +1,24 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from lightsync.models.device import DeviceConfig
|
||||
import lightsync.main as state # access registry via module-level ref
|
||||
|
||||
router = APIRouter(tags=["devices"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_devices() -> list[DeviceConfig]:
|
||||
return state.registry.list_all()
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
async def add_device(config: DeviceConfig) -> DeviceConfig:
|
||||
state.registry.add(config)
|
||||
await state.registry.save()
|
||||
return config
|
||||
|
||||
|
||||
@router.delete("/{device_id}", status_code=204)
|
||||
async def remove_device(device_id: str):
|
||||
if not state.registry.remove(device_id):
|
||||
raise HTTPException(404, "Device not found")
|
||||
await state.registry.save()
|
||||
34
lightsync/api/shows.py
Normal file
34
lightsync/api/shows.py
Normal file
@@ -0,0 +1,34 @@
|
||||
from fastapi import APIRouter, HTTPException
|
||||
from lightsync.models.show import ShowModel
|
||||
import lightsync.main as state # access show_store via module-level ref
|
||||
|
||||
router = APIRouter(tags=["shows"])
|
||||
|
||||
|
||||
@router.get("/")
|
||||
async def list_shows():
|
||||
"""Return summary list of all shows."""
|
||||
summaries = []
|
||||
for show_id in state.show_store.list_ids():
|
||||
show = await state.show_store.load(show_id)
|
||||
if show is not None:
|
||||
summaries.append({
|
||||
"id": str(show.id),
|
||||
"name": show.name,
|
||||
"created_at": show.created_at.isoformat(),
|
||||
})
|
||||
return summaries
|
||||
|
||||
|
||||
@router.post("/", status_code=201)
|
||||
async def create_show(show: ShowModel) -> ShowModel:
|
||||
await state.show_store.save(show)
|
||||
return show
|
||||
|
||||
|
||||
@router.get("/{show_id}")
|
||||
async def get_show(show_id: str) -> ShowModel:
|
||||
show = await state.show_store.load(show_id)
|
||||
if show is None:
|
||||
raise HTTPException(404, "Show not found")
|
||||
return show
|
||||
45
lightsync/api/ws.py
Normal file
45
lightsync/api/ws.py
Normal file
@@ -0,0 +1,45 @@
|
||||
import json
|
||||
from fastapi import APIRouter, WebSocket, WebSocketDisconnect
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
|
||||
class ConnectionManager:
|
||||
def __init__(self):
|
||||
self.active_connections: list[WebSocket] = []
|
||||
|
||||
async def connect(self, ws: WebSocket) -> None:
|
||||
await ws.accept()
|
||||
self.active_connections.append(ws)
|
||||
|
||||
def disconnect(self, ws: WebSocket) -> None:
|
||||
if ws in self.active_connections:
|
||||
self.active_connections.remove(ws)
|
||||
|
||||
async def broadcast(self, message: dict) -> None:
|
||||
dead = []
|
||||
for connection in self.active_connections:
|
||||
try:
|
||||
await connection.send_text(json.dumps(message))
|
||||
except Exception:
|
||||
dead.append(connection)
|
||||
for d in dead:
|
||||
if d in self.active_connections:
|
||||
self.active_connections.remove(d)
|
||||
|
||||
|
||||
manager = ConnectionManager()
|
||||
|
||||
|
||||
@router.websocket("/ws")
|
||||
async def websocket_endpoint(websocket: WebSocket):
|
||||
await manager.connect(websocket)
|
||||
try:
|
||||
while True:
|
||||
data = await websocket.receive_text()
|
||||
msg = json.loads(data)
|
||||
# Phase 2 will dispatch msg["type"] to handlers
|
||||
# For now: echo back with type=ack
|
||||
await websocket.send_text(json.dumps({"type": "ack", "echo": msg}))
|
||||
except WebSocketDisconnect:
|
||||
manager.disconnect(websocket)
|
||||
Reference in New Issue
Block a user