docs(01-foundation): create phase plan — 3 plans in 2 waves
This commit is contained in:
@@ -32,7 +32,7 @@ Decimal phases appear between their surrounding integers in numeric order.
|
||||
3. The web app loads in a browser with terminal/hacker aesthetic — dark background, monospace fonts, no generic SaaS look
|
||||
4. FastAPI serves the frontend and exposes REST stubs for shows and devices
|
||||
5. Adding a new strip type requires only a new device class — no changes to core engine code
|
||||
**Plans**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 01-01: Backend scaffold — FastAPI app, Pydantic show/device models, show store (JSON load/save), device registry
|
||||
@@ -50,7 +50,7 @@ Plans:
|
||||
3. The waveform is rendered in the timeline UI from the loaded audio file
|
||||
4. Windows 11 timer resolution is set to 1ms at startup (timeBeginPeriod(1)) — confirmed via timing harness showing <2ms tick variance
|
||||
5. MPV IPC runs in a dedicated thread — pausing or seeking does not block WebSocket message processing
|
||||
**Plans**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 02-01: MPV IPC bridge — python-mpv subprocess, named pipe reader thread, shared position variable, Windows timer fix
|
||||
@@ -68,7 +68,7 @@ Plans:
|
||||
3. The animation library contains all 7 built-in types (chase, pulse, rainbow, strobe, color wipe, fire, solid color) with configurable params
|
||||
4. SK6812 (RGBW, 4-byte) and WS2801 (RGB, 3-byte) frames are encoded distinctly — a simulator can distinguish them by packet header
|
||||
5. The software simulator logs all received packets — no hardware required to validate protocol behavior
|
||||
**Plans**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 03-01: Device abstraction + UDP sender — BaseDevice ABC, SK6812 and WS2801 implementations, async UDP socket pool
|
||||
@@ -86,7 +86,7 @@ Plans:
|
||||
4. Each block has a color picker and editable parameters (speed, colors, direction, length); changes persist in the show file
|
||||
5. The playback cursor moves in real time across the timeline as audio plays, reflecting audio position accurately
|
||||
6. A beat calibration offset control shifts all beat marks backward by a configurable amount to compensate for librosa's systematic latency bias
|
||||
**Plans**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 04-01: Timeline canvas — per-device tracks, time axis, block rendering, playback cursor
|
||||
@@ -105,7 +105,7 @@ Plans:
|
||||
3. The live preview panel shows a simulated LED strip visualization in the browser canvas, updated in sync with playback
|
||||
4. All primary operations (play, pause, seek, undo, save) are accessible via keyboard shortcuts without touching the mouse
|
||||
5. A show saved during one session loads completely — all blocks, devices, and analysis data — in a subsequent session
|
||||
**Plans**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 05-01: Cue scheduler — position-driven scheduling loop, pre-fire offset, seek-safe cue pointer reset
|
||||
@@ -121,7 +121,7 @@ Plans:
|
||||
1. A YouTube URL can be loaded into the app — audio plays via MPV with no manual download step required
|
||||
2. Triggering AI sync fills the timeline with animation blocks matched to beats, drops, and song sections — producing a playable starting point for editing
|
||||
3. Structural segmentation (verse/chorus/bridge via librosa) appears as colored region overlays on the waveform
|
||||
**Plans**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 06-01: YouTube loading — yt-dlp passthrough via MPV --ytdl flag, URL input field, loading state feedback
|
||||
@@ -137,7 +137,7 @@ Plans:
|
||||
2. A microcontroller receives DRGB/DRGBW raw frame packets and outputs correct RGB/RGBW pixel values to the strip
|
||||
3. SK6812 (RGBW) and WS2801 (RGB) are both verified on physical hardware against the protocol spec from Phase 3
|
||||
4. Firmware runs on at least one of: ESP32, Pico, or Raspberry Pi 4B
|
||||
**Plans**: TBD
|
||||
**Plans**: 3 plans
|
||||
|
||||
Plans:
|
||||
- [ ] 07-01: Firmware scaffold — MicroPython UDP socket listener, packet type discriminator, device config (LED count, strip type)
|
||||
|
||||
279
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
279
.planning/phases/01-foundation/01-01-PLAN.md
Normal file
@@ -0,0 +1,279 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 01
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- pyproject.toml
|
||||
- lightsync/__init__.py
|
||||
- lightsync/__main__.py
|
||||
- lightsync/main.py
|
||||
- lightsync/models/__init__.py
|
||||
- lightsync/models/device.py
|
||||
- lightsync/models/show.py
|
||||
- lightsync/devices/__init__.py
|
||||
- lightsync/devices/base.py
|
||||
- lightsync/devices/sk6812.py
|
||||
- lightsync/devices/ws2801.py
|
||||
- lightsync/devices/registry.py
|
||||
- lightsync/store/__init__.py
|
||||
- lightsync/store/show_store.py
|
||||
- lightsync/api/__init__.py
|
||||
- lightsync/api/devices.py
|
||||
- lightsync/api/shows.py
|
||||
- lightsync/api/ws.py
|
||||
- .env
|
||||
autonomous: true
|
||||
requirements:
|
||||
- INF-01
|
||||
- INF-02
|
||||
- DEV-01
|
||||
- DEV-02
|
||||
- SHW-01
|
||||
- SHW-02
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "A DeviceConfig can be created with name, strip_type, led_count, ip, port and serialized to JSON"
|
||||
- "A ShowModel can be created with schema_version=1, audio ref, device snapshot, empty tracks and serialized to JSON"
|
||||
- "Adding a new strip type requires only a new device class file and one entry in _DEVICE_CLASSES dict"
|
||||
- "DeviceRegistry loads from and saves to devices.json — data survives process restart"
|
||||
- "ShowStore saves a ShowModel as JSON file and loads it back identically"
|
||||
- "python -m lightsync starts the FastAPI server without error"
|
||||
artifacts:
|
||||
- path: "lightsync/models/device.py"
|
||||
provides: "DeviceConfig Pydantic model"
|
||||
contains: "class DeviceConfig(BaseModel)"
|
||||
- path: "lightsync/models/show.py"
|
||||
provides: "ShowModel with schema_version, AudioRef, TrackModel, CueModel, AnalysisBlock"
|
||||
contains: "schema_version: int = 1"
|
||||
- path: "lightsync/devices/base.py"
|
||||
provides: "BaseDevice ABC with encode_frame, encode_animation_cmd, bytes_per_pixel"
|
||||
contains: "class BaseDevice(ABC)"
|
||||
- path: "lightsync/devices/registry.py"
|
||||
provides: "DeviceRegistry with load/save/add/remove/list_all"
|
||||
contains: "_DEVICE_CLASSES"
|
||||
- path: "lightsync/store/show_store.py"
|
||||
provides: "ShowStore with save/load/list_ids"
|
||||
contains: "model_dump_json"
|
||||
key_links:
|
||||
- from: "lightsync/devices/registry.py"
|
||||
to: "lightsync/devices/base.py"
|
||||
via: "_DEVICE_CLASSES maps strip_type to BaseDevice subclass"
|
||||
pattern: "_DEVICE_CLASSES.*BaseDevice"
|
||||
- from: "lightsync/devices/registry.py"
|
||||
to: "lightsync/models/device.py"
|
||||
via: "DeviceConfig used for persistence"
|
||||
pattern: "DeviceConfig.model_validate"
|
||||
- from: "lightsync/store/show_store.py"
|
||||
to: "lightsync/models/show.py"
|
||||
via: "ShowModel serialization"
|
||||
pattern: "ShowModel.model_validate_json"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the complete Python backend scaffold: package structure, Pydantic data models, device abstraction (BaseDevice ABC + SK6812/WS2801), device registry with JSON persistence, show store with JSON save/load, FastAPI app with lifespan, and WebSocket stub.
|
||||
|
||||
Purpose: Establishes every backend module, data model, and persistence mechanism that all future phases depend on. The models defined here are the contracts for the entire system.
|
||||
Output: A runnable `python -m lightsync` that starts FastAPI with route stubs, device registry, and show store.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation/01-RESEARCH.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create package structure, Pydantic models, and pyproject.toml</name>
|
||||
<files>
|
||||
pyproject.toml
|
||||
.env
|
||||
lightsync/__init__.py
|
||||
lightsync/models/__init__.py
|
||||
lightsync/models/device.py
|
||||
lightsync/models/show.py
|
||||
lightsync/devices/__init__.py
|
||||
lightsync/devices/base.py
|
||||
lightsync/devices/sk6812.py
|
||||
lightsync/devices/ws2801.py
|
||||
</files>
|
||||
<read_first>
|
||||
.planning/phases/01-foundation/01-RESEARCH.md
|
||||
.planning/phases/01-foundation/01-CONTEXT.md
|
||||
</read_first>
|
||||
<action>
|
||||
Create pyproject.toml (per D-05) with:
|
||||
- [build-system] requires = ["hatchling"], build-backend = "hatchling.build"
|
||||
- [project] name = "lightsync", version = "0.1.0", requires-python = ">=3.11"
|
||||
- dependencies: fastapi>=0.115.0, uvicorn[standard]>=0.34.0, pydantic>=2.0.0, aiofiles>=24.0.0, python-dotenv>=1.0.0, structlog>=24.0.0
|
||||
- [project.optional-dependencies] dev = ["httpx>=0.27.0"]
|
||||
|
||||
Create .env with HOST=0.0.0.0 and PORT=8000.
|
||||
|
||||
Create lightsync/__init__.py with `__version__ = "0.1.0"`.
|
||||
|
||||
Create lightsync/models/__init__.py that re-exports DeviceConfig, ShowModel.
|
||||
|
||||
Create lightsync/models/device.py (per Research Pattern 2):
|
||||
- StripType = Literal["sk6812", "ws2801", "generic"]
|
||||
- class DeviceConfig(BaseModel): id: UUID = Field(default_factory=uuid4), name: str, strip_type: StripType, led_count: int = Field(gt=0, le=1000), ip: str, port: int = Field(ge=1, le=65535), enabled: bool = True
|
||||
- model_config = {"populate_by_name": True}
|
||||
|
||||
Create lightsync/models/show.py (per Research Pattern 2):
|
||||
- class AudioRef(BaseModel): source_type: Literal["file", "youtube"] = "file", path: str | None = None, yt_url: str | None = None, duration_seconds: float | None = None
|
||||
- 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 TrackModel(BaseModel): device_id: UUID, cues: list[CueModel] = Field(default_factory=list)
|
||||
- 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)
|
||||
- class ShowModel(BaseModel): schema_version: int = 1, id: UUID = Field(default_factory=uuid4), name: str, created_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)), updated_at: datetime = Field(default_factory=lambda: datetime.now(timezone.utc)), audio: AudioRef = Field(default_factory=AudioRef), devices: list[DeviceConfig] = Field(default_factory=list), analysis: AnalysisBlock = Field(default_factory=AnalysisBlock), tracks: list[TrackModel] = Field(default_factory=list), ai_sequences: list[Any] = Field(default_factory=list)
|
||||
|
||||
Create lightsync/devices/__init__.py (empty).
|
||||
|
||||
Create lightsync/devices/base.py (per Research Pattern 3):
|
||||
- class BaseDevice(ABC) with __init__(self, config: DeviceConfig), properties id and led_count
|
||||
- @abstractmethod encode_frame(self, pixels: list[tuple]) -> bytes
|
||||
- @abstractmethod encode_animation_cmd(self, animation: str, params: dict) -> bytes
|
||||
- @property @abstractmethod bytes_per_pixel(self) -> int
|
||||
|
||||
Create lightsync/devices/sk6812.py:
|
||||
- class SK6812Device(BaseDevice) with bytes_per_pixel = 4
|
||||
- encode_frame: iterate pixels, for each (r, g, b, w) append 4 bytes to bytearray; return bytes
|
||||
- encode_animation_cmd: stub returning b"" (Phase 3 implements real encoding)
|
||||
|
||||
Create lightsync/devices/ws2801.py:
|
||||
- class WS2801Device(BaseDevice) with bytes_per_pixel = 3
|
||||
- encode_frame: iterate pixels, for each (r, g, b) append 3 bytes to bytearray; return bytes
|
||||
- encode_animation_cmd: stub returning b"" (Phase 3 implements real encoding)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/claude/led2 && python -c "from lightsync.models.device import DeviceConfig; d = DeviceConfig(name='test', strip_type='sk6812', led_count=60, ip='192.168.1.10', port=21324); print(d.model_dump_json())" && python -c "from lightsync.models.show import ShowModel; s = ShowModel(name='test'); print(s.schema_version); assert s.schema_version == 1; print(s.model_dump_json()[:100])" && python -c "from lightsync.devices.sk6812 import SK6812Device; from lightsync.models.device import DeviceConfig; d = SK6812Device(DeviceConfig(name='t', strip_type='sk6812', led_count=1, ip='0', port=1)); assert d.bytes_per_pixel == 4; print('OK')"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- pyproject.toml contains `name = "lightsync"` and `fastapi>=0.115.0` and `aiofiles>=24.0.0`
|
||||
- lightsync/models/device.py contains `class DeviceConfig(BaseModel)` and `StripType = Literal["sk6812", "ws2801", "generic"]`
|
||||
- lightsync/models/show.py contains `schema_version: int = 1` and `class ShowModel(BaseModel)` and `class AudioRef(BaseModel)` and `class TrackModel(BaseModel)` and `class CueModel(BaseModel)` and `class AnalysisBlock(BaseModel)`
|
||||
- lightsync/devices/base.py contains `class BaseDevice(ABC)` and `def encode_frame` and `def encode_animation_cmd` and `def bytes_per_pixel`
|
||||
- lightsync/devices/sk6812.py contains `class SK6812Device(BaseDevice)` and `bytes_per_pixel` returning 4
|
||||
- lightsync/devices/ws2801.py contains `class WS2801Device(BaseDevice)` and `bytes_per_pixel` returning 3
|
||||
- `python -c "from lightsync.models.show import ShowModel; s = ShowModel(name='x'); assert s.schema_version == 1"` exits 0
|
||||
</acceptance_criteria>
|
||||
<done>All Pydantic models serialize/deserialize correctly. BaseDevice ABC exists with two concrete implementations. pyproject.toml lists all Phase 1 dependencies.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create device registry, show store, FastAPI app with lifespan, API route stubs, and WebSocket stub</name>
|
||||
<files>
|
||||
lightsync/devices/registry.py
|
||||
lightsync/store/__init__.py
|
||||
lightsync/store/show_store.py
|
||||
lightsync/api/__init__.py
|
||||
lightsync/api/devices.py
|
||||
lightsync/api/shows.py
|
||||
lightsync/api/ws.py
|
||||
lightsync/main.py
|
||||
lightsync/__main__.py
|
||||
</files>
|
||||
<read_first>
|
||||
.planning/phases/01-foundation/01-RESEARCH.md
|
||||
lightsync/models/device.py
|
||||
lightsync/models/show.py
|
||||
lightsync/devices/base.py
|
||||
</read_first>
|
||||
<action>
|
||||
Create lightsync/devices/registry.py (per Research Pattern 3):
|
||||
- _DEVICE_CLASSES: dict[str, type[BaseDevice]] = {"sk6812": SK6812Device, "ws2801": WS2801Device}
|
||||
- class DeviceRegistry with __init__(self, path: Path), _devices: dict[str, DeviceConfig]
|
||||
- async load(): read JSON from self._path, parse each entry via DeviceConfig.model_validate
|
||||
- async save(): serialize all devices via model_dump(mode="json"), write JSON with indent=2
|
||||
- add(config: DeviceConfig), remove(device_id: str) -> bool, get(device_id: str) -> DeviceConfig | None, list_all() -> list[DeviceConfig]
|
||||
- instantiate(device_id: str) -> BaseDevice | None: lookup config, lookup class in _DEVICE_CLASSES, return instance
|
||||
|
||||
Create lightsync/store/__init__.py (empty).
|
||||
|
||||
Create lightsync/store/show_store.py (per Research Pattern 4):
|
||||
- class ShowStore with __init__(self, shows_dir: Path)
|
||||
- ensure_dir(): self._dir.mkdir(parents=True, exist_ok=True)
|
||||
- _path_for(show_id: str) -> Path: return self._dir / f"{show_id}.json"
|
||||
- async save(show: ShowModel): write show.model_dump_json(indent=2) to _path_for(str(show.id))
|
||||
- async load(show_id: str) -> ShowModel | None: read file, return ShowModel.model_validate_json(content)
|
||||
- list_ids() -> list[str]: return [p.stem for p in self._dir.glob("*.json")]
|
||||
|
||||
Create lightsync/api/__init__.py (empty).
|
||||
|
||||
Create lightsync/api/devices.py (per Research Pattern 5):
|
||||
- router = APIRouter(tags=["devices"])
|
||||
- GET / -> list[DeviceConfig]: return state.registry.list_all()
|
||||
- POST / status_code=201 -> DeviceConfig: registry.add(config), await registry.save(), return config
|
||||
- DELETE /{device_id} status_code=204: registry.remove or raise HTTPException(404)
|
||||
- Import registry via `import lightsync.main as state` and access `state.registry`
|
||||
|
||||
Create lightsync/api/shows.py:
|
||||
- router = APIRouter(tags=["shows"])
|
||||
- GET / -> list of show summaries: iterate show_store.list_ids(), load each, return list of {id, name, created_at}
|
||||
- POST / status_code=201: accept ShowModel body, save via show_store, return show
|
||||
- GET /{show_id}: load from show_store or raise 404
|
||||
- Import show_store via `import lightsync.main as state` and access `state.show_store`
|
||||
|
||||
Create lightsync/api/ws.py (per Research Pattern 6):
|
||||
- class ConnectionManager with active_connections list, connect/disconnect/broadcast methods
|
||||
- module-level `manager = ConnectionManager()`
|
||||
- @router.websocket("/ws"): accept, loop receive_text, echo back {"type": "ack", "echo": msg}
|
||||
- Handle WebSocketDisconnect with manager.disconnect
|
||||
|
||||
Create lightsync/main.py (per Research Pattern 1):
|
||||
- Module-level: registry: DeviceRegistry | None = None, show_store: ShowStore | None = None
|
||||
- @asynccontextmanager async def lifespan(app): initialize registry from Path("devices.json"), await registry.load(), initialize show_store from Path("shows"), show_store.ensure_dir(), yield, await registry.save()
|
||||
- def create_app() -> FastAPI: create app with title="LightSync" and lifespan, include_router for shows (prefix="/api/shows"), devices (prefix="/api/devices"), ws; mount StaticFiles LAST at "/" with directory=Path(__file__).parent / "frontend" and html=True
|
||||
- app = create_app()
|
||||
|
||||
Create lightsync/__main__.py (per Research Pattern 1):
|
||||
- if __name__ == "__main__": load_dotenv(), uvicorn.run("lightsync.main:app", host from env default "0.0.0.0", port from env default 8000, reload=False)
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/claude/led2 && pip install -e ".[dev]" 2>&1 | tail -3 && python -c "from lightsync.devices.registry import DeviceRegistry; from lightsync.store.show_store import ShowStore; print('imports OK')" && timeout 5 python -m lightsync 2>&1 || true</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- lightsync/devices/registry.py contains `_DEVICE_CLASSES` and `class DeviceRegistry` and `async def load` and `async def save` and `def instantiate`
|
||||
- lightsync/store/show_store.py contains `class ShowStore` and `model_dump_json` and `model_validate_json` and `def list_ids`
|
||||
- lightsync/api/devices.py contains `router = APIRouter` and `async def list_devices` and `async def add_device` and `async def remove_device`
|
||||
- lightsync/api/shows.py contains `router = APIRouter` and `/api/shows` routes for GET and POST
|
||||
- lightsync/api/ws.py contains `class ConnectionManager` and `@router.websocket("/ws")`
|
||||
- lightsync/main.py contains `async def lifespan` and `StaticFiles` and `include_router` and routes registered BEFORE StaticFiles mount
|
||||
- lightsync/__main__.py contains `if __name__ == "__main__"` and `uvicorn.run`
|
||||
- `pip install -e .` completes without error
|
||||
- `python -m lightsync` starts and listens on port 8000 (may fail on missing frontend dir — that is OK, plan 01-02 creates it)
|
||||
</acceptance_criteria>
|
||||
<done>FastAPI app starts via `python -m lightsync`. Device registry persists to devices.json. Show store saves/loads JSON files. All API route stubs respond. WebSocket stub echoes messages.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `pip install -e ".[dev]"` succeeds
|
||||
2. `python -c "from lightsync.models.show import ShowModel; s = ShowModel(name='test'); assert s.schema_version == 1"` passes
|
||||
3. `python -c "from lightsync.devices.registry import DeviceRegistry; print('OK')"` passes
|
||||
4. `python -m lightsync` starts without import errors (may warn about missing frontend/ directory)
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- All Python modules under lightsync/ import without error
|
||||
- Pydantic models serialize to and deserialize from JSON correctly
|
||||
- DeviceRegistry round-trips through devices.json
|
||||
- ShowStore round-trips through shows/ directory
|
||||
- FastAPI app starts and registers all route prefixes
|
||||
- Adding a new strip type requires only a new file in devices/ and one dict entry
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-01-SUMMARY.md`
|
||||
</output>
|
||||
251
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
251
.planning/phases/01-foundation/01-02-PLAN.md
Normal file
@@ -0,0 +1,251 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 02
|
||||
type: execute
|
||||
wave: 1
|
||||
depends_on: []
|
||||
files_modified:
|
||||
- lightsync/frontend/index.html
|
||||
- lightsync/frontend/style.css
|
||||
- lightsync/frontend/app.js
|
||||
autonomous: true
|
||||
requirements:
|
||||
- UI-01
|
||||
- UI-02
|
||||
- DEV-03
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "The web app loads in a browser showing the full DAW skeleton layout"
|
||||
- "The UI has terminal/hacker aesthetic — dark background, monospace fonts, cyan accents, no generic SaaS look"
|
||||
- "All final panels are visible: DEVICES, ANIMATIONS, TIMELINE, TRANSPORT — most as labeled placeholders"
|
||||
- "The DEVICES panel has an add-device form and a device list area"
|
||||
- "Panel headers are uppercase and borders are dimmed cyan — matching terminal window pane style"
|
||||
artifacts:
|
||||
- path: "lightsync/frontend/index.html"
|
||||
provides: "Full DAW skeleton HTML structure with all panels"
|
||||
contains: "LIGHTSYNC"
|
||||
- path: "lightsync/frontend/style.css"
|
||||
provides: "Terminal aesthetic CSS — dark theme, monospace, cyan accents"
|
||||
contains: "--accent"
|
||||
- path: "lightsync/frontend/app.js"
|
||||
provides: "WebSocket client stub, device panel interactions, API fetch helpers"
|
||||
contains: "new WebSocket"
|
||||
key_links:
|
||||
- from: "lightsync/frontend/app.js"
|
||||
to: "/ws"
|
||||
via: "WebSocket connection for future position broadcasts"
|
||||
pattern: "new WebSocket.*ws"
|
||||
- from: "lightsync/frontend/app.js"
|
||||
to: "/api/devices"
|
||||
via: "fetch calls for device CRUD"
|
||||
pattern: "fetch.*api/devices"
|
||||
- from: "lightsync/frontend/index.html"
|
||||
to: "lightsync/frontend/style.css"
|
||||
via: "stylesheet link"
|
||||
pattern: "style.css"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Build the complete terminal-aesthetic frontend shell: HTML with full DAW skeleton layout (all panels), CSS with dark theme / monospace / cyan accents, and JavaScript with WebSocket stub and device panel interactivity.
|
||||
|
||||
Purpose: Establishes the visual identity and layout structure that all future phases will fill in. The user sees the final app skeleton from day one.
|
||||
Output: Three files in lightsync/frontend/ — index.html, style.css, app.js — that render the full DAW interface.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation/01-RESEARCH.md
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Create HTML structure and CSS terminal aesthetic</name>
|
||||
<files>
|
||||
lightsync/frontend/index.html
|
||||
lightsync/frontend/style.css
|
||||
</files>
|
||||
<read_first>
|
||||
.planning/phases/01-foundation/01-CONTEXT.md
|
||||
.planning/phases/01-foundation/01-RESEARCH.md
|
||||
</read_first>
|
||||
<action>
|
||||
Create lightsync/frontend/ directory.
|
||||
|
||||
Create lightsync/frontend/index.html with this structure (per D-06 through D-10):
|
||||
|
||||
DOCTYPE html, lang="en", meta charset UTF-8, viewport meta, title "LIGHTSYNC".
|
||||
Link to style.css, defer script app.js.
|
||||
|
||||
Body layout using CSS Grid (per D-06 — full DAW skeleton from Phase 1):
|
||||
|
||||
HEADER (per D-07):
|
||||
- div.header containing:
|
||||
- ASCII art or stylized text "LIGHTSYNC" in a pre or span element (use simple ASCII block letters or styled text — not an image)
|
||||
- span.status-indicator with id="status" text "OFFLINE" (will toggle to "CONNECTED" via JS)
|
||||
- span.show-name with id="show-name" text "No show loaded"
|
||||
|
||||
LEFT SIDEBAR (per D-08):
|
||||
- div.sidebar containing two panels stacked vertically:
|
||||
- section.panel#devices-panel:
|
||||
- h2.panel-header text "DEVICES"
|
||||
- div#device-list (empty, populated by JS)
|
||||
- form#add-device-form with inputs: name (text), strip_type (select: sk6812, ws2801, generic), led_count (number, min=1, max=1000), ip (text), port (number, min=1, max=65535), and a submit button text "+ ADD DEVICE"
|
||||
- section.panel#animations-panel:
|
||||
- h2.panel-header text "ANIMATIONS"
|
||||
- div.placeholder-content text "[ Phase 3+ ]"
|
||||
|
||||
MAIN CENTER (per D-09):
|
||||
- div.main containing:
|
||||
- section.panel#timeline-panel:
|
||||
- h2.panel-header text "TIMELINE"
|
||||
- div.placeholder-content text "[ TIMELINE -- Phase 2+ ]"
|
||||
|
||||
BOTTOM BAR (per D-10):
|
||||
- div.transport containing:
|
||||
- section.panel#transport-panel:
|
||||
- h2.panel-header text "TRANSPORT"
|
||||
- div.placeholder-content text "[ TRANSPORT -- Phase 2+ ]"
|
||||
|
||||
Create lightsync/frontend/style.css with terminal aesthetic (per D-11 through D-15):
|
||||
|
||||
CSS custom properties on :root:
|
||||
- --bg: #0a0a0a (per D-12)
|
||||
- --bg-panel: #111111
|
||||
- --accent: #00ffff (per D-11)
|
||||
- --accent-dim: #004444 (dimmed cyan for borders, per D-13)
|
||||
- --text: #cccccc
|
||||
- --text-bright: #ffffff
|
||||
- --text-dim: #666666
|
||||
- --font-mono: 'JetBrains Mono', 'Fira Code', 'Cascadia Code', 'Consolas', monospace (per D-12)
|
||||
|
||||
@import url for JetBrains Mono from Google Fonts (weight 400,700).
|
||||
|
||||
*, *::before, *::after: box-sizing border-box, margin 0, padding 0.
|
||||
html, body: height 100%, background var(--bg), color var(--text), font-family var(--font-mono), font-size 14px, overflow hidden.
|
||||
|
||||
Body grid layout: grid-template-areas "header header header" / "sidebar main main" / "sidebar main main" / "transport transport transport". grid-template-columns: 280px 1fr. grid-template-rows: auto 1fr auto.
|
||||
|
||||
.header: grid-area header, display flex, align-items center, gap 1rem, padding 8px 16px, border-bottom 1px solid var(--accent-dim), background var(--bg-panel).
|
||||
.header pre (for ASCII title): color var(--accent), font-size 10px, line-height 1.1, white-space pre.
|
||||
.status-indicator: font-size 11px, text-transform uppercase, padding 2px 8px, border 1px solid var(--accent-dim). When connected: color var(--accent). Default (offline): color #ff4444.
|
||||
.show-name: margin-left auto, font-size 12px, color var(--text-dim).
|
||||
|
||||
.sidebar: grid-area sidebar, display flex, flex-direction column, gap 0, border-right 1px solid var(--accent-dim), overflow-y auto.
|
||||
.main: grid-area main, display flex, flex-direction column, overflow hidden, padding 8px.
|
||||
.transport: grid-area transport, border-top 1px solid var(--accent-dim).
|
||||
|
||||
.panel: background var(--bg-panel), border 1px solid var(--accent-dim) (per D-13), padding 0, margin 0. No border-radius (per D-15). No box-shadow (per D-15).
|
||||
.panel-header: text-transform uppercase (per D-14), font-size 11px, letter-spacing 2px, color var(--accent), padding 8px 12px, border-bottom 1px solid var(--accent-dim), font-weight 700.
|
||||
|
||||
.placeholder-content: display flex, align-items center, justify-content center, min-height 200px, color var(--text-dim), font-size 13px, letter-spacing 1px.
|
||||
|
||||
#devices-panel: flex 1.
|
||||
#animations-panel: flex 0 0 auto, border-top 1px solid var(--accent-dim).
|
||||
#timeline-panel: flex 1, display flex, flex-direction column. The .placeholder-content inside: flex 1.
|
||||
#transport-panel: padding 0.
|
||||
#transport-panel .placeholder-content: min-height 48px.
|
||||
|
||||
#device-list: padding 8px 12px. Each .device-item: display flex, justify-content space-between, align-items center, padding 4px 0, border-bottom 1px solid var(--accent-dim), font-size 12px. .device-item .device-name: color var(--text-bright). .device-item .device-meta: color var(--text-dim), font-size 11px. .device-item .device-remove: background none, border none, color #ff4444, cursor pointer, font-family var(--font-mono), font-size 14px.
|
||||
|
||||
#add-device-form: padding 8px 12px, display flex, flex-direction column, gap 6px.
|
||||
#add-device-form input, #add-device-form select: background var(--bg), border 1px solid var(--accent-dim), color var(--text), font-family var(--font-mono), font-size 12px, padding 4px 8px, outline none. Focus: border-color var(--accent).
|
||||
#add-device-form button: background var(--accent-dim), border 1px solid var(--accent), color var(--accent), font-family var(--font-mono), font-size 12px, padding 6px, cursor pointer, text-transform uppercase, letter-spacing 1px. Hover: background var(--accent), color var(--bg).
|
||||
|
||||
Scrollbar styling for webkit: 6px wide, track var(--bg), thumb var(--accent-dim), thumb hover var(--accent).
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f /home/claude/led2/lightsync/frontend/index.html && test -f /home/claude/led2/lightsync/frontend/style.css && grep -c "LIGHTSYNC" /home/claude/led2/lightsync/frontend/index.html && grep -c "\-\-accent.*#00ffff" /home/claude/led2/lightsync/frontend/style.css && grep -c "JetBrains Mono" /home/claude/led2/lightsync/frontend/style.css</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- lightsync/frontend/index.html contains "LIGHTSYNC" in header area
|
||||
- lightsync/frontend/index.html contains id="devices-panel" and id="animations-panel" and id="timeline-panel" and id="transport-panel"
|
||||
- lightsync/frontend/index.html contains id="add-device-form" with inputs for name, strip_type, led_count, ip, port
|
||||
- lightsync/frontend/index.html contains id="device-list"
|
||||
- lightsync/frontend/index.html contains "Phase 3+" in animations panel and "Phase 2+" in timeline panel and "Phase 2+" in transport panel
|
||||
- lightsync/frontend/style.css contains `--accent: #00ffff` and `--bg: #0a0a0a` and `--font-mono`
|
||||
- lightsync/frontend/style.css contains `text-transform: uppercase` for panel headers
|
||||
- lightsync/frontend/style.css contains `border-radius` nowhere (per D-15 no rounded corners) OR only `border-radius: 0`
|
||||
- lightsync/frontend/style.css contains `font-family: 'JetBrains Mono'` or references var(--font-mono) on body
|
||||
- lightsync/frontend/style.css contains grid-template-areas with "header" and "sidebar" and "main" and "transport"
|
||||
</acceptance_criteria>
|
||||
<done>HTML renders full DAW skeleton. CSS applies terminal aesthetic with dark background, monospace font, cyan accents, flat sharp edges. All panels visible with correct labels.</done>
|
||||
</task>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 2: Create JavaScript — WebSocket client, device panel CRUD, API helpers</name>
|
||||
<files>
|
||||
lightsync/frontend/app.js
|
||||
</files>
|
||||
<read_first>
|
||||
lightsync/frontend/index.html
|
||||
.planning/phases/01-foundation/01-RESEARCH.md
|
||||
</read_first>
|
||||
<action>
|
||||
Create lightsync/frontend/app.js with these modules:
|
||||
|
||||
1. API helper functions:
|
||||
- async function apiGet(path): fetch(path), check response.ok, return response.json()
|
||||
- async function apiPost(path, data): fetch(path, {method: "POST", headers: {"Content-Type": "application/json"}, body: JSON.stringify(data)}), return response.json()
|
||||
- async function apiDelete(path): fetch(path, {method: "DELETE"})
|
||||
|
||||
2. WebSocket client stub:
|
||||
- let ws = null
|
||||
- function connectWebSocket(): create new WebSocket(`ws://${location.host}/ws`), set onopen to update #status text to "CONNECTED" and add .connected class, set onclose to update #status text to "OFFLINE" and remove .connected class, set onmessage to parse JSON and call handleWsMessage(data), set onerror to console.error. Auto-reconnect: onclose sets setTimeout(connectWebSocket, 2000).
|
||||
- function handleWsMessage(msg): switch on msg.type — for now just console.log("WS:", msg). Phase 2 will add position/state handlers.
|
||||
|
||||
3. Device panel logic:
|
||||
- async function loadDevices(): call apiGet("/api/devices"), for each device call renderDevice(device), populate #device-list
|
||||
- function renderDevice(device): create div.device-item containing: span.device-name with device.name, span.device-meta with `${device.strip_type} | ${device.led_count} LEDs | ${device.ip}:${device.port}`, button.device-remove with text "x" and onclick calling removeDevice(device.id)
|
||||
- async function removeDevice(id): call apiDelete(`/api/devices/${id}`), then loadDevices() to refresh
|
||||
- Form submit handler on #add-device-form: preventDefault, collect form values (name, strip_type, led_count as int, ip, port as int), call apiPost("/api/devices", data), reset form, call loadDevices()
|
||||
|
||||
4. Initialization:
|
||||
- document.addEventListener("DOMContentLoaded", () => { connectWebSocket(); loadDevices(); })
|
||||
|
||||
Add .connected CSS class to style.css for the status indicator: .status-indicator.connected { color: var(--accent); border-color: var(--accent); }
|
||||
</action>
|
||||
<verify>
|
||||
<automated>test -f /home/claude/led2/lightsync/frontend/app.js && grep -c "new WebSocket" /home/claude/led2/lightsync/frontend/app.js && grep -c "api/devices" /home/claude/led2/lightsync/frontend/app.js && grep -c "loadDevices" /home/claude/led2/lightsync/frontend/app.js && grep -c "connectWebSocket" /home/claude/led2/lightsync/frontend/app.js</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- lightsync/frontend/app.js contains `new WebSocket` connecting to `/ws`
|
||||
- lightsync/frontend/app.js contains `fetch` calls to `/api/devices` for GET, POST, and DELETE
|
||||
- lightsync/frontend/app.js contains `function loadDevices` and `function renderDevice` and `function removeDevice`
|
||||
- lightsync/frontend/app.js contains `DOMContentLoaded` listener calling `connectWebSocket()` and `loadDevices()`
|
||||
- lightsync/frontend/app.js contains `function handleWsMessage` with msg.type switch/check
|
||||
- lightsync/frontend/app.js contains auto-reconnect logic (setTimeout with connectWebSocket)
|
||||
- lightsync/frontend/app.js contains `apiGet` and `apiPost` and `apiDelete` helper functions
|
||||
</acceptance_criteria>
|
||||
<done>JavaScript connects WebSocket, displays connection status, loads device list from API, supports adding and removing devices via the form. All interactions use the API helpers.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. `ls lightsync/frontend/` shows index.html, style.css, app.js
|
||||
2. index.html contains all four panel IDs: devices-panel, animations-panel, timeline-panel, transport-panel
|
||||
3. style.css uses --accent: #00ffff and --bg: #0a0a0a
|
||||
4. app.js connects to /ws and fetches from /api/devices
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Opening the app in a browser shows: header with LIGHTSYNC title, left sidebar with DEVICES and ANIMATIONS panels, center TIMELINE placeholder, bottom TRANSPORT placeholder
|
||||
- Dark background (#0a0a0a), monospace fonts, cyan accent color throughout
|
||||
- No rounded corners, drop shadows, or gradients anywhere
|
||||
- Device add form is functional (sends POST to /api/devices)
|
||||
- WebSocket connects and shows CONNECTED status
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-02-SUMMARY.md`
|
||||
</output>
|
||||
254
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
254
.planning/phases/01-foundation/01-03-PLAN.md
Normal file
@@ -0,0 +1,254 @@
|
||||
---
|
||||
phase: 01-foundation
|
||||
plan: 03
|
||||
type: execute
|
||||
wave: 2
|
||||
depends_on:
|
||||
- 01-01
|
||||
- 01-02
|
||||
files_modified:
|
||||
- lightsync/api/devices.py
|
||||
- lightsync/api/shows.py
|
||||
- lightsync/main.py
|
||||
- docker-compose.prod.yml
|
||||
- Dockerfile
|
||||
autonomous: true
|
||||
requirements:
|
||||
- INF-01
|
||||
- DEV-01
|
||||
- DEV-02
|
||||
- DEV-03
|
||||
- SHW-01
|
||||
- SHW-02
|
||||
- UI-01
|
||||
|
||||
must_haves:
|
||||
truths:
|
||||
- "POST /api/devices creates a device that appears in GET /api/devices and persists after restart"
|
||||
- "DELETE /api/devices/{id} removes the device from the registry and from devices.json"
|
||||
- "POST /api/shows creates a show file with schema_version=1 in the shows/ directory"
|
||||
- "GET /api/shows/{id} returns the full show JSON including audio, devices, tracks fields"
|
||||
- "Opening http://localhost:8000 serves index.html with working device CRUD through the UI"
|
||||
- "The app runs inside Docker behind Traefik on lightsync.groll.cloud"
|
||||
artifacts:
|
||||
- path: "Dockerfile"
|
||||
provides: "Python alpine container running lightsync"
|
||||
contains: "python -m lightsync"
|
||||
- path: "docker-compose.prod.yml"
|
||||
provides: "Traefik-labeled service for lightsync.groll.cloud"
|
||||
contains: "lightsync.groll.cloud"
|
||||
key_links:
|
||||
- from: "lightsync/frontend/app.js"
|
||||
to: "lightsync/api/devices.py"
|
||||
via: "fetch /api/devices — full CRUD round-trip"
|
||||
pattern: "api/devices"
|
||||
- from: "lightsync/api/shows.py"
|
||||
to: "lightsync/store/show_store.py"
|
||||
via: "ShowStore save/load calls"
|
||||
pattern: "show_store"
|
||||
- from: "lightsync/api/devices.py"
|
||||
to: "lightsync/devices/registry.py"
|
||||
via: "DeviceRegistry CRUD + persist"
|
||||
pattern: "registry"
|
||||
---
|
||||
|
||||
<objective>
|
||||
Wire everything together: verify REST API endpoints work end-to-end with the frontend, add Docker deployment for groll.cloud, and run an integration smoke test confirming device CRUD and show save/load work through the full stack.
|
||||
|
||||
Purpose: Proves the foundation actually works as a system — not just individual modules. Deploys to production so the user can see it live.
|
||||
Output: Working app at lightsync.groll.cloud with device CRUD and show save/load functional.
|
||||
</objective>
|
||||
|
||||
<execution_context>
|
||||
@$HOME/.claude/get-shit-done/workflows/execute-plan.md
|
||||
@$HOME/.claude/get-shit-done/templates/summary.md
|
||||
</execution_context>
|
||||
|
||||
<context>
|
||||
@.planning/PROJECT.md
|
||||
@.planning/ROADMAP.md
|
||||
@.planning/STATE.md
|
||||
@.planning/phases/01-foundation/01-CONTEXT.md
|
||||
@.planning/phases/01-foundation/01-01-SUMMARY.md
|
||||
@.planning/phases/01-foundation/01-02-SUMMARY.md
|
||||
|
||||
<interfaces>
|
||||
<!-- Key types and contracts from Plan 01 that this plan wires together -->
|
||||
|
||||
From lightsync/models/device.py:
|
||||
```python
|
||||
StripType = Literal["sk6812", "ws2801", "generic"]
|
||||
class DeviceConfig(BaseModel):
|
||||
id: UUID, name: str, strip_type: StripType, led_count: int, ip: str, port: int, enabled: bool
|
||||
```
|
||||
|
||||
From lightsync/models/show.py:
|
||||
```python
|
||||
class ShowModel(BaseModel):
|
||||
schema_version: int = 1, id: UUID, name: str, created_at: datetime, updated_at: datetime,
|
||||
audio: AudioRef, devices: list[DeviceConfig], analysis: AnalysisBlock, tracks: list[TrackModel]
|
||||
```
|
||||
|
||||
From lightsync/devices/registry.py:
|
||||
```python
|
||||
class DeviceRegistry:
|
||||
async def load(), async def save(), def add(config), def remove(device_id) -> bool,
|
||||
def get(device_id), def list_all() -> list[DeviceConfig]
|
||||
```
|
||||
|
||||
From lightsync/store/show_store.py:
|
||||
```python
|
||||
class ShowStore:
|
||||
def ensure_dir(), async def save(show), async def load(show_id) -> ShowModel | None, def list_ids() -> list[str]
|
||||
```
|
||||
</interfaces>
|
||||
</context>
|
||||
|
||||
<tasks>
|
||||
|
||||
<task type="auto">
|
||||
<name>Task 1: Integration test — verify API endpoints, fix any wiring issues, add Docker deployment</name>
|
||||
<files>
|
||||
lightsync/api/devices.py
|
||||
lightsync/api/shows.py
|
||||
lightsync/main.py
|
||||
Dockerfile
|
||||
docker-compose.prod.yml
|
||||
</files>
|
||||
<read_first>
|
||||
lightsync/main.py
|
||||
lightsync/api/devices.py
|
||||
lightsync/api/shows.py
|
||||
lightsync/api/ws.py
|
||||
lightsync/devices/registry.py
|
||||
lightsync/store/show_store.py
|
||||
lightsync/frontend/app.js
|
||||
lightsync/frontend/index.html
|
||||
</read_first>
|
||||
<action>
|
||||
STEP 1 — Install and start the app:
|
||||
- pip install -e ".[dev]"
|
||||
- Start `python -m lightsync` in background
|
||||
- Wait for server ready
|
||||
|
||||
STEP 2 — Smoke test device CRUD via curl:
|
||||
- POST /api/devices with body {"name": "Test Strip", "strip_type": "sk6812", "led_count": 60, "ip": "192.168.1.10", "port": 21324} — expect 201 with id field
|
||||
- GET /api/devices — expect array containing the created device
|
||||
- DELETE /api/devices/{id} — expect 204
|
||||
- GET /api/devices — expect empty array
|
||||
- Stop and restart the app, POST a device, stop, restart, GET — verify device persists (DEV-02)
|
||||
|
||||
STEP 3 — Smoke test show CRUD via curl:
|
||||
- POST /api/shows with body {"name": "Test Show"} — expect 201 with schema_version=1
|
||||
- GET /api/shows — expect array with show summary
|
||||
- GET /api/shows/{id} — expect full show JSON with fields: schema_version, id, name, audio, devices, tracks
|
||||
- Verify JSON file exists in shows/ directory
|
||||
|
||||
STEP 4 — Fix any issues found during smoke test:
|
||||
- If any endpoint returns unexpected status codes or missing fields, fix the route handler
|
||||
- If the shows API list endpoint is too slow (loading all JSON files), add a lightweight list that returns only {id, name} without full deserialization
|
||||
- Ensure the shows POST endpoint generates a UUID if the client doesn't provide one, and sets created_at/updated_at
|
||||
- Ensure devices POST endpoint generates a UUID if the client doesn't provide one
|
||||
|
||||
STEP 5 — Verify frontend integration:
|
||||
- GET / returns index.html (StaticFiles mount working)
|
||||
- Verify style.css and app.js are served at /style.css and /app.js
|
||||
|
||||
STEP 6 — Create Dockerfile:
|
||||
- FROM python:3.11-alpine
|
||||
- WORKDIR /app
|
||||
- COPY pyproject.toml .
|
||||
- RUN pip install --no-cache-dir .
|
||||
- COPY lightsync/ lightsync/
|
||||
- EXPOSE 8000
|
||||
- CMD ["python", "-m", "lightsync"]
|
||||
- Note: Use alpine to save RAM per CLAUDE.md constraint
|
||||
|
||||
STEP 7 — Create docker-compose.prod.yml:
|
||||
- service name: lightsync
|
||||
- build: .
|
||||
- container_name: lightsync
|
||||
- restart: unless-stopped
|
||||
- volumes: ./shows:/app/shows, ./devices.json:/app/devices.json
|
||||
- networks: edge (external: true)
|
||||
- labels for Traefik:
|
||||
- traefik.enable=true
|
||||
- traefik.http.routers.lightsync.rule=Host(`lightsync.groll.cloud`)
|
||||
- traefik.http.routers.lightsync.entrypoints=websecure
|
||||
- traefik.http.routers.lightsync.tls.certresolver=letsencrypt
|
||||
- traefik.http.routers.lightsync.middlewares=authelia@docker
|
||||
- traefik.http.services.lightsync.loadbalancer.server.port=8000
|
||||
- environment: HOST=0.0.0.0, PORT=8000
|
||||
|
||||
STEP 8 — Deploy:
|
||||
- Run ~/bin/deploy.sh /home/claude/led2
|
||||
- Verify container starts successfully
|
||||
|
||||
STEP 9 — Update ~/start/projects.json:
|
||||
- Add entry: name "LightSync", url "https://lightsync.groll.cloud", icon "zap", badge "NEW", badgeColor "green", protected true, description "LED Light Show Manager", section "project"
|
||||
- Run ~/bin/deploy.sh ~/start
|
||||
</action>
|
||||
<verify>
|
||||
<automated>cd /home/claude/led2 && pip install -e ".[dev]" 2>&1 | tail -1 && python -c "
|
||||
import httpx, asyncio, json
|
||||
from lightsync.main import app
|
||||
async def test():
|
||||
async with httpx.AsyncClient(app=app, base_url='http://test') as c:
|
||||
# Device CRUD
|
||||
r = await c.post('/api/devices', json={'name':'Smoke','strip_type':'sk6812','led_count':30,'ip':'10.0.0.1','port':21324})
|
||||
assert r.status_code == 201, f'POST devices: {r.status_code}'
|
||||
did = r.json()['id']
|
||||
r = await c.get('/api/devices')
|
||||
assert r.status_code == 200 and len(r.json()) >= 1, 'GET devices failed'
|
||||
# Show CRUD
|
||||
r = await c.post('/api/shows', json={'name':'Smoke Show'})
|
||||
assert r.status_code == 201, f'POST shows: {r.status_code}'
|
||||
sid = r.json()['id']
|
||||
assert r.json()['schema_version'] == 1, 'Missing schema_version'
|
||||
r = await c.get(f'/api/shows/{sid}')
|
||||
assert r.status_code == 200, f'GET show: {r.status_code}'
|
||||
body = r.json()
|
||||
assert 'audio' in body and 'tracks' in body and 'devices' in body, f'Missing fields: {list(body.keys())}'
|
||||
# Static files
|
||||
r = await c.get('/')
|
||||
assert r.status_code == 200, f'GET /: {r.status_code}'
|
||||
print('ALL SMOKE TESTS PASSED')
|
||||
asyncio.run(test())
|
||||
"</automated>
|
||||
</verify>
|
||||
<acceptance_criteria>
|
||||
- POST /api/devices returns 201 with valid JSON containing id, name, strip_type, led_count, ip, port
|
||||
- GET /api/devices returns 200 with array of DeviceConfig objects
|
||||
- DELETE /api/devices/{id} returns 204 for existing device and 404 for missing
|
||||
- POST /api/shows returns 201 with JSON containing schema_version=1
|
||||
- GET /api/shows returns 200 with array of show summaries
|
||||
- GET /api/shows/{id} returns 200 with full ShowModel JSON including audio, devices, tracks, analysis fields
|
||||
- GET / returns 200 with HTML containing "LIGHTSYNC"
|
||||
- Dockerfile contains `FROM python:3.11-alpine` and `CMD ["python", "-m", "lightsync"]`
|
||||
- docker-compose.prod.yml contains `lightsync.groll.cloud` and `authelia@docker` and network `edge`
|
||||
- Container runs successfully via `docker compose -f docker-compose.prod.yml ps` showing status "Up"
|
||||
</acceptance_criteria>
|
||||
<done>All API endpoints respond correctly. Device persistence survives restart. Show files save as JSON with schema_version=1. Frontend loads from StaticFiles. App deployed to lightsync.groll.cloud behind Authelia.</done>
|
||||
</task>
|
||||
|
||||
</tasks>
|
||||
|
||||
<verification>
|
||||
1. httpx smoke test (in verify above) passes all assertions
|
||||
2. `docker compose -f docker-compose.prod.yml ps` shows lightsync as running
|
||||
3. `curl -s http://localhost:8000/api/devices` returns JSON array
|
||||
4. `curl -s http://localhost:8000/` contains "LIGHTSYNC"
|
||||
5. `ls shows/` shows at least one .json file after creating a show
|
||||
</verification>
|
||||
|
||||
<success_criteria>
|
||||
- Complete device CRUD works: add device via POST, see it in GET, remove via DELETE, persists across restarts
|
||||
- Complete show CRUD works: create show via POST with schema_version=1, list shows, load individual show with all fields
|
||||
- Frontend serves from FastAPI StaticFiles at /
|
||||
- App runs in Docker on lightsync.groll.cloud behind Authelia
|
||||
- All five Phase 1 success criteria from ROADMAP.md are met
|
||||
</success_criteria>
|
||||
|
||||
<output>
|
||||
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`
|
||||
</output>
|
||||
Reference in New Issue
Block a user