10 KiB
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 |
|
|
true |
|
|
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.mdFrom 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]
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>