--- phase: 04-timeline-editor plan: 01 type: execute wave: 1 depends_on: [] files_modified: - lightsync/models/show.py - lightsync/frontend/index.html - lightsync/frontend/style.css - lightsync/frontend/timeline/timeline.js autonomous: true requirements: [TL-01, TL-05] must_haves: truths: - "Per-device horizontal tracks render on a time axis with device name labels" - "Playback cursor moves across the timeline in realtime as audio plays" - "Waveform peaks render as a background layer behind tracks" - "CueModel has a duration field that defaults to 4.0" artifacts: - path: "lightsync/models/show.py" provides: "CueModel with duration field, AnalysisBlock with calibration_offset" contains: "duration: float = 4.0" - path: "lightsync/frontend/timeline/timeline.js" provides: "TimelineCanvas class with rendering loop" exports: ["TimelineCanvas"] min_lines: 150 - path: "lightsync/frontend/index.html" provides: "Timeline canvas element, toolbar, inspector placeholder" contains: "timeline-canvas" - path: "lightsync/frontend/style.css" provides: "Timeline layout styles, toolbar, inspector strip" contains: "#timeline-canvas" key_links: - from: "lightsync/frontend/timeline/timeline.js" to: "HTML5 audio element" via: "requestAnimationFrame reads audio.currentTime for cursor position" pattern: "audio\\.currentTime" - from: "lightsync/frontend/timeline/timeline.js" to: "/api/audio/waveform" via: "fetch waveform peaks on audio load" pattern: "api/audio/waveform" - from: "lightsync/frontend/timeline/timeline.js" to: "/api/devices" via: "fetch device list to build tracks" pattern: "api/devices" --- Build the timeline canvas foundation — data model changes, HTML layout restructure, canvas rendering with per-device tracks, time axis, waveform background, and realtime playback cursor. Purpose: This is the visual backbone of the DAW UI. Everything else (block interactions, beat overlay, inspector) builds on top of this canvas and coordinate system. Output: A visible, scrollable timeline with device tracks and a moving playback cursor synced to the HTML5 audio element. @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md @.planning/PROJECT.md @.planning/ROADMAP.md @.planning/STATE.md @.planning/phases/04-timeline-editor/04-CONTEXT.md @.planning/phases/04-timeline-editor/04-RESEARCH.md @.planning/phases/04-timeline-editor/04-UI-SPEC.md @lightsync/models/show.py @lightsync/frontend/index.html @lightsync/frontend/style.css @lightsync/frontend/app.js class CueModel(BaseModel): id: UUID = Field(default_factory=uuid4) timestamp: float mode: Literal["animation", "frame_sequence"] = "animation" animation: str | None = None params: dict[str, Any] = Field(default_factory=dict) class AnalysisBlock(BaseModel): analysed_at: datetime | None = None tempo_bpm: float | None = None beat_times: list[float] = Field(default_factory=list) onset_times: list[float] = Field(default_factory=list) // WebSocket client: client.send({ type, ... }) // Audio: document.getElementById('player') — HTML5 audio element // API: apiPost(path, data), apiDelete(path) // Devices: loadDevices() populates #device-list --bg-primary: #0a0a0a; --bg-panel: #0f0f0f; --bg-panel-dark: #080808; --border-dim: #1a4a4a; --accent: #00ffff; --text-primary: #cccccc; --text-dim: #555555; --text-accent: var(--accent); --font-mono: JetBrains Mono chain Task 1: Data model changes + HTML layout restructure lightsync/models/show.py, lightsync/frontend/index.html lightsync/models/show.py, lightsync/frontend/index.html, lightsync/frontend/style.css **1. lightsync/models/show.py — Add duration field to CueModel and calibration_offset to AnalysisBlock:** ```python class CueModel(BaseModel): id: UUID = Field(default_factory=uuid4) timestamp: float duration: float = 4.0 # block length in seconds; defaults for old files mode: Literal["animation", "frame_sequence"] = "animation" animation: str | None = None params: dict[str, Any] = Field(default_factory=dict) ``` ```python class AnalysisBlock(BaseModel): analysed_at: datetime | None = None tempo_bpm: float | None = None beat_times: list[float] = Field(default_factory=list) onset_times: list[float] = Field(default_factory=list) calibration_offset: float = 0.03 # seconds; compensates librosa latency bias ``` Do NOT change schema_version (stays at 1). Duration defaults gracefully for old files via Pydantic. **2. lightsync/frontend/index.html — Replace the animation grid in main-area with timeline layout:** Replace the entire content of `
` (the `.section-header` ANIMATIONS div and the `#animation-grid` div) with this structure: ```html
``` Move the ANIMATIONS section into the sidebar. Add a second panel BELOW the existing DEVICES panel (inside `