docs(01): create gap closure plan 01-04 — device CRUD UI + CSS variable fix

This commit is contained in:
Claude
2026-04-05 20:01:48 +00:00
parent af1100a850
commit ab162fb244
2 changed files with 358 additions and 3 deletions

View File

@@ -12,7 +12,7 @@ LightSync is built bottom-up: data model first, audio engine second, UDP protoco
Decimal phases appear between their surrounding integers in numeric order. Decimal phases appear between their surrounding integers in numeric order.
- [x] **Phase 1: Foundation** - FastAPI skeleton, show file schema (Pydantic), device registry, terminal-aesthetic frontend shell (completed 2026-04-05) - [ ] **Phase 1: Foundation** - FastAPI skeleton, show file schema (Pydantic), device registry, terminal-aesthetic frontend shell (gap closure in progress)
- [ ] **Phase 2: Audio Engine** - MPV IPC bridge, waveform rendering, playback controls, Windows timer fix - [ ] **Phase 2: Audio Engine** - MPV IPC bridge, waveform rendering, playback controls, Windows timer fix
- [ ] **Phase 3: Communication Protocol** - UDP packet spec, DRGB/DNRGB/DRGBW encoders, animation library, software simulator - [ ] **Phase 3: Communication Protocol** - UDP packet spec, DRGB/DNRGB/DRGBW encoders, animation library, software simulator
- [ ] **Phase 4: Timeline Editor** - DAW UI (tracks, drag-place blocks, resize, delete), undo/redo, beat detection, snap-to-beat - [ ] **Phase 4: Timeline Editor** - DAW UI (tracks, drag-place blocks, resize, delete), undo/redo, beat detection, snap-to-beat
@@ -32,12 +32,13 @@ 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 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 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 5. Adding a new strip type requires only a new device class — no changes to core engine code
**Plans**: 3 plans **Plans**: 4 plans
Plans: Plans:
- [x] 01-01: Backend scaffold — FastAPI app, Pydantic show/device models, show store (JSON load/save), device registry - [x] 01-01: Backend scaffold — FastAPI app, Pydantic show/device models, show store (JSON load/save), device registry
- [x] 01-02: Frontend shell — HTML/CSS terminal aesthetic, WebSocket client stub, device panel, show panel - [x] 01-02: Frontend shell — HTML/CSS terminal aesthetic, WebSocket client stub, device panel, show panel
- [x] 01-03: REST API — CRUD endpoints for devices and shows, static file serving, integration smoke test - [x] 01-03: REST API — CRUD endpoints for devices and shows, static file serving, integration smoke test
- [ ] 01-04: Gap closure — device CRUD UI (add form, remove button), CSS variable naming fix, panel IDs
**UI hint**: yes **UI hint**: yes
### Phase 2: Audio Engine ### Phase 2: Audio Engine
@@ -151,7 +152,7 @@ Phases execute in numeric order: 1 → 2 → 3 → 4 → 5 → 6 → 7
| Phase | Plans Complete | Status | Completed | | Phase | Plans Complete | Status | Completed |
|-------|----------------|--------|-----------| |-------|----------------|--------|-----------|
| 1. Foundation | 3/3 | Complete | 2026-04-05 | | 1. Foundation | 3/4 | Gap closure | - |
| 2. Audio Engine | 0/3 | Not started | - | | 2. Audio Engine | 0/3 | Not started | - |
| 3. Communication Protocol | 0/3 | Not started | - | | 3. Communication Protocol | 0/3 | Not started | - |
| 4. Timeline Editor | 0/4 | Not started | - | | 4. Timeline Editor | 0/4 | Not started | - |

View File

@@ -0,0 +1,354 @@
---
phase: 01-foundation
plan: 04
type: execute
wave: 3
depends_on:
- 01-03
files_modified:
- lightsync/frontend/index.html
- lightsync/frontend/style.css
- lightsync/frontend/app.js
autonomous: true
requirements:
- DEV-03
- UI-02
gap_closure: true
must_haves:
truths:
- "The DEVICES panel has an add-device form with inputs for name, strip_type, led_count, ip, port and a submit button"
- "Submitting the add-device form creates a device via POST /api/devices and the new device appears in the list"
- "Each device row has a remove button that calls DELETE /api/devices/{id} and refreshes the list"
- "CSS uses --accent: #00ffff as the canonical accent variable name"
- "index.html has panel IDs: devices-panel, animations-panel, timeline-panel, transport-panel"
artifacts:
- path: "lightsync/frontend/index.html"
provides: "Add-device form and panel IDs"
contains: "add-device-form"
- path: "lightsync/frontend/app.js"
provides: "Device CRUD functions: apiPost, apiDelete, removeDevice, form handler"
contains: "apiPost"
- path: "lightsync/frontend/style.css"
provides: "Canonical --accent CSS variable"
contains: "--accent: #00ffff"
key_links:
- from: "lightsync/frontend/app.js"
to: "/api/devices"
via: "apiPost for form submit, apiDelete for remove button"
pattern: "apiPost.*api/devices"
- from: "lightsync/frontend/index.html"
to: "lightsync/frontend/app.js"
via: "form#add-device-form submit event listener"
pattern: "add-device-form"
---
<objective>
Close two verification gaps from 01-VERIFICATION.md: (1) add device CRUD UI — form, POST, DELETE, remove buttons; (2) fix CSS variable naming to use --accent and add missing panel IDs to index.html.
Purpose: Completes DEV-03 (device list with UI interaction) and UI-02 (CSS contract alignment with plan spec).
Output: Updated index.html, app.js, and style.css with full device management UI and corrected CSS variable naming.
</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-VERIFICATION.md
</context>
<tasks>
<task type="auto">
<name>Task 1: Add device CRUD UI — form in index.html, POST/DELETE/remove in app.js</name>
<files>
lightsync/frontend/index.html
lightsync/frontend/app.js
</files>
<read_first>
lightsync/frontend/index.html
lightsync/frontend/app.js
lightsync/api/devices.py
</read_first>
<action>
**index.html changes:**
In the sidebar, the first panel (DEVICES panel, currently lines 23-28) must get an id and have the add-device form inserted after the device-list div. Replace the current DEVICES panel block:
```html
<div class="panel" id="devices-panel" style="flex: 1; min-height: 0;">
<div class="panel-header">DEVICES</div>
<div class="panel-content" id="device-list">
<span class="text-dim">loading...</span>
</div>
<form id="add-device-form" class="device-form">
<input type="text" name="name" placeholder="Device name" required>
<select name="strip_type" required>
<option value="sk6812">SK6812</option>
<option value="ws2801">WS2801</option>
<option value="generic">Generic</option>
</select>
<input type="number" name="led_count" placeholder="LED count" min="1" max="1000" required>
<input type="text" name="ip" placeholder="IP address" required>
<input type="number" name="port" placeholder="Port" min="1" max="65535" value="21324" required>
<button type="submit">+ ADD DEVICE</button>
</form>
</div>
```
The form goes AFTER the panel-content div but still inside the panel div, so it sits at the bottom of the devices panel. The form class is "device-form" (styles added in Task 2).
**app.js changes:**
Add two API helper functions before the LightSyncClient class (near the top, after the WS_URL const):
```javascript
async function apiPost(path, data) {
const res = await fetch(path, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (!res.ok) throw new Error(`POST ${path}: HTTP ${res.status}`);
return res.json();
}
async function apiDelete(path) {
const res = await fetch(path, { method: "DELETE" });
if (!res.ok && res.status !== 204) throw new Error(`DELETE ${path}: HTTP ${res.status}`);
}
```
Modify the `loadDevices()` function: change the device row rendering to include a remove button. Replace the current `list.innerHTML = devices.map(...)` block with:
```javascript
list.innerHTML = devices.map(d =>
`<div class="device-row">
<div class="device-row-info">
<span class="text-accent">${escapeHtml(d.name)}</span>
<span class="text-dim">${escapeHtml(d.strip_type)} / ${d.led_count} LEDs / ${escapeHtml(d.ip)}:${d.port}</span>
</div>
<button class="device-remove" onclick="removeDevice('${d.id}')" title="Remove device">&times;</button>
</div>`
).join("");
```
Add the `removeDevice` function (after `loadDevices`):
```javascript
async function removeDevice(id) {
try {
await apiDelete(`/api/devices/${id}`);
await loadDevices();
} catch (err) {
console.error("[devices] remove failed:", err);
}
}
```
Add a form submit handler. After the `loadDevices()` call at the bottom of the file, add:
```javascript
document.getElementById("add-device-form").addEventListener("submit", async (e) => {
e.preventDefault();
const form = e.target;
const data = {
name: form.name.value.trim(),
strip_type: form.strip_type.value,
led_count: parseInt(form.led_count.value, 10),
ip: form.ip.value.trim(),
port: parseInt(form.port.value, 10),
};
try {
await apiPost("/api/devices", data);
form.reset();
form.port.value = "21324";
await loadDevices();
} catch (err) {
console.error("[devices] add failed:", err);
}
});
```
Note: Since app.js uses `type="module"`, the `removeDevice` function must be exported to global scope. Add this line after the function definition:
```javascript
window.removeDevice = removeDevice;
```
</action>
<verify>
<automated>cd /home/claude/led2 && grep -c "add-device-form" lightsync/frontend/index.html && grep -c "apiPost" lightsync/frontend/app.js && grep -c "apiDelete" lightsync/frontend/app.js && grep -c "removeDevice" lightsync/frontend/app.js && grep -c "device-remove" lightsync/frontend/index.html</automated>
</verify>
<acceptance_criteria>
- lightsync/frontend/index.html contains `id="add-device-form"` with a form element
- lightsync/frontend/index.html contains `name="name"` input, `name="strip_type"` select with options sk6812/ws2801/generic, `name="led_count"` input, `name="ip"` input, `name="port"` input
- lightsync/frontend/index.html contains `type="submit"` button with text "+ ADD DEVICE"
- lightsync/frontend/index.html contains `class="device-remove"` button with onclick calling removeDevice
- lightsync/frontend/app.js contains `async function apiPost(path, data)` with `method: "POST"` and `Content-Type: application/json`
- lightsync/frontend/app.js contains `async function apiDelete(path)` with `method: "DELETE"`
- lightsync/frontend/app.js contains `async function removeDevice(id)` that calls `apiDelete` with `/api/devices/${id}`
- lightsync/frontend/app.js contains `addEventListener("submit"` on `add-device-form` that calls `apiPost("/api/devices", data)`
- lightsync/frontend/app.js contains `window.removeDevice = removeDevice` to expose function globally for onclick handler
</acceptance_criteria>
<done>The DEVICES panel has a working add-device form (name, strip_type select, led_count, ip, port, submit button). Each device row has a remove button. Form submit calls POST /api/devices, remove calls DELETE /api/devices/{id}, both refresh the device list.</done>
</task>
<task type="auto">
<name>Task 2: Fix CSS variable naming to --accent and add panel IDs to index.html</name>
<files>
lightsync/frontend/style.css
lightsync/frontend/index.html
</files>
<read_first>
lightsync/frontend/style.css
lightsync/frontend/index.html
</read_first>
<action>
**style.css changes:**
In the `:root` block (line 1-14), replace the current variable declarations with these renamed variables. The key change is adding `--accent: #00ffff` as the canonical accent variable. Keep `--text-accent` and `--border-bright` as aliases that reference `--accent`:
Replace lines 1-14 with:
```css
:root {
--bg-primary: #0a0a0a;
--bg-panel: #0f0f0f;
--bg-panel-dark: #080808;
--border-dim: #1a4a4a;
--accent: #00ffff;
--border-bright: var(--accent);
--text-primary: #cccccc;
--text-dim: #555555;
--text-accent: var(--accent);
--text-warning: #ff6600;
--font-mono: 'JetBrains Mono', 'Fira Code', 'Consolas', monospace;
--font-size: 13px;
--panel-gap: 1px;
}
```
This makes `--accent` the single source of truth for `#00ffff`. The existing `--text-accent` and `--border-bright` usages throughout the CSS file continue to work because they now reference `--accent`.
Add CSS rules for the device form and remove button (append after the existing `button:hover` rule at the end of the file):
```css
/* Device form */
.device-form {
display: flex;
flex-direction: column;
gap: 4px;
padding: 8px 12px;
border-top: 1px solid var(--border-dim);
}
.device-form button[type="submit"] {
margin-top: 4px;
text-transform: uppercase;
letter-spacing: 0.08em;
}
/* Device remove button */
.device-remove {
background: none;
border: none;
color: var(--text-dim);
font-family: var(--font-mono);
font-size: 16px;
cursor: pointer;
padding: 0 4px;
line-height: 1;
flex-shrink: 0;
}
.device-remove:hover {
color: #ff3333;
border: none;
}
.device-row {
display: flex;
align-items: center;
justify-content: space-between;
padding: 6px 0;
border-bottom: 1px solid var(--bg-panel-dark);
gap: 8px;
}
.device-row-info {
display: flex;
flex-direction: column;
gap: 2px;
min-width: 0;
}
```
Note: The existing `.device-row` rule (lines 143-152) should be replaced by this updated version that adds `align-items: center` and `justify-content: space-between` to accommodate the remove button alongside the info.
**index.html changes:**
Add panel IDs to the remaining panels that are missing them. After Task 1, devices-panel already has its ID. The following still need IDs:
1. The ANIMATIONS panel (currently line 29 `<div class="panel" style="flex: 0 0 120px;">`): Add `id="animations-panel"`:
```html
<div class="panel" id="animations-panel" style="flex: 0 0 120px;">
```
2. The main-area timeline (currently line 35 `<main class="main-area">`): Wrap or add ID to the main element. Add a panel wrapper inside:
```html
<main class="main-area">
<div class="panel" id="timeline-panel" style="flex: 1; display: flex; align-items: center; justify-content: center;">
<span>[ TIMELINE &#8212; PHASE 2+ ]</span>
</div>
</main>
```
3. The transport-bar footer (currently line 39 `<footer class="transport-bar">`): Add ID to the footer or add an inner panel:
```html
<footer class="transport-bar" id="transport-panel">
<span>[ TRANSPORT &#8212; PHASE 2+ ]</span>
</footer>
```
</action>
<verify>
<automated>cd /home/claude/led2 && grep -c "\-\-accent.*#00ffff" lightsync/frontend/style.css && grep "devices-panel" lightsync/frontend/index.html | head -1 && grep "animations-panel" lightsync/frontend/index.html | head -1 && grep "timeline-panel" lightsync/frontend/index.html | head -1 && grep "transport-panel" lightsync/frontend/index.html | head -1</automated>
</verify>
<acceptance_criteria>
- lightsync/frontend/style.css contains `--accent: #00ffff` (not `--accent: var(...)`)
- lightsync/frontend/style.css contains `--text-accent: var(--accent)` (alias, not a separate #00ffff value)
- lightsync/frontend/style.css contains `--border-bright: var(--accent)` (alias, not a separate #00ffff value)
- lightsync/frontend/style.css contains `.device-form` rule with `display: flex` and `flex-direction: column`
- lightsync/frontend/style.css contains `.device-remove` rule with `cursor: pointer`
- lightsync/frontend/index.html contains `id="devices-panel"`
- lightsync/frontend/index.html contains `id="animations-panel"`
- lightsync/frontend/index.html contains `id="timeline-panel"`
- lightsync/frontend/index.html contains `id="transport-panel"`
</acceptance_criteria>
<done>CSS uses --accent: #00ffff as the canonical variable with --text-accent and --border-bright as aliases. All four panel IDs present in index.html. Device form and remove button have proper styling.</done>
</task>
</tasks>
<verification>
1. `grep "add-device-form" lightsync/frontend/index.html` returns a match
2. `grep "apiPost\|apiDelete\|removeDevice" lightsync/frontend/app.js` returns 3+ matches
3. `grep "\-\-accent:.*#00ffff" lightsync/frontend/style.css` returns a match
4. `grep "devices-panel\|animations-panel\|timeline-panel\|transport-panel" lightsync/frontend/index.html` returns 4 matches
5. After deploying: open https://lightsync.groll.cloud, fill the add-device form, submit, see device appear in list, click remove, device disappears
</verification>
<success_criteria>
- The add-device form is visible in the DEVICES panel with all 5 inputs and submit button
- Submitting the form creates a device via POST /api/devices and refreshes the list
- Each device row shows a remove button that calls DELETE /api/devices/{id}
- CSS variable --accent: #00ffff exists as the single source of truth for the cyan accent color
- All four panel IDs (devices-panel, animations-panel, timeline-panel, transport-panel) are present in index.html
</success_criteria>
<output>
After completion, create `.planning/phases/01-foundation/01-04-SUMMARY.md`
</output>