255 lines
10 KiB
Markdown
255 lines
10 KiB
Markdown
---
|
|
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>
|