Build History
How this actually got built.
Eight rounds so far. This is the condensed version — the expanded, blow-by-blow write-up of each real bug (what broke, how it was found, how it was fixed, and how the fix was verified) lives in the repo's iterations/ directory and HANDOFF.md.
Round 1 — local control from zero
Started with no credentials at all for a Bytech A19 Wi-Fi RGB+CCT bulb. A mitmproxy interception attempt failed (the Tuya app's real API traffic runs over HTTP/3/QUIC, which slips past a standard proxy). The Tuya IoT Platform's QR-link step scanned but never confirmed — a known data-center-mismatch bug. What actually worked: tuya-device-sharing-sdk, the same cloud-assisted QR-login flow Home Assistant's own Tuya integration uses, needing only the app's User Code — no developer platform project at all. Verified with real color changes: red, then blue, then yellow, watched on the physical bulb.
Round 2 & 3 — audio-reactive lighting, discovery, orchestration, security
Built the 12-mode audio engine, network auto-discovery, multi-bulb orchestration, and the PIN gate. Five real bugs found via actual testing, not code review alone:
- An IP-change log entry reported the wrong “old IP.”
- The brightness floor during silence never actually reached its floor.
- Bulb network calls inside the audio callback froze the whole analysis pipeline when the bulb was slow or offline.
- Hue smoothing broke at the 0°/360° wrap boundary.
- The PIN gate blocked its own login page once enabled.
All five are documented with the actual failure evidence and the fix verification in iterations/001 through 004.
Round 4 — parallel-phase build, mobile fix, Tailscale, docs (v0.3.0)
Four roadmap phases built in parallel via isolated subagent worktrees, hub-verified and hand-merged: two new audio modes, PIN-gate hardening (session revocation, audit logging, per-IP rate limiting), a stdlib CLI (bulbctl), and a real 76-test pytest suite. Followed by a QoL round (live-ticking timers/status, visual polish, power-user niceties) and a mobile-friendliness fix shipped as its own PR. Three real bugs found via actual testing:
- A hue-blend flicker in the new
harmonic_pairsaudio mode — two anchors exactly 180° apart cancelled to a zero vector at a 50/50 energy split. - A real mobile layout bug — the sidebar was collapsing to a ~16px sliver on phones due to an unreset grid-column span.
- A test-isolation gap — enabling the PIN gate for real (for a Tailscale exposure) broke 16 previously-passing tests that weren't isolating auth state.
Tailscale Serve was set up and verified end-to-end as the actual off-LAN access path (tailnet-only HTTPS, PIN gate enabled alongside it) — 21/21 live security checks passed.
Round 5 — Week 1 roadmap (open as PR #68, not yet merged)
Four roadmap phases built in parallel via isolated subagent worktrees, hub-verified through real hand-resolved git merges (not patch application, since all four phases touched the same core classes) across roughly 40 conflict blocks: new audio modes + a tempo tracker (Phase A), signal conditioning + a golden-value test harness (Phase B), multi-bulb orchestration + zones (Phase C), and session management + safety/accessibility (Phase D). This is currently open as PR #68, tracked in issues #64–#67, not yet merged to master — pending Owen's own testing.
5 real post-merge test failures were found and fixed (a dropped mode-validation check, a rate-limiter state leak across tests, stale test mocks, and a route-vs-module naming collision). Live-testing this over Tailscale then surfaced 3 more real bugs, this time via a physical bulb that had gone unreachable on the network:
- An unreachable device hung any
/statuscall for minutes — timed at 3m26s against the real bulb — because tinytuya's socket timeout and retry limit were unbounded. Capped both; same device now fails in ~2s. - The status badge got stuck reading “connecting…” forever once a poll had never succeeded even once — a stale early-return condition that never cleared.
- The badge and the Control panel briefly showed contradictory labels (“LIVE DATA · OFF” vs “OFFLINE”) for the same offline device, once the first fix exposed a spot where the badge read
.powerinstead of checking.onlinedirectly.
All verified with real timing tests and Playwright screenshots against the actual unreachable device — full backend suite stayed green (353/353) throughout.
Rounds 6 & 7 — Week 2 hardening, then CVEs, live streaming and the docs browser
Week 2 shipped in four phases and closed: PIN-gate hardening (configurable lockout with exponential backoff, enforced PIN complexity, separately-revocable guest PINs, IPv6-correct per-IP tracking) and a general per-IP API rate limiter; TLS and reverse proxy (validated Caddy and nginx configs, systemd units, trusted-proxy X-Forwarded-For handling, a /healthz probe); audit logging, secrets management and backup/restore; then observability and network resilience.
Round 7 closed both applicable starlette CVEs (FastAPI 0.115.6 → 0.141.1), made audio analysis 57% faster with bit-identical output, moved beat detection onto the bass band rather than broadband RMS — measured 9/9 tempos correct against 1/9 on a dense mix — added a live SSE stream, expanded the genre presets from 8 to 24, and built the in-app documentation browser. Six real bugs, including a local_key leak through a tinytuya exception string, and auth-state file corruption that bricked the entire dashboard because a non-atomic write left half a file the gate read on every request.
Round 8 — audio-reactive lighting actually works
The flagship feature had never once worked in the way the dashboard is actually deployed. A Linux container on Docker Desktop for Windows has no access to host audio devices, so a session there ran, reported itself running, and never reacted to a sound. This round closed that end to end, and for the first time the bulb reacted to real audio through the deployed container.
Latency stopped being a claim and became a number: per-stage capture, analysis and bulb round-trip, each with a typical figure and a worst case, live in the dashboard. Then analysis was rebuilt to run a short hop over a longer window, so latency and frequency resolution stopped fighting each other. Software sound-to-decision went from 12.19ms to 6.44ms, inside the 10ms target — proven by the instrumentation rather than asserted. analyze_frame() itself was not modified; all 21 golden-value tests passed with no expected values changed, which is what proves the change was timing and nothing else.
The interesting failures were the ones only real hardware exposed:
- The instrumentation flattered itself. Capture latency was first measured as the median gap between capture callbacks. Against DirectSound that read 0.93ms while the mean was 11.59ms — the backend delivers in bursts, so half the gaps are sub-millisecond while the data is no fresher. A 12ms pipeline reported 1.5ms and claimed it met the target. Capture is now the block period plus delivery lateness, with the raw gaps kept as a separate delivery-health signal.
- Then it flattered itself again, differently. With a 256-sample hop it reported a 5.8ms floor even when the source only delivered every 20ms. A decision can never be fresher than its input, so the floor became
max(configured hop, observed delivery period)— and the dashboard now names which one is binding. - A longer analysis window is not better. 2048 samples resolves bass twice as finely as 1024 and tracks one fewer tempo; 4096 tracks half of them, all biased high. A longer window smears the transient it is trying to locate. The default is the measured compromise, not a guess.
- The launcher hardcoded a capture device that was silent. It would connect, stream flawlessly and deliver nothing but zeros. Auto-detection replaced it — and “pick the loudest” turned out to be wrong too, choosing a microphone hearing the speakers (peak 0.53) over a clean loopback feed (0.23). Loopback wins now, because a microphone hears the room.
- The status chip could not describe the failure it was looking at. Audio arriving with no session consuming it read as “live” while the bulb sat still. It now says so explicitly.
MSG_WAITALLis unsupported on Windows — the only platform the capture tool runs on. Caught by a test before it ever ran on the host.- The in-app docs browser was empty in every container deployment. The Dockerfile never copied
docs/; it had only ever worked from a host checkout.
Also shipped: a one-command switch between container and native-host audio, minimum dwell adjustable while a session runs (so tuning by ear doesn't mean stopping and restarting), and a bridge protocol revision that lets the capture device be chosen in the dashboard — necessary because the container cannot enumerate the host's audio devices, so the tool now reports its own inventory over the same socket.
729 tests passing, up from 671 at the start of the round.
Known-fragile / watch for
- The physical bulb genuinely drops off Wi-Fi periodically — observed multiple times across every build session, unprompted. This is bulb/router behavior, not a dashboard bug.
- The bulb's round-trip is wildly variable: measured between 11ms and 637ms on the same lamp within an hour. Any latency that feels wrong should be checked against the dashboard's per-stage panel before blaming a setting — at worst the bulb alone is ~100× the entire software budget.
- Audio-reactive mode tuning (hue anchors, beat thresholds) is verified correct in direction via synthetic tones, and the 24 genre presets are reasoned rather than tuned by ear. Every blocker to doing it properly is now cleared; the judgement pass itself is still outstanding.
docker-compose.yml'snetwork_mode: hostonly works on Linux Docker hosts.
What's not built (intentionally)
- Bluetooth bulb support — no BLE hardware to test against yet.
- A second physical bulb — architecture already supports it; just needs the hardware.
- A dedicated adversarial security pentest against a real deployed instance — scoped as its own roadmap phase. Round 4's security pass was a same-machine verification, not an independent attacker's attempt.
Full narrative with every command run and its actual output: the complete HANDOFF.md.