Files
led2/lightsync/main.py
Claude 658c92f94d feat(04-02): command pattern + CommandHistory + block CRUD backend
- CommandHistory class with execute/undo/redo, 50-step max, redo-clear on new action
- PlaceBlockCommand, MoveBlockCommand, ResizeBlockCommand, DeleteBlockCommand
- setCurrentShowId() module-level function for backend sync routing
- syncBlock() fire-and-forget fetch helper for backend sync
- POST/PATCH/DELETE /api/shows/{show_id}/tracks/{device_id}/blocks endpoints
- timeline router registered in main.py with prefix=/api
2026-04-06 23:37:29 +00:00

73 lines
2.3 KiB
Python

import os
from contextlib import asynccontextmanager
from pathlib import Path
from fastapi import FastAPI, Request
from fastapi.responses import FileResponse
from fastapi.staticfiles import StaticFiles
from lightsync.devices.registry import DeviceRegistry
from lightsync.store.show_store import ShowStore
from lightsync.protocol.udp_sender import UDPSender
# Module-level containers populated in lifespan
registry: DeviceRegistry | None = None
show_store: ShowStore | None = None
@asynccontextmanager
async def lifespan(app: FastAPI):
global registry, show_store
# Startup
registry = DeviceRegistry(Path("devices.json"))
await registry.load()
show_store = ShowStore(Path("shows"))
show_store.ensure_dir()
# Beat analysis cache: filepath -> {"tempo": float, "beats": [float, ...]}
app.state.beats = {}
# UDP sender for device communication (Phase 3)
udp_sender = UDPSender()
await udp_sender.start()
app.state.udp_sender = udp_sender
yield
# Shutdown
await app.state.udp_sender.stop()
await registry.save()
def create_app() -> FastAPI:
app = FastAPI(title="LightSync", lifespan=lifespan, redirect_slashes=False)
# API routes (registered before static mount — see Pattern 7)
from lightsync.api import shows, devices, ws, audio, timeline
app.include_router(shows.router, prefix="/api/shows")
app.include_router(devices.router, prefix="/api/devices")
app.include_router(ws.router)
app.include_router(audio.router, prefix="/api/audio")
app.include_router(timeline.router, prefix="/api")
# Serve uploaded audio files so <audio src=...> can load them
shows_dir = Path("/app/shows")
shows_dir.mkdir(parents=True, exist_ok=True)
app.mount("/shows", StaticFiles(directory=str(shows_dir)), name="shows")
# Static file serving — no-cache headers to prevent stale JS/CSS
frontend_dir = Path(__file__).parent / "frontend"
@app.get("/{filename:path}")
async def static_files(request: Request, filename: str):
path = frontend_dir / (filename or "index.html")
if not path.exists() or not path.is_file():
path = frontend_dir / "index.html"
response = FileResponse(path)
response.headers["Cache-Control"] = "no-cache, no-store, must-revalidate"
return response
return app
app = create_app()