--- 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" --- 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. @$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/01-foundation/01-CONTEXT.md @.planning/phases/01-foundation/01-01-SUMMARY.md @.planning/phases/01-foundation/01-02-SUMMARY.md 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] ``` Task 1: Integration test — verify API endpoints, fix any wiring issues, add Docker deployment lightsync/api/devices.py lightsync/api/shows.py lightsync/main.py Dockerfile docker-compose.prod.yml 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 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 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()) " - 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" 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. 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 - 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 After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`