`
+ ).join("");
+}
+
+loadDevices();
+```
+
+### Anti-Patterns to Avoid
+
+- **StaticFiles before API routes:** Mount static files last or `/api/*` paths return 404
+- **Blocking file I/O in async handlers:** Always use `aiofiles` for JSON reads/writes — stdlib `open()` blocks the event loop
+- **Module-level device instantiation:** Don't call `DeviceRegistry.load()` at import time — put it in lifespan
+- **Pydantic v1 `.json()` and `.parse_obj()`:** These are removed in v2; use `.model_dump_json()` and `.model_validate()`
+- **`@app.on_event("startup")`:** Deprecated; use the lifespan context manager
+- **Hardcoded paths:** Use `Path(__file__).parent` for paths relative to the package, not hardcoded strings like `"lightsync/frontend"`
+
+---
+
+## Don't Hand-Roll
+
+| Problem | Don't Build | Use Instead | Why |
+|---------|-------------|-------------|-----|
+| JSON validation on input | Manual type checks in route handlers | Pydantic BaseModel as route body type | FastAPI auto-validates, auto-returns 422 on bad input |
+| File serving | Custom route that reads files | `StaticFiles(html=True)` | Handles 404, directory index, cache headers, MIME types |
+| UUID generation | `str(random.randint(...))` | `uuid4()` as Pydantic `default_factory` | Collision-proof, standard format |
+| Async file write | Threads + stdlib `open()` | `aiofiles.open()` | Non-blocking; stdlib blocks the event loop |
+| WebSocket connection tracking | Raw dict with manual cleanup | `ConnectionManager` pattern (see above) | Handles disconnects, dead connection removal |
+| CSS terminal font stack | Single hardcoded font | CSS custom property `--font-mono` with fallbacks | Survives font load failure on offline dev |
+
+**Key insight:** FastAPI + Pydantic v2 handles about 60% of what you'd otherwise write by hand (validation, serialization, 422 errors, OpenAPI docs). Don't add logic that duplicates what the framework already does.
+
+---
+
+## Common Pitfalls
+
+### Pitfall 1: StaticFiles Intercepts API Routes
+**What goes wrong:** `/api/devices/` returns 404 from the static file handler instead of the router.
+**Why it happens:** `app.mount()` is processed in definition order. If `StaticFiles` at `"/"` is mounted before `include_router()`, it matches everything.
+**How to avoid:** Always mount StaticFiles after all `include_router()` calls.
+**Warning signs:** All API routes return 404 with `Content-Type: text/html` (the 404.html from StaticFiles).
+
+### Pitfall 2: Pydantic v2 Breaking Changes from v1
+**What goes wrong:** Code works during local dev but breaks on a clean install because of v1/v2 API differences.
+**Why it happens:** STACK.md and ARCHITECTURE.md use some v1 patterns (`.json()`, `.parse_obj()`).
+**How to avoid:** Use only v2 APIs: `model_dump()`, `model_dump_json()`, `model_validate()`, `model_validate_json()`. Never call `.dict()` or `.json()`.
+**Warning signs:** `AttributeError: 'ShowModel' object has no attribute 'json'`
+
+### Pitfall 3: uvicorn Hot Reload on Windows
+**What goes wrong:** `uvicorn.run(..., reload=True)` crashes with a multiprocessing error on Windows when called without `if __name__ == "__main__"` guard.
+**Why it happens:** Windows uses `spawn` (not `fork`) for multiprocessing. Without the guard, the worker process re-executes the module and tries to start another server.
+**How to avoid:** Always use `if __name__ == "__main__"` in `__main__.py`. Do NOT enable reload by default; document it as a dev-only flag.
+**Warning signs:** `RuntimeError: This event loop is already running` or `AssertionError` on Windows startup.
+
+### Pitfall 4: Path Separator Bugs on Windows
+**What goes wrong:** Show file paths stored as `/shows/my_show.json` fail on Windows because Windows uses backslash.
+**Why it happens:** Hardcoded forward slashes in path strings.
+**How to avoid:** Use `pathlib.Path` everywhere. `Path("shows") / show_id` handles separators automatically. Never concatenate path strings.
+**Warning signs:** `FileNotFoundError` with forward-slash paths on Windows.
+
+### Pitfall 5: DeviceConfig id as UUID vs str
+**What goes wrong:** `device_id` parameter in URL routes is typed as `str` but the registry stores keys as `str(UUID)`. Mismatch causes lookups to silently fail.
+**Why it happens:** UUID `__str__` uses lowercase hex with dashes; manual construction may differ.
+**How to avoid:** Store registry keys as `str(config.id)` explicitly. In route handlers, accept `device_id: str` and pass directly to `registry.get(device_id)`.
+**Warning signs:** DELETE /devices/{id} returns 404 for a valid device.
+
+### Pitfall 6: Frontend Google Fonts on Offline Dev
+**What goes wrong:** The UI uses a raw system monospace font instead of JetBrains Mono during offline development.
+**Why it happens:** Google Fonts CDN is unavailable without network.
+**How to avoid:** This is acceptable behavior — CSS fallback chain handles it. Alternatively, bundle the font file in `lightsync/frontend/fonts/`. Document the expected behavior.
+**Warning signs:** UI renders in Courier New instead of JetBrains Mono — cosmetic only, not a bug.
+
+---
+
+## State of the Art
+
+| Old Approach | Current Approach | When Changed | Impact |
+|--------------|------------------|--------------|--------|
+| `@app.on_event("startup")` | `lifespan=` context manager | FastAPI 0.93 (2023) | Old events deprecated; lifespan is now the only supported pattern |
+| `model.json()` (Pydantic v1) | `model.model_dump_json()` | Pydantic v2 (2023) | v1 methods raise AttributeError in v2 |
+| `Model.parse_obj(data)` (v1) | `Model.model_validate(data)` | Pydantic v2 (2023) | Same — v1 API removed |
+| `uvicorn[all]` extra | `uvicorn[standard]` extra | ~2023 | `[all]` was removed; `[standard]` is the correct WebSocket + watchfiles bundle |
+
+**Deprecated/outdated:**
+- `@app.on_event`: Deprecated since FastAPI 0.93; still works but emits deprecation warnings
+- `pydantic.validator`: Replaced by `@field_validator` in v2; old decorator removed
+
+---
+
+## Environment Availability
+
+> Step 2.6: The project runs on Windows 11 (primary runtime). Research machine is Linux. Environment audit is for the Windows 11 target.
+
+| Dependency | Required By | Available (Windows 11) | Notes | Fallback |
+|------------|------------|------------------------|-------|----------|
+| Python 3.11 | INF-01 | Must verify | Standard install from python.org or winget | — |
+| uv | Dependency mgmt | Recommended | `winget install astral-sh.uv` or `pip install uv` | pip directly |
+| FastAPI + uvicorn | INF-01 | Install via uv | No OS-level dependencies | — |
+| Pydantic v2 | Models | FastAPI dependency | Installed automatically | — |
+| aiofiles | Show store | Install via uv | Pure Python, no OS deps | — |
+| JetBrains Mono font | UI-02 | Served via Google Fonts CDN | Requires internet; fallback to system monospace | Consolas (Windows built-in) |
+| Browser | UI-01 | Chrome/Edge/Firefox on Win11 | Any modern browser works | — |
+
+**Missing dependencies with no fallback:**
+- Python 3.11 must be present on the Windows 11 machine before any installation step
+
+**Missing dependencies with fallback:**
+- Google Fonts CDN: fallback to Consolas (Windows built-in monospace)
+- uv: fallback to pip
+
+**Windows-specific notes:**
+- `asyncio.ProactorEventLoop` is the Windows default since Python 3.8 — no configuration needed for UDP and subprocess (Phase 2+)
+- `uvicorn.run()` uses `WindowsSelectorEventLoopPolicy` internally when `--reload` is active — don't fight this
+- File paths: use `pathlib.Path` throughout; `show_dir = Path("shows")` resolves relative to CWD on Windows correctly
+
+---
+
+## Open Questions
+
+1. **Device registry storage: separate `devices.json` vs embedded in each show**
+ - What we know: ARCHITECTURE.md says "Config in show JSON or separate devices.json"
+ - What's unclear: The show file embeds a device snapshot (for portability), but the registry for the UI list needs a separate source of truth
+ - Recommendation (Claude's discretion): Use **both** — `devices.json` is the live registry (what the UI CRUD panel manages), and each show file embeds a snapshot of the devices at the time of show creation. This is consistent with ARCHITECTURE.md's "self-contained show file" principle.
+
+2. **Dependency injection for registry/store in route handlers**
+ - What we know: Module-level globals in `main.py` work for Phase 1 (single process)
+ - What's unclear: FastAPI's `Depends()` is cleaner but adds boilerplate for a solo project
+ - Recommendation: Use module-level state for Phase 1. Add a TODO comment to migrate to `Depends()` when tests are added in Phase 3.
+
+3. **Show name vs show ID in URL routes**
+ - What we know: Show IDs are UUIDs (hard to type in browser), names are user-defined
+ - Recommendation: `/api/shows/` returns list with both; `/api/shows/{show_id}` uses UUID. Frontend always uses the UUID from the API response.
+
+---
+
+## Code Examples
+
+### Full pyproject.toml
+
+```toml
+[build-system]
+requires = ["hatchling"]
+build-backend = "hatchling.build"
+
+[project]
+name = "lightsync"
+version = "0.1.0"
+description = "Music-to-light synchronization system"
+requires-python = ">=3.11"
+dependencies = [
+ "fastapi>=0.115.0",
+ "uvicorn[standard]>=0.34.0",
+ "aiofiles>=24.0.0",
+ "python-dotenv>=1.0.0",
+ "structlog>=24.0.0",
+]
+
+[project.optional-dependencies]
+dev = [
+ "httpx>=0.27.0",
+]
+
+[tool.hatch.build.targets.wheel]
+packages = ["lightsync"]
+```
+
+**Note:** Pydantic is not listed separately — it is pulled in as a FastAPI dependency. If a specific version pin is needed: add `"pydantic>=2.0.0"` to dependencies.
+
+### SK6812 Device Implementation Example
+
+```python
+# lightsync/devices/sk6812.py
+from lightsync.devices.base import BaseDevice
+
+class SK6812Device(BaseDevice):
+ """RGBW strip — 4 bytes per LED."""
+
+ @property
+ def bytes_per_pixel(self) -> int:
+ return 4
+
+ def encode_frame(self, pixels: list[tuple]) -> bytes:
+ # WLED DRGBW protocol: header [3, timeout_seconds]
+ payload = bytearray([3, 2])
+ for pixel in pixels:
+ r, g, b = pixel[:3]
+ w = pixel[3] if len(pixel) > 3 else 0
+ payload.extend([r, g, b, w])
+ return bytes(payload)
+
+ def encode_animation_cmd(self, animation: str, params: dict) -> bytes:
+ import json
+ cmd = {"type": "animation", "anim": animation, "params": params}
+ return json.dumps(cmd).encode("utf-8")
+```
+
+---
+
+## Sources
+
+### Primary (HIGH confidence)
+- [FastAPI Lifespan Events](https://fastapi.tiangolo.com/advanced/events/) — lifespan context manager pattern, startup/shutdown
+- [FastAPI Static Files](https://fastapi.tiangolo.com/tutorial/static-files/) — StaticFiles mounting, html=True for SPA serving
+- [FastAPI WebSockets](https://fastapi.tiangolo.com/advanced/websockets/) — ConnectionManager pattern, broadcast
+- [Pydantic v2 Models](https://docs.pydantic.dev/latest/concepts/models/) — BaseModel, Field, default_factory
+- [Pydantic v2 Serialization](https://docs.pydantic.dev/latest/concepts/serialization/) — model_dump_json, model_validate
+- [Python ABC docs](https://docs.python.org/3/library/abc.html) — abstract base class infrastructure
+- [uvicorn Settings](https://uvicorn.dev/settings/) — ProactorEventLoop, Windows behavior, reload guard
+
+### Secondary (MEDIUM confidence)
+- [WebSearch: uvicorn Windows ProactorEventLoop](https://github.com/fastapi/fastapi/discussions/13549) — confirmed if __name__ == "__main__" guard required on Windows
+- [terminal.css](https://terminalcss.xyz/) — terminal CSS aesthetic reference patterns
+- [CSS-Tricks: Old Timey Terminal Styling](https://css-tricks.com/old-timey-terminal-styling/) — terminal CSS techniques
+
+### Tertiary (LOW confidence — from STACK.md/ARCHITECTURE.md, accepted as project-internal decisions)
+- `.planning/research/STACK.md` — version recommendations, technology decisions
+- `.planning/research/ARCHITECTURE.md` — show file JSON schema, component boundaries, data flow
+
+---
+
+## Metadata
+
+**Confidence breakdown:**
+- Standard stack: HIGH — FastAPI/Pydantic v2/uvicorn are stable, well-documented, official docs verified
+- Architecture: HIGH — patterns from official FastAPI docs; device ABC is standard Python
+- CSS terminal aesthetic: MEDIUM — no official spec; based on CSS custom properties patterns + terminal.css reference
+- Windows pitfalls: HIGH — uvicorn/Windows reload issue is documented in FastAPI discussions
+- Pydantic v2 API: HIGH — official docs consulted, v1→v2 migration changes verified
+
+**Research date:** 2026-04-05
+**Valid until:** 2026-10-05 (stable libraries; Pydantic v2 and FastAPI 0.115.x are not fast-moving)