Day-to-day workflow, environment, and the non-obvious pitfalls that have bitten this project. Read this before debugging anything that "should work".
npm install
cp .env.example .env # optional: LOG_LEVEL
npm run dev # terminal 1 — Next.js dev server
npm run logs # terminal 2 — pretty-printed tail of the log fileThe dev server listens on http://localhost:3000 (Next.js auto-increments to
3001+ if 3000 is taken — watch the startup output for the real port). Configure
the WLED host in the UI top bar.
| Script | Does |
|---|---|
npm run dev |
Next.js dev server |
npm run build |
Production build |
npm run start |
Production server |
npm run logs |
tail -f logs/hex-hive.log | pino-pretty |
npm run typecheck |
tsc --noEmit |
| Var | Default | Purpose |
|---|---|---|
LOG_LEVEL |
debug |
Pino log level (trace/debug/info/warn/error) |
NODE_ENV |
— | production disables stdout log stream |
Pino writes JSON to logs/hex-hive.log (and stdout in dev). Categories:
http, wled, ws, store, wave, pattern, mapper, sse. Tail with
npm run logs for a colorized, human-readable view. The log is the primary
debugging tool — every WLED call and user action is recorded.
Next.js dev HMR reloads modules but does not reset globalThis. It can also
load multiple copies of a module across webpack chunks. Long-lived singletons
(wledWs, waveStreamer, sseBus, log, the config cache) therefore live on
globalThis.
Two failure modes seen during development:
- Stale-cache bug: a module-level
let cachemeant a route writing config and the WS subscriber reading config held different caches → "WS won't connect after host change". Fix: cache onglobalThis. - Zombie-instance bug:
waveStreamerwas a plainexport const new WaveStreamer(). After HMR,pattern/applyheld the old instance and calledstop()on it while a newer instance kept ticking → "I stopped the wave but POSTs keep firing". Fix: export a wrapper whose methods all route throughglobalThis.__hexhive_wave.
Rule of thumb: anything with a timer, a socket, or a cache must be reached
through globalThis, never a captured module-level binding. When in doubt
during dev, restart the dev server — it's the only guaranteed clean slate.
Pino's worker-thread transports (pino/file, pino-pretty) don't survive
Next.js webpack bundling. log.ts uses pino.destination / pino.multistream
(synchronous, no worker threads) instead, and next.config.mjs lists pino in
serverComponentsExternalPackages.
The target is an ESP8266. It has ~40KB of heap and a hard 16-segment limit.
- The wave streamer POSTs at ~16Hz. Over long sessions this fragments WLED's
heap. Healthy
freeheapis 25KB+; under ~10KB the device starts dropping WebSocket clients and corrupting flash config. Reboot WLED to recover (POST /json/state {"rb":true}or power-cycle). - If colors look channel-swapped, check WLED's color order setting (GRB for WS281x) on the device's own settings page. This app never writes device config — a wrong color order is a device-side setting, often nudged by the heap corruption above.
WLED remembers the last transition value across state updates. Live streaming
must send transition: 0 or every frame fights a 1.4s interpolation. The
encoder handles this for buildApplyHexColors.
WLED's overlap rule: higher segment id wins. A per-LED i-array update on
segment 0 is overridden by any active segments 1–15. Per-LED payloads must emit
segments 1–15 as zero-length to win cleanly.
Next.js 14 (App Router) · React 18 · TypeScript (strict) · Tailwind CSS ·
Pino · ws · zod. See package.json for versions.