- Port all 7 animations from lightsync/animations/*.py to MicroPython
- Inline _hsv_to_rgb (no colorsys in MicroPython)
- Use bytearray for FireAnimation heat buffer (avoids GC pressure)
- create_animation() factory maps parsed command dicts to instances
- No lightsync imports, no abc, no type hints
- Add AI SYNC button to timeline toolbar, panel hidden by default (D-08)
- Panel: feature toggles (BEATS, ONSETS, CHROMA, RMS), GENERATE button, USE LLM toggle
- GENERATE disabled until audio loaded; re-enables on loadedmetadata event
- LLM toggle enabled/disabled based on /api/sync/llm-available at page load
- D-07: confirmation dialog before replacing existing timeline blocks
- D-11: section_range passed when a segment is selected in the overlay
- Segment selection watcher (200ms interval) updates info text dynamically
- Add AI sync panel styles — flat terminal aesthetic, no border-radius
- Create 06-02-SUMMARY.md with full execution record
- Update STATE.md: plan advanced to 3/3, progress 95%, decisions logged
- Update ROADMAP.md: phase 6 now 2/3 summaries
- Mark SYNC-03 complete in REQUIREMENTS.md
- Create lightsync/audio/features.py with extract_features and extract_features_async
- Create lightsync/audio/segments.py with detect_segments via agglomerative clustering
- Add /api/audio/segments and /api/audio/features endpoints to audio.py
- Initialize app.state.segments cache in main.py lifespan
- Add yt-url input, FETCH button, and yt-progress span to transport bar
- Add fetchWithProgress() SSE ReadableStream reader function
- Add renderProgressBar() terminal-style block fill renderer (DOWNLOADING ████)
- Wire FETCH button click handler with D-02 disable-during-download behavior
- Auto-load audio into player after download (D-01 same pipeline as upload)
- Add .yt-url-input and .yt-progress CSS styles (flat, monospace, no border-radius)
- Enable yt-url input on page load via JS
- 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
- Add yt-dlp>=2025.0.0 and anthropic>=0.40.0 to pyproject.toml
- Wire ANTHROPIC_API_KEY env var in docker-compose.prod.yml (empty default)
- Rebuilt Docker image, both packages importable in container
- audio 'play' listener now sends client.send({ type: 'play', position }) to server
- audio 'pause' listener now sends client.send({ type: 'pause', position }) to server
- Server is_playing flag now set correctly on mouse-initiated playback
- UDP cue scheduler fires for both mouse and keyboard playback
- Space toggles play/pause, sends play/pause msg to server
- ArrowLeft/ArrowRight seek +/-5s, send seek msg, clear preview strips
- Ctrl+Z calls timeline.history.undo()
- Ctrl+Shift+Z and Ctrl+Y both call timeline.history.redo()
- Ctrl+S prevents browser dialog, fetches show and POSTs to save
- All shortcuts suppressed when focus is on input/select/textarea
- Space skips when active element is a button (prevents double-toggle)
- wireKeyboard() called at init after wireAudio()
- Import setCurrentShowId from commands.js
- Add preview_update WebSocket message handler that calls updatePreviewStrip
- Add updatePreviewStrip() function to update device strip color and animation
- Add buildPreviewStrips() function to populate #live-preview from device list
- Call buildPreviewStrips() inside device fetch callback in initTimeline
- Add async refreshShows() to populate show-select dropdown from /api/shows
- Add btn-load-show click handler: loads show, populates tracks, sends WebSocket load msg
- Clear preview strips on seek (remove active class, reset color and anim label)
- Add #live-preview div between timeline-canvas and block-inspector
- Add show selector (show-select + btn-load-show) to transport bar
- Add keyboard-hint span at far right of transport bar
- Append .live-preview, .device-strip, .strip-label, .strip-color, .strip-anim, .keyboard-hint CSS styles
- 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)
- app.js imports BlockInspector and DeleteBlockCommand
- Inspector created with #block-inspector element and timeline.history
- timeline._onSelectBlock callback shows/hides inspector on block select
- inspector._onDelete fires DeleteBlockCommand and hides inspector
- inspector.tracks kept in sync after loadTracks() resolves
- timeline.js block rendering uses blockColor + '1a' for color accent fill
- undo/redo handlers re-trigger _onSelectBlock to refresh inspector values
- UpdateParamsCommand with execute/undo for color/speed/direction/length
- Uses module-level currentShowId (not this._showId) matching existing pattern
- BlockInspector renders compact strip with animation label, color swatch,
speed input, direction select, length input, and remove button
- Color change committed on 'change' event (not 'input') to avoid command flood
- _onDelete callback wired to remove button for app.js to handle
- Create beats.js with BeatGrid class: load(), getCalibratedBeats(), getCalibratedOnsets(), snap(), hasBeats getter
- BeatGrid manages calibrationOffset and snapEnabled state
- snap() uses 100ms threshold against calibrated beat positions
- Handles API field variants: beats/beat_times, tempo/tempo_bpm
- Refactor timeline.js: replace beatTimes/calibrationOffset/snapEnabled with this.beatGrid
- Add getters/setters for calibrationOffset and snapEnabled (backward compat with app.js)
- render() now shows DETECTING BEATS... loading indicator and error state
- render() draws beat marks via getCalibratedBeats(), onset ticks via getCalibratedOnsets()
- snapToBeat() delegates to beatGrid.snap()
- app.js chains BPM label update after loadBeats() resolves
- Import CommandHistory and all 4 command classes from new modules
- hitTest() with 8px EDGE for resize-left/resize-right/move zones
- _hasOverlap() check for time range collision on same track
- snapToBeat() with 0.1s threshold and calibrationOffset
- _setupInteractions() with mousedown/mousemove/keydown listeners
- _startMove(): live drag preview, snap on mouseup, overlap rejection
- _startResize(): left/right edge handles, 0.5s min duration, snap
- handleDrop(): place block from palette with crypto.randomUUID()
- Keyboard: Delete, Ctrl+Z (undo), Ctrl+Shift+Z / Ctrl+Y (redo)
- app.js: dragstart on palette, dragover/dragleave/drop on canvas
- Ghost block rendered during dragover for visual feedback
- Resize handle indicators drawn on selected block
- 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