- POST /api/audio/fetch accepts {url: string}
- Spawns yt-dlp subprocess via sys.executable -m yt_dlp
- Streams SSE progress events with PROGRESS_RE line-by-line parsing
- Uses --newline flag for reliable progress output
- Uses yt_%(id)s.%(ext)s template for predictable filenames
- Runs beat analysis on completion and caches in app.state.beats
- Returns done event with path and filename on success
- LOOKAHEAD = 0.060 (60ms window per D-02)
- Scheduler iterates show.tracks/cues on each tick, gated on is_playing and show
- Cues within lookahead window fire UDP via encode_animation_cmd + UDPSender.send
- Past cues (>100ms behind) silently marked fired without UDP dispatch
- active_cue tracking per track enables preview state for already-fired blocks
- preview_update messages broadcast to all clients on each tick (D-07)
- Clear preview_update (animation: null) sent when no active cue on a device
- Import encode_animation_cmd and ShowModel at module level
- Add show, fired_ids, is_playing per-connection state variables
- Extend load handler to load show from show_store by show_id (D-04)
- play handler sets is_playing=True
- pause handler sets is_playing=False without touching fired_ids
- seek handler rebuilds fired_ids from show cues before seek position (D-03)
- 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
- Remove engine dependency entirely (no MPVEngine, no app.state.engine)
- POST /api/audio/upload: saves file, runs librosa beat analysis, caches in app.state.beats
- GET /api/audio/beats?path=...: returns cached or on-demand beat analysis
- GET /api/audio/waveform?path=...: now takes path query param (no engine state lookup)
- GET /api/audio/files: unchanged
- Removed POST /api/audio/load and GET /api/audio/state (engine-only endpoints)
- Create lightsync/audio/waveform.py with extract_peaks() (AUD-05)
- MP3 via ffmpeg pipe, WAV/FLAC/OGG via soundfile+numpy
- Add GET /api/audio/waveform endpoint with asyncio.to_thread
- peaks param clamped 100-5000, returns {peaks, count, file}
- Create lightsync/api/audio.py with POST /load and GET /state endpoints
- Validate file existence and extension (.mp3/.wav/.flac/.ogg)
- Use asyncio.to_thread for engine.load to avoid blocking event loop
- Return 404/400/503 on error conditions
- Register audio router at /api/audio prefix in main.py
- AUD-02 (YouTube) noted as Phase 6 deferral
- Add broadcast_loop sending position ticks at 10Hz to all clients
- Replace echo stub with play/pause/seek/load command dispatch to MPVEngine
- Start broadcast_loop as asyncio task in lifespan, cancel on shutdown
- Use get_engine callable to avoid circular import at module level
- 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)