Files
led2/.planning/phases/01-foundation/01-03-PLAN.md

10 KiB

phase, plan, type, wave, depends_on, files_modified, autonomous, requirements, must_haves
phase plan type wave depends_on files_modified autonomous requirements must_haves
01-foundation 03 execute 2
01-01
01-02
lightsync/api/devices.py
lightsync/api/shows.py
lightsync/main.py
docker-compose.prod.yml
Dockerfile
true
INF-01
DEV-01
DEV-02
DEV-03
SHW-01
SHW-02
UI-01
truths artifacts key_links
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
path provides contains
Dockerfile Python alpine container running lightsync python -m lightsync
path provides contains
docker-compose.prod.yml Traefik-labeled service for lightsync.groll.cloud lightsync.groll.cloud
from to via pattern
lightsync/frontend/app.js lightsync/api/devices.py fetch /api/devices — full CRUD round-trip api/devices
from to via pattern
lightsync/api/shows.py lightsync/store/show_store.py ShowStore save/load calls show_store
from to via pattern
lightsync/api/devices.py lightsync/devices/registry.py DeviceRegistry CRUD + persist 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.

<execution_context> @$HOME/.claude/get-shit-done/workflows/execute-plan.md @$HOME/.claude/get-shit-done/templates/summary.md </execution_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

From lightsync/models/device.py:

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:

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:

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:

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

<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>
After completion, create `.planning/phases/01-foundation/01-03-SUMMARY.md`