{
  "schema_version": 1,
  "slug": "octolux-app-server-debug",
  "type": "skill",
  "version": "1.0.0",
  "title": "Octolux App Server — debug startup and runtime failures",
  "brief": "Diagnose any Octolux Node.js app server failure that breaks the Dashboard (startup or runtime). Six symptoms with root causes and fixes.",
  "content": "# Octolux App Server — debug startup and runtime failures\n\nDiagnose any Octolux Node.js app server failure that breaks the Dashboard (startup or runtime). Six symptoms with root causes and fixes.\n\n## Known errors this skill resolves\n\nUntil the underlying bugs ship a fix, this skill is the reference for diagnosing and working around the items below. Strike a line off this list when the upstream fix lands.\n\n| | Error / symptom | Symptom # |\n|---|---|---|\n| ☐ | `connect ECONNREFUSED 0.0.0.0:8830` in the Dashboard webview | 1 |\n| ☐ | `octolux health` returns `ERROR: Octolux server is not running` | 1 |\n| ☐ | `Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ws'` from server.js | 1 |\n| ☐ | `ERR_MODULE_NOT_FOUND` for any other npm package from server.js | 1 |\n| ☐ | `Error: listen EADDRINUSE: address already in use 0.0.0.0:8830` | 2 |\n| ☐ | `octolux health` OK but the Dashboard webview is blank | 3 |\n| ☐ | Server starts then dies a few seconds later (silent crash) | 4 |\n| ☐ | Dashboard section button (VNC / Deploy / Stream / Camera / Projects) does nothing on the **first** click | 5 |\n| ☐ | Dashboard section button does nothing on **every** click; server returns `{\"ok\":true,\"action\":\"created\"}` but no tab appears | 6 |\n| ☐ | `adom-cli hydrogen workspace …` returns `409: No browser session is connected. The editor tab may need to be refreshed.` | 6 |\n| ☐ | `add-tab returned 409` in `~/ntx-embedded/apps/octolux/server.js` log | 6 |\n| ☐ | Dashboard API returns `error code: 502` (no JSON body) — Adom HTTPS proxy strips 5xx bodies | 6 |\n\n\n---\nname: octolux-app-debug\ndescription: >\n  Debug the Octolux Node.js app server (the thing on port 8830 that serves\n  the Dashboard, Projects, Deploy, VNC, Screencast, Remote sections). Use\n  for both startup failures (dashboard tab won't load, `octolux health` is\n  failing, port 8830 is unbound, server crashes silently) and runtime\n  feature bugs (section buttons on the dashboard not opening, click does\n  nothing on first click, every click silently fails, tab created but\n  invisible, Hydrogen workspace mutations returning 409, server claiming\n  success but no tab actually appearing). Covers a generic diagnostic flow\n  (proxy `ECONNREFUSED` is ALWAYS upstream — read the server log, not the\n  proxy), six documented symptoms with root causes and fixes (including\n  the `cli()` wrapper bug that silently dropped Hydrogen 409s and the Adom\n  HTTPS proxy 5xx-body-stripping gotcha that requires HTTP 200 with\n  `{ok:false}` instead of 5xx), the \"do not do these\" rules (e.g. never\n  `pkill node` — VS Code runs as Node and you will kill the container\n  session), and prevention guidance for cutting recurrence on fresh\n  containers.\n  Scope: the Octolux app server only. NOT device-side issues (Weston,\n  Chromium kiosk, HMI won't boot) — those belong in `octolux-platform`.\n  NOT push-to-device flow — that's `octolux-deploy`.\n  Trigger words: octolux server not running, octolux dashboard error,\n  octolux dashboard blank, octolux dashboard won't load, octolux health\n  failing, octolux app server, octolux server.js crash, octolux 8830,\n  ECONNREFUSED 0.0.0.0:8830, octolux econnrefused, octolux ws module not\n  found, ERR_MODULE_NOT_FOUND octolux, octolux first run, octolux fresh\n  container, octolux app crashes on startup, octolux server log, where do\n  octolux logs go, can't reach octolux dashboard, octolux EADDRINUSE,\n  octolux port already in use, octolux silent crash, octolux vnc button\n  doesn't work, octolux dashboard button does nothing, clicking VNC opens\n  nothing, octolux deploy button broken, octolux section button first\n  click, dashboard tab created but invisible, hydrogen open-or-refresh\n  active-tab, hydrogen browser session disconnected, hydrogen 409, no\n  browser session is connected, hydrogen workspace mutations failing,\n  refresh hydrogen editor tab, octolux dashboard every click does nothing,\n  octolux server returns ok true but nothing happens, adom-cli silent\n  failure, execFile error swallowed, dashboard 502 stripped body, adom\n  proxy body stripping, octolux toast error.\n---\n\n# octolux-app-debug — debug the Octolux app server\n\n> **Scope.** This skill covers the **Octolux Node.js app server** in `~/ntx-embedded/apps/octolux/` — the thing that serves the Dashboard, Projects, Deploy, VNC, Screencast, and Remote sections on port 8830. Device-side issues (HMI won't boot, Chromium kiosk crashes, Weston compositor, audio, GPIO) belong in the `octolux-platform` skill. Deploy / push-to-device flow belongs in `octolux-deploy`.\n\n## When to use this skill\n\n- User reports the Octolux Dashboard webview is blank, shows a proxy error, or won't load.\n- `octolux health` returns anything other than `OK: Octolux server is running`.\n- Nothing is bound to port 8830 (`ss -ltn | awk '$4 ~ /:8830/'` is empty).\n- User says \"the dashboard isn't loading,\" \"the octolux server keeps dying,\" \"port 8830 isn't listening,\" \"ECONNREFUSED on the dashboard.\"\n\n## Diagnostic flow — start here, every time\n\n**The Hydrogen dashboard tab's `ECONNREFUSED 0.0.0.0:8830` is *always* upstream of the proxy.** It never means the proxy itself is broken; it means the server on the other side of the proxy is not listening. Diagnose upstream:\n\n```bash\n# 1. Is the server process alive?\noctolux health\n\n# 2. If not, did it crash on startup? Read the log.\ncat /tmp/octolux-server.log    # or wherever the server's stdout was redirected\n\n# 3. Is anything on the port?\nss -ltn | awk '$4 ~ /:8830/'\n\n# 4. Did npm install actually run?\nls ~/ntx-embedded/apps/octolux/node_modules/ws 2>/dev/null \\\n  && echo \"ws present\" || echo \"ws MISSING — run npm install\"\n```\n\nThe log is the source of truth. `octolux health` only tells you *that* the server is dead, not *why*. Skipping straight to \"restart it\" without reading the log is the #1 mistake — you'll restart it into the same crash and be back where you started.\n\n## Symptoms\n\nEach symptom below has a recognition pattern, the actual root cause, and the fix.\n\n### Symptom 1 — `ECONNREFUSED 0.0.0.0:8830` on the dashboard (and `node_modules/` is missing)\n\nThe fresh-container default. Documented in detail because most first-time users hit it.\n\n**Recognition**\n\n| Layer | What you see |\n|---|---|\n| Webview | Dashboard tab shows only `connect ECONNREFUSED 0.0.0.0:8830` |\n| CLI | `octolux health` → `ERROR: Octolux server is not running` |\n| Process | Nothing on port 8830 |\n| Server log | `Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ws' imported from .../server.js` |\n\n**Root cause.** `~/ntx-embedded/apps/octolux/node_modules/` was never populated — `ws` and other runtime deps are missing because the container doesn't pre-run `npm install` inside `apps/octolux/`, and there is no top-level `package.json` at `~/ntx-embedded/` that would cover it.\n\n**Fix.**\n\n```bash\ncd ~/ntx-embedded/apps/octolux\nnpm install --no-audit --no-fund\n\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\n\noctolux health\n#   → OK: Octolux server is running (octolux v1.0.0)\n```\n\nThen refresh the Dashboard tab:\n\n```bash\nadom-cli hydrogen webview open-or-refresh --name \"Octolux\" \\\n  --url \"$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')dashboard/\" \\\n  --panel-id \"$(adom-cli hydrogen workspace tabs 2>/dev/null \\\n      | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"tabs\"][0][\"panelId\"])' 2>/dev/null)\"\n```\n\n### Symptom 2 — `EADDRINUSE 0.0.0.0:8830` (or server log says the port is taken)\n\n**Recognition.** `octolux health` says the server isn't running, but `ss -ltn` shows something IS on port 8830. Starting a new server.js gets `Error: listen EADDRINUSE: address already in use 0.0.0.0:8830`.\n\n**Root cause.** A stale `node server.js` from a previous shell, or a different process that grabbed 8830.\n\n**Fix.** Find the specific PID and kill only that one — **never** `pkill node` (see *Do not do these*):\n\n```bash\npgrep -af 'node.*octolux/server.js'\n# → 12345 node /home/adom/ntx-embedded/apps/octolux/server.js\nkill 12345\n# verify the port is free, then start fresh:\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\n```\n\nIf `ss -ltn` shows 8830 held by a non-`node` process, that's a different problem — figure out what it is before killing it.\n\n### Symptom 3 — `octolux health` is OK but the dashboard is still blank\n\n**Recognition.** Server says it's running. `ss` confirms it's listening on 8830. But the webview tab is still empty or stuck.\n\n**Root cause.** Almost never the server. Two likely culprits:\n- The Hydrogen webview is caching the old `ECONNREFUSED` page and hasn't reloaded.\n- The browser-side JS in the dashboard page is failing (a CSP / cross-origin / proxy-rewrite issue).\n\n**Fix.** Refresh the tab via `adom-cli hydrogen webview open-or-refresh` (it does a hard reload). If still blank after that, capture the panel for inspection:\n\n```bash\nadom-cli hydrogen screenshot panel --name \"Octolux\" --reason \"dashboard blank despite healthy server\"\n```\n\nRead the screenshot for browser-side errors (often visible as a small text overlay in the corner) before assuming the server is bad.\n\n### Symptom 4 — server starts, then crashes a few seconds later\n\n**Recognition.** `octolux health` is briefly OK, then immediately starts returning errors again. Or `nohup` shows the process exited shortly after launch.\n\n**Root cause.** The server got past dependency loading and crashed inside its own init code — typically a missing env var, a malformed config file, or an uncaught exception inside a route handler that fires on startup.\n\n**Fix.** Tail the log live to catch the actual error:\n\n```bash\ntail -f /tmp/octolux-server.log\n# in another shell:\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\n```\n\nThen fix whatever the log surfaces — there is no canonical fix here, it depends on the specific exception.\n\n### Symptom 5 — section buttons (VNC / Deploy / Stream / Camera / Projects) do nothing on first click\n\nA runtime feature bug, not a startup failure. The server is healthy but the dashboard's per-device toolbar buttons appear dead on first interaction. Documented 2026-05-29.\n\n**Recognition.** `octolux health` is OK; the dashboard webview loads and renders the device cards. But clicking a section button on a device card — VNC, Deploy, Stream, Camera — or the top-bar Projects button does nothing the first time. The button looks like it depresses, then nothing visible happens. Curling the underlying endpoint directly returns success:\n\n```bash\nPROXY=$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')\ncurl -s -X POST \"${PROXY}dashboard/api/open-app\" \\\n  -H \"Content-Type: application/json\" -d '{\"app\":\"vnc\"}'\n# → {\"ok\":true,\"action\":\"created\",\"tab\":\"Octolux VNC\"}\n```\n\nThe server *does* create the tab. The user just can't see it.\n\n**Root cause.** In `~/ntx-embedded/apps/octolux/server.js`, the `/dashboard/api/open-app` handler creates the section tab via `adom-cli hydrogen webview open-or-refresh` but only foregrounds it (via `active-tab`) when the tab already existed. On a *first* click, the new tab is created in the same Hydrogen pane as the dashboard (the server correctly resolves the dashboard's pane via the `panelState.url` heuristic) but is placed *behind* the active dashboard tab. The dashboard stays foreground, so the user concludes the click did nothing.\n\nThe original code (look around line 723 of `server.js`):\n\n```javascript\n} else {\n  const args = ['hydrogen', 'webview', 'open-or-refresh', '--name', targetName, '--url', appUrl];\n  if (panelId) args.push('--panel-id', panelId);\n  await cli(...args);\n  if (tabExists) await cli('hydrogen', 'workspace', 'active-tab', '--name', targetName);  // ← bug\n  action = tabExists ? 'navigated' : 'created';\n}\n```\n\n**Fix.** Always call `active-tab` after `open-or-refresh`, regardless of `tabExists`:\n\n```javascript\n} else {\n  const args = ['hydrogen', 'webview', 'open-or-refresh', '--name', targetName, '--url', appUrl];\n  if (panelId) args.push('--panel-id', panelId);\n  await cli(...args);\n  // Always foreground the tab — a newly created tab is placed behind whichever tab\n  // is already active in the same pane (i.e. the Dashboard), so the user sees no\n  // change without this explicit activation.\n  await cli('hydrogen', 'workspace', 'active-tab', '--name', targetName);\n  action = tabExists ? 'navigated' : 'created';\n}\n```\n\nThen restart the server with the canonical \"kill specific PID, never `pkill`\" pattern:\n\n```bash\nOCTOLUX_PID=$(pgrep -af 'node.*octolux/server\\.js' | awk 'NR==1{print $1}')\n[ -n \"$OCTOLUX_PID\" ] && kill $OCTOLUX_PID\nsleep 1\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\noctolux health\n```\n\n**Verification.** After the restart, click any section button and confirm `isActive`:\n\n```bash\nadom-cli hydrogen workspace find-tab \"Octolux VNC\"\n# → \"isActive\": true\n```\n\n**Why this was easy to miss.** Once you've clicked a section button once in a session, the tab persists. Future clicks hit the `tabExists` branch and *do* call `active-tab`, so the button \"works\" — only the very first click is dead. If your dev workflow always opens a section before testing, you never see the bug. This is the kind of regression that wants a smoke test that runs against a *fresh* container state where no section tabs exist yet.\n\n**Scope of the bug.** Affects **all six** sections that route through `/dashboard/api/open-app`: VNC, Deploy, Stream (Screencast), Camera, plus the top-bar Projects button. It was reported as a VNC issue, but the fix applies to the whole class.\n\n### Symptom 6 — section buttons do nothing on *every* click because the Hydrogen browser session is disconnected\n\nSame surface symptom as Symptom 5, completely different root cause. If Symptom 5's fix is in place and clicking a section button *still* does nothing — including subsequent clicks, not just the first one — this is the case. Documented 2026-05-29.\n\n**Recognition.** All section buttons silently fail every time, not just the first click. `octolux health` is OK. The server logs `[open-app] failed: …` lines and the response body — when read against `localhost:8830` directly — contains:\n\n```text\nadom-cli hydrogen webview open-or-refresh ... failed:\n  {\"error\": \"add-tab returned 409: ...\"}\n  {\"error\": \"No browser session is connected.\n            The editor tab may need to be refreshed.\",\n   \"hint\":  \"Workspace mutations require a live browser session\n             to take effect. Try refreshing the Hydrogen editor tab.\"}\n```\n\nDiagnostic command:\n\n```bash\nPROXY=$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')\ncurl -s -X POST \"${PROXY}dashboard/api/open-app\" \\\n  -H \"Content-Type: application/json\" -d '{\"app\":\"vnc\"}'\n# → {\"ok\":false, \"error\":\"...409 No browser session...\", \"hint\":\"Refresh...\"}\n```\n\n**Root cause (the environmental one).** Hydrogen workspace mutations (`webview open-or-refresh`, `workspace add-tab`, `workspace active-tab`) require a **live browser-side connection** to Hydrogen — i.e., your editor browser tab open and connected to its workspace WebSocket. When that browser session drops (tab closed, network blip, sleep / wake), every workspace mutation from anywhere in the container fails with HTTP 409 from `hydrogen.adom.inc`.\n\n**Root cause (the server bug that hid it).** The original `cli()` wrapper in `~/ntx-embedded/apps/octolux/server.js` `/dashboard/api/open-app` handler resolved with `{err, stdout}` but callers only destructured `stdout` — so subprocess failures were silently dropped and the handler returned `{\"ok\":true,\"action\":\"created\"}` even when nothing had actually been created. The 409 from Hydrogen was completely invisible to the dashboard until you patched the wrapper.\n\n**Fix part 1 — make `cli()` actually surface failures** (the server change):\n\n```javascript\n// Before — silently drops errors:\nconst cli = (...args) => new Promise(r => {\n  execFile('adom-cli', args, { timeout: 8000 }, (err, stdout) => r({ err, stdout }));\n});\n\n// After — fails loudly when the CLI exited non-zero OR when stdout JSON\n// contains an `error` field (Hydrogen 409 surfaces this way: exit-0,\n// JSON body {\"error\": \"...\"}). Either case rejects so the handler can\n// return a proper error to the dashboard.\nconst cli = (...args) => new Promise((resolve, reject) => {\n  execFile('adom-cli', args, { timeout: 8000 }, (err, stdout, stderr) => {\n    let parsed = null;\n    try { parsed = JSON.parse(stdout || ''); } catch { /* not JSON */ }\n    if (err) {\n      const msg = (parsed && parsed.error) || (stderr && String(stderr).trim()) || err.message;\n      return reject(new Error(`adom-cli ${args.join(' ')} failed: ${msg}`));\n    }\n    if (parsed && parsed.error) {\n      return reject(new Error(`adom-cli ${args.join(' ')} failed: ${parsed.error}`));\n    }\n    resolve({ stdout, parsed });\n  });\n});\n```\n\nWrap the body of the handler in `try { ... } catch (e) { return json(res, 200, { ok: false, error: e.message, hint: ... }); }`. Detect the \"No browser session is connected\" substring and surface a user-facing hint pointing at the editor-tab refresh.\n\n**Fix part 2 — return 200 with `ok:false`, NOT 5xx** (the proxy gotcha):\n\nThe Adom container's HTTPS proxy at `*.adom.cloud` strips response bodies on 5xx (Cloudflare-style stock error page). If the server returns `HTTP 502 { ... }` the dashboard only sees `error code: 502` and the hint never reaches the user. Always return HTTP 200 with `{ ok: false, error: ..., hint: ... }` for application-level failures. The dashboard JS must key off `body.ok`, not `r.ok`:\n\n```javascript\nconst r = await fetch(base + '/api/open-app', { ... });\nconst body = await r.json().catch(() => ({}));\nif (!r.ok || body.ok === false) {\n  showToast(`Couldn't open ${action.toUpperCase()}`, body.error, body.hint);\n}\n```\n\n**Fix part 3 — show the toast on the dashboard** (the visible part):\n\nAdd a small toast renderer to `ui/dashboard/index.html` so the user sees the title + error + hint instead of a button that looks dead. The hint is the key payload — it tells them to refresh the editor tab.\n\n**The user-side recovery action.** Once these patches are in place, the user-side fix is always the same: refresh the Hydrogen editor browser tab (right-click → reload, or `Cmd/Ctrl-R`). That re-establishes the workspace session, and the next click works.\n\n**Why this kept reproducing in our session.** Symptom 5's fix to always-call-active-tab was correct, but Hydrogen had silently disconnected (e.g., the editor tab paused in the background or lost its WebSocket). With the old `cli()` wrapper, every `add-tab` / `open-or-refresh` after that point looked like it succeeded — including on the very first click and every subsequent click — so the surface symptom \"button looks dead\" persisted even though the *original* root cause was fixed. The lesson: **don't ignore subprocess errors**, and **don't rely on 5xx semantics through a body-stripping proxy**.\n\n**Verification.** After applying parts 1+2+3:\n\n```bash\nPROXY=$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')\ncurl -s -X POST \"${PROXY}dashboard/api/open-app\" \\\n  -H \"Content-Type: application/json\" -d '{\"app\":\"vnc\"}'\n```\n\nWhen Hydrogen is disconnected, the response body must contain `\"ok\":false`, a real `error` string, and `\"hint\":\"Refresh the Hydrogen editor browser tab ...\"`. Status code stays 200 so the proxy passes the body through.\n\n## Other ERR_MODULE_NOT_FOUND variants\n\n`ERR_MODULE_NOT_FOUND` for any package name, not just `ws`, has the same root cause as Symptom 1 (missing `node_modules/`). The fix is identical: `npm install` in `~/ntx-embedded/apps/octolux/`. If npm install completes successfully but a *different* module is still missing on the next start, the app's `package.json` is incomplete or out of sync with what `server.js` actually imports — file it as a bug against the app rather than working around it locally.\n\n## Do NOT do these\n\n- **Do not `pkill node`** — VS Code Server runs as Node in this container; killing all Node processes kills the editor and effectively ends the container session (only an Adom admin can restart it). Always find the specific Octolux server PID with `pgrep -af 'node.*octolux/server.js'` and kill only that PID.\n- **Do not bounce the container's HTTPS proxy** in response to `ECONNREFUSED 0.0.0.0:8830` — the proxy is working; it's accurately reporting an upstream failure. The fix is on the Octolux server side.\n- **Do not assume Octolux deploy or device-side issues share root causes** with app-server issues — they have separate skills (`octolux-deploy`, `octolux-platform`).\n- **Do not \"fix\" the dashboard by restarting the Hydrogen tab repeatedly** — if `ECONNREFUSED` is reproducing, the tab is doing its job. Diagnose the server first.\n\n## Preventing recurrence\n\nEvery new container hits Symptom 1 on first dashboard open. Options for cutting that recurrence (ordered cheapest → most invasive):\n\n1. **Document it in `~/.claude/CLAUDE.md` for this project.** Add a line under \"Quick Start\" or a new \"First-run setup\" section saying explicitly: *\"Run `cd ~/ntx-embedded/apps/octolux && npm install` before the first `octolux health` on a new container.\"* Cheap, no infrastructure change, surfaces to every Claude thread that loads the project context.\n2. **Auto-install on `octolux health` if `node_modules/` is missing.** Modify the `octolux` CLI's `health` command so that, before reporting \"server not running,\" it checks for `node_modules/ws` and if absent prints `HINT: deps are not installed. Run \\`cd ~/ntx-embedded/apps/octolux && npm install\\` first.` — converts the current bare error into a self-diagnosing one. The CLI already prints a `Hint:` line for the start command; this is the same pattern.\n3. **Add the npm install to the container bootstrap.** The gallia installer (`~/gallia/install.mjs`) runs on every fresh container — adding a step that `npm install`s any `~/ntx-embedded/apps/*` that has a `package.json` would eliminate the symptom entirely. More invasive (changes shared infrastructure), but the right long-term fix.\n4. **Have `server.js` print a useful error before crashing.** Wrap the top-level imports in a try/catch that detects `ERR_MODULE_NOT_FOUND` and prints `MISSING DEPS — run npm install in this directory` before re-throwing. Less helpful than option 2 because users still have to read the log.\n\nOption 1 is what we did today (via this skill itself, which now ships in the wiki). Option 2 is the right next step if the issue keeps biting people.\n\n## Why this happens (architecture)\n\nThe Adom Docker container ships the `~/ntx-embedded/` repo cloned in place but does **not** pre-run `npm install` inside `apps/octolux/`. There is no top-level `package.json` at `~/ntx-embedded/` that could cover it — the Octolux app's `package.json` is local to its own directory.\n\nWhen the user opens the Octolux Dashboard for the first time via `adom-cli hydrogen webview …`, Hydrogen opens a tab against the in-container HTTPS proxy at `…/proxy/8830/dashboard/`. The proxy hits `0.0.0.0:8830`, finds nothing listening, and renders the error page with body `connect ECONNREFUSED 0.0.0.0:8830`.\n\nIf the user follows the `octolux health` hint and runs `node server.js`, Node's ESM resolver fails on the first `import 'ws'` and exits before any port is bound. A `nohup`'d background start swallows the crash output — the user sees the same dashboard error and concludes \"the start command didn't work,\" when in fact the server briefly ran and died.\n\nThe fix is therefore always **(a) read the server log to confirm what actually crashed it, then (b) install the missing piece, then (c) start the server again** — never just \"try the start command harder.\"\n\n## File map\n\n| Path | What |\n|---|---|\n| `~/ntx-embedded/apps/octolux/server.js` | The Node.js app server entry point |\n| `~/ntx-embedded/apps/octolux/package.json` | The app's Node dep manifest |\n| `~/ntx-embedded/apps/octolux/node_modules/` | Created by `npm install` — must exist for server to start |\n| `/tmp/octolux-server.log` | Recommended stdout/stderr redirect for the detached server |\n| `/usr/local/bin/octolux` | CLI for `octolux health`, `octolux projects list`, `octolux deploy …`, etc. |\n| `/usr/local/bin/adom-cli` | Hydrogen webview / screenshot driver used to open the Dashboard tab |\n\n## Related skills\n\n- `octolux-platform` — device-side issues (Weston, Chromium kiosk, boot sequence, audio, GPIO). Use when the HMI device itself misbehaves, not when the app server misbehaves.\n- `octolux-deploy` — push HTML to a device, manage orientation, the deploy API. Use after the server is up and you're trying to ship an interface.\n- `octolux-hmi-design` — design the HTML/CSS for an HMI surface.\n- `adom-workspace-control` — open / refresh the Octolux Dashboard panel from any agent.\n",
  "skill_source": "# Octolux App Server — debug startup and runtime failures\n\nDiagnose any Octolux Node.js app server failure that breaks the Dashboard (startup or runtime). Six symptoms with root causes and fixes.\n\n## Known errors this skill resolves\n\nUntil the underlying bugs ship a fix, this skill is the reference for diagnosing and working around the items below. Strike a line off this list when the upstream fix lands.\n\n| | Error / symptom | Symptom # |\n|---|---|---|\n| ☐ | `connect ECONNREFUSED 0.0.0.0:8830` in the Dashboard webview | 1 |\n| ☐ | `octolux health` returns `ERROR: Octolux server is not running` | 1 |\n| ☐ | `Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ws'` from server.js | 1 |\n| ☐ | `ERR_MODULE_NOT_FOUND` for any other npm package from server.js | 1 |\n| ☐ | `Error: listen EADDRINUSE: address already in use 0.0.0.0:8830` | 2 |\n| ☐ | `octolux health` OK but the Dashboard webview is blank | 3 |\n| ☐ | Server starts then dies a few seconds later (silent crash) | 4 |\n| ☐ | Dashboard section button (VNC / Deploy / Stream / Camera / Projects) does nothing on the **first** click | 5 |\n| ☐ | Dashboard section button does nothing on **every** click; server returns `{\"ok\":true,\"action\":\"created\"}` but no tab appears | 6 |\n| ☐ | `adom-cli hydrogen workspace …` returns `409: No browser session is connected. The editor tab may need to be refreshed.` | 6 |\n| ☐ | `add-tab returned 409` in `~/ntx-embedded/apps/octolux/server.js` log | 6 |\n| ☐ | Dashboard API returns `error code: 502` (no JSON body) — Adom HTTPS proxy strips 5xx bodies | 6 |\n\n\n---\nname: octolux-app-debug\ndescription: >\n  Debug the Octolux Node.js app server (the thing on port 8830 that serves\n  the Dashboard, Projects, Deploy, VNC, Screencast, Remote sections). Use\n  for both startup failures (dashboard tab won't load, `octolux health` is\n  failing, port 8830 is unbound, server crashes silently) and runtime\n  feature bugs (section buttons on the dashboard not opening, click does\n  nothing on first click, every click silently fails, tab created but\n  invisible, Hydrogen workspace mutations returning 409, server claiming\n  success but no tab actually appearing). Covers a generic diagnostic flow\n  (proxy `ECONNREFUSED` is ALWAYS upstream — read the server log, not the\n  proxy), six documented symptoms with root causes and fixes (including\n  the `cli()` wrapper bug that silently dropped Hydrogen 409s and the Adom\n  HTTPS proxy 5xx-body-stripping gotcha that requires HTTP 200 with\n  `{ok:false}` instead of 5xx), the \"do not do these\" rules (e.g. never\n  `pkill node` — VS Code runs as Node and you will kill the container\n  session), and prevention guidance for cutting recurrence on fresh\n  containers.\n  Scope: the Octolux app server only. NOT device-side issues (Weston,\n  Chromium kiosk, HMI won't boot) — those belong in `octolux-platform`.\n  NOT push-to-device flow — that's `octolux-deploy`.\n  Trigger words: octolux server not running, octolux dashboard error,\n  octolux dashboard blank, octolux dashboard won't load, octolux health\n  failing, octolux app server, octolux server.js crash, octolux 8830,\n  ECONNREFUSED 0.0.0.0:8830, octolux econnrefused, octolux ws module not\n  found, ERR_MODULE_NOT_FOUND octolux, octolux first run, octolux fresh\n  container, octolux app crashes on startup, octolux server log, where do\n  octolux logs go, can't reach octolux dashboard, octolux EADDRINUSE,\n  octolux port already in use, octolux silent crash, octolux vnc button\n  doesn't work, octolux dashboard button does nothing, clicking VNC opens\n  nothing, octolux deploy button broken, octolux section button first\n  click, dashboard tab created but invisible, hydrogen open-or-refresh\n  active-tab, hydrogen browser session disconnected, hydrogen 409, no\n  browser session is connected, hydrogen workspace mutations failing,\n  refresh hydrogen editor tab, octolux dashboard every click does nothing,\n  octolux server returns ok true but nothing happens, adom-cli silent\n  failure, execFile error swallowed, dashboard 502 stripped body, adom\n  proxy body stripping, octolux toast error.\n---\n\n# octolux-app-debug — debug the Octolux app server\n\n> **Scope.** This skill covers the **Octolux Node.js app server** in `~/ntx-embedded/apps/octolux/` — the thing that serves the Dashboard, Projects, Deploy, VNC, Screencast, and Remote sections on port 8830. Device-side issues (HMI won't boot, Chromium kiosk crashes, Weston compositor, audio, GPIO) belong in the `octolux-platform` skill. Deploy / push-to-device flow belongs in `octolux-deploy`.\n\n## When to use this skill\n\n- User reports the Octolux Dashboard webview is blank, shows a proxy error, or won't load.\n- `octolux health` returns anything other than `OK: Octolux server is running`.\n- Nothing is bound to port 8830 (`ss -ltn | awk '$4 ~ /:8830/'` is empty).\n- User says \"the dashboard isn't loading,\" \"the octolux server keeps dying,\" \"port 8830 isn't listening,\" \"ECONNREFUSED on the dashboard.\"\n\n## Diagnostic flow — start here, every time\n\n**The Hydrogen dashboard tab's `ECONNREFUSED 0.0.0.0:8830` is *always* upstream of the proxy.** It never means the proxy itself is broken; it means the server on the other side of the proxy is not listening. Diagnose upstream:\n\n```bash\n# 1. Is the server process alive?\noctolux health\n\n# 2. If not, did it crash on startup? Read the log.\ncat /tmp/octolux-server.log    # or wherever the server's stdout was redirected\n\n# 3. Is anything on the port?\nss -ltn | awk '$4 ~ /:8830/'\n\n# 4. Did npm install actually run?\nls ~/ntx-embedded/apps/octolux/node_modules/ws 2>/dev/null \\\n  && echo \"ws present\" || echo \"ws MISSING — run npm install\"\n```\n\nThe log is the source of truth. `octolux health` only tells you *that* the server is dead, not *why*. Skipping straight to \"restart it\" without reading the log is the #1 mistake — you'll restart it into the same crash and be back where you started.\n\n## Symptoms\n\nEach symptom below has a recognition pattern, the actual root cause, and the fix.\n\n### Symptom 1 — `ECONNREFUSED 0.0.0.0:8830` on the dashboard (and `node_modules/` is missing)\n\nThe fresh-container default. Documented in detail because most first-time users hit it.\n\n**Recognition**\n\n| Layer | What you see |\n|---|---|\n| Webview | Dashboard tab shows only `connect ECONNREFUSED 0.0.0.0:8830` |\n| CLI | `octolux health` → `ERROR: Octolux server is not running` |\n| Process | Nothing on port 8830 |\n| Server log | `Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ws' imported from .../server.js` |\n\n**Root cause.** `~/ntx-embedded/apps/octolux/node_modules/` was never populated — `ws` and other runtime deps are missing because the container doesn't pre-run `npm install` inside `apps/octolux/`, and there is no top-level `package.json` at `~/ntx-embedded/` that would cover it.\n\n**Fix.**\n\n```bash\ncd ~/ntx-embedded/apps/octolux\nnpm install --no-audit --no-fund\n\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\n\noctolux health\n#   → OK: Octolux server is running (octolux v1.0.0)\n```\n\nThen refresh the Dashboard tab:\n\n```bash\nadom-cli hydrogen webview open-or-refresh --name \"Octolux\" \\\n  --url \"$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')dashboard/\" \\\n  --panel-id \"$(adom-cli hydrogen workspace tabs 2>/dev/null \\\n      | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"tabs\"][0][\"panelId\"])' 2>/dev/null)\"\n```\n\n### Symptom 2 — `EADDRINUSE 0.0.0.0:8830` (or server log says the port is taken)\n\n**Recognition.** `octolux health` says the server isn't running, but `ss -ltn` shows something IS on port 8830. Starting a new server.js gets `Error: listen EADDRINUSE: address already in use 0.0.0.0:8830`.\n\n**Root cause.** A stale `node server.js` from a previous shell, or a different process that grabbed 8830.\n\n**Fix.** Find the specific PID and kill only that one — **never** `pkill node` (see *Do not do these*):\n\n```bash\npgrep -af 'node.*octolux/server.js'\n# → 12345 node /home/adom/ntx-embedded/apps/octolux/server.js\nkill 12345\n# verify the port is free, then start fresh:\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\n```\n\nIf `ss -ltn` shows 8830 held by a non-`node` process, that's a different problem — figure out what it is before killing it.\n\n### Symptom 3 — `octolux health` is OK but the dashboard is still blank\n\n**Recognition.** Server says it's running. `ss` confirms it's listening on 8830. But the webview tab is still empty or stuck.\n\n**Root cause.** Almost never the server. Two likely culprits:\n- The Hydrogen webview is caching the old `ECONNREFUSED` page and hasn't reloaded.\n- The browser-side JS in the dashboard page is failing (a CSP / cross-origin / proxy-rewrite issue).\n\n**Fix.** Refresh the tab via `adom-cli hydrogen webview open-or-refresh` (it does a hard reload). If still blank after that, capture the panel for inspection:\n\n```bash\nadom-cli hydrogen screenshot panel --name \"Octolux\" --reason \"dashboard blank despite healthy server\"\n```\n\nRead the screenshot for browser-side errors (often visible as a small text overlay in the corner) before assuming the server is bad.\n\n### Symptom 4 — server starts, then crashes a few seconds later\n\n**Recognition.** `octolux health` is briefly OK, then immediately starts returning errors again. Or `nohup` shows the process exited shortly after launch.\n\n**Root cause.** The server got past dependency loading and crashed inside its own init code — typically a missing env var, a malformed config file, or an uncaught exception inside a route handler that fires on startup.\n\n**Fix.** Tail the log live to catch the actual error:\n\n```bash\ntail -f /tmp/octolux-server.log\n# in another shell:\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\n```\n\nThen fix whatever the log surfaces — there is no canonical fix here, it depends on the specific exception.\n\n### Symptom 5 — section buttons (VNC / Deploy / Stream / Camera / Projects) do nothing on first click\n\nA runtime feature bug, not a startup failure. The server is healthy but the dashboard's per-device toolbar buttons appear dead on first interaction. Documented 2026-05-29.\n\n**Recognition.** `octolux health` is OK; the dashboard webview loads and renders the device cards. But clicking a section button on a device card — VNC, Deploy, Stream, Camera — or the top-bar Projects button does nothing the first time. The button looks like it depresses, then nothing visible happens. Curling the underlying endpoint directly returns success:\n\n```bash\nPROXY=$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')\ncurl -s -X POST \"${PROXY}dashboard/api/open-app\" \\\n  -H \"Content-Type: application/json\" -d '{\"app\":\"vnc\"}'\n# → {\"ok\":true,\"action\":\"created\",\"tab\":\"Octolux VNC\"}\n```\n\nThe server *does* create the tab. The user just can't see it.\n\n**Root cause.** In `~/ntx-embedded/apps/octolux/server.js`, the `/dashboard/api/open-app` handler creates the section tab via `adom-cli hydrogen webview open-or-refresh` but only foregrounds it (via `active-tab`) when the tab already existed. On a *first* click, the new tab is created in the same Hydrogen pane as the dashboard (the server correctly resolves the dashboard's pane via the `panelState.url` heuristic) but is placed *behind* the active dashboard tab. The dashboard stays foreground, so the user concludes the click did nothing.\n\nThe original code (look around line 723 of `server.js`):\n\n```javascript\n} else {\n  const args = ['hydrogen', 'webview', 'open-or-refresh', '--name', targetName, '--url', appUrl];\n  if (panelId) args.push('--panel-id', panelId);\n  await cli(...args);\n  if (tabExists) await cli('hydrogen', 'workspace', 'active-tab', '--name', targetName);  // ← bug\n  action = tabExists ? 'navigated' : 'created';\n}\n```\n\n**Fix.** Always call `active-tab` after `open-or-refresh`, regardless of `tabExists`:\n\n```javascript\n} else {\n  const args = ['hydrogen', 'webview', 'open-or-refresh', '--name', targetName, '--url', appUrl];\n  if (panelId) args.push('--panel-id', panelId);\n  await cli(...args);\n  // Always foreground the tab — a newly created tab is placed behind whichever tab\n  // is already active in the same pane (i.e. the Dashboard), so the user sees no\n  // change without this explicit activation.\n  await cli('hydrogen', 'workspace', 'active-tab', '--name', targetName);\n  action = tabExists ? 'navigated' : 'created';\n}\n```\n\nThen restart the server with the canonical \"kill specific PID, never `pkill`\" pattern:\n\n```bash\nOCTOLUX_PID=$(pgrep -af 'node.*octolux/server\\.js' | awk 'NR==1{print $1}')\n[ -n \"$OCTOLUX_PID\" ] && kill $OCTOLUX_PID\nsleep 1\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\noctolux health\n```\n\n**Verification.** After the restart, click any section button and confirm `isActive`:\n\n```bash\nadom-cli hydrogen workspace find-tab \"Octolux VNC\"\n# → \"isActive\": true\n```\n\n**Why this was easy to miss.** Once you've clicked a section button once in a session, the tab persists. Future clicks hit the `tabExists` branch and *do* call `active-tab`, so the button \"works\" — only the very first click is dead. If your dev workflow always opens a section before testing, you never see the bug. This is the kind of regression that wants a smoke test that runs against a *fresh* container state where no section tabs exist yet.\n\n**Scope of the bug.** Affects **all six** sections that route through `/dashboard/api/open-app`: VNC, Deploy, Stream (Screencast), Camera, plus the top-bar Projects button. It was reported as a VNC issue, but the fix applies to the whole class.\n\n### Symptom 6 — section buttons do nothing on *every* click because the Hydrogen browser session is disconnected\n\nSame surface symptom as Symptom 5, completely different root cause. If Symptom 5's fix is in place and clicking a section button *still* does nothing — including subsequent clicks, not just the first one — this is the case. Documented 2026-05-29.\n\n**Recognition.** All section buttons silently fail every time, not just the first click. `octolux health` is OK. The server logs `[open-app] failed: …` lines and the response body — when read against `localhost:8830` directly — contains:\n\n```text\nadom-cli hydrogen webview open-or-refresh ... failed:\n  {\"error\": \"add-tab returned 409: ...\"}\n  {\"error\": \"No browser session is connected.\n            The editor tab may need to be refreshed.\",\n   \"hint\":  \"Workspace mutations require a live browser session\n             to take effect. Try refreshing the Hydrogen editor tab.\"}\n```\n\nDiagnostic command:\n\n```bash\nPROXY=$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')\ncurl -s -X POST \"${PROXY}dashboard/api/open-app\" \\\n  -H \"Content-Type: application/json\" -d '{\"app\":\"vnc\"}'\n# → {\"ok\":false, \"error\":\"...409 No browser session...\", \"hint\":\"Refresh...\"}\n```\n\n**Root cause (the environmental one).** Hydrogen workspace mutations (`webview open-or-refresh`, `workspace add-tab`, `workspace active-tab`) require a **live browser-side connection** to Hydrogen — i.e., your editor browser tab open and connected to its workspace WebSocket. When that browser session drops (tab closed, network blip, sleep / wake), every workspace mutation from anywhere in the container fails with HTTP 409 from `hydrogen.adom.inc`.\n\n**Root cause (the server bug that hid it).** The original `cli()` wrapper in `~/ntx-embedded/apps/octolux/server.js` `/dashboard/api/open-app` handler resolved with `{err, stdout}` but callers only destructured `stdout` — so subprocess failures were silently dropped and the handler returned `{\"ok\":true,\"action\":\"created\"}` even when nothing had actually been created. The 409 from Hydrogen was completely invisible to the dashboard until you patched the wrapper.\n\n**Fix part 1 — make `cli()` actually surface failures** (the server change):\n\n```javascript\n// Before — silently drops errors:\nconst cli = (...args) => new Promise(r => {\n  execFile('adom-cli', args, { timeout: 8000 }, (err, stdout) => r({ err, stdout }));\n});\n\n// After — fails loudly when the CLI exited non-zero OR when stdout JSON\n// contains an `error` field (Hydrogen 409 surfaces this way: exit-0,\n// JSON body {\"error\": \"...\"}). Either case rejects so the handler can\n// return a proper error to the dashboard.\nconst cli = (...args) => new Promise((resolve, reject) => {\n  execFile('adom-cli', args, { timeout: 8000 }, (err, stdout, stderr) => {\n    let parsed = null;\n    try { parsed = JSON.parse(stdout || ''); } catch { /* not JSON */ }\n    if (err) {\n      const msg = (parsed && parsed.error) || (stderr && String(stderr).trim()) || err.message;\n      return reject(new Error(`adom-cli ${args.join(' ')} failed: ${msg}`));\n    }\n    if (parsed && parsed.error) {\n      return reject(new Error(`adom-cli ${args.join(' ')} failed: ${parsed.error}`));\n    }\n    resolve({ stdout, parsed });\n  });\n});\n```\n\nWrap the body of the handler in `try { ... } catch (e) { return json(res, 200, { ok: false, error: e.message, hint: ... }); }`. Detect the \"No browser session is connected\" substring and surface a user-facing hint pointing at the editor-tab refresh.\n\n**Fix part 2 — return 200 with `ok:false`, NOT 5xx** (the proxy gotcha):\n\nThe Adom container's HTTPS proxy at `*.adom.cloud` strips response bodies on 5xx (Cloudflare-style stock error page). If the server returns `HTTP 502 { ... }` the dashboard only sees `error code: 502` and the hint never reaches the user. Always return HTTP 200 with `{ ok: false, error: ..., hint: ... }` for application-level failures. The dashboard JS must key off `body.ok`, not `r.ok`:\n\n```javascript\nconst r = await fetch(base + '/api/open-app', { ... });\nconst body = await r.json().catch(() => ({}));\nif (!r.ok || body.ok === false) {\n  showToast(`Couldn't open ${action.toUpperCase()}`, body.error, body.hint);\n}\n```\n\n**Fix part 3 — show the toast on the dashboard** (the visible part):\n\nAdd a small toast renderer to `ui/dashboard/index.html` so the user sees the title + error + hint instead of a button that looks dead. The hint is the key payload — it tells them to refresh the editor tab.\n\n**The user-side recovery action.** Once these patches are in place, the user-side fix is always the same: refresh the Hydrogen editor browser tab (right-click → reload, or `Cmd/Ctrl-R`). That re-establishes the workspace session, and the next click works.\n\n**Why this kept reproducing in our session.** Symptom 5's fix to always-call-active-tab was correct, but Hydrogen had silently disconnected (e.g., the editor tab paused in the background or lost its WebSocket). With the old `cli()` wrapper, every `add-tab` / `open-or-refresh` after that point looked like it succeeded — including on the very first click and every subsequent click — so the surface symptom \"button looks dead\" persisted even though the *original* root cause was fixed. The lesson: **don't ignore subprocess errors**, and **don't rely on 5xx semantics through a body-stripping proxy**.\n\n**Verification.** After applying parts 1+2+3:\n\n```bash\nPROXY=$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')\ncurl -s -X POST \"${PROXY}dashboard/api/open-app\" \\\n  -H \"Content-Type: application/json\" -d '{\"app\":\"vnc\"}'\n```\n\nWhen Hydrogen is disconnected, the response body must contain `\"ok\":false`, a real `error` string, and `\"hint\":\"Refresh the Hydrogen editor browser tab ...\"`. Status code stays 200 so the proxy passes the body through.\n\n## Other ERR_MODULE_NOT_FOUND variants\n\n`ERR_MODULE_NOT_FOUND` for any package name, not just `ws`, has the same root cause as Symptom 1 (missing `node_modules/`). The fix is identical: `npm install` in `~/ntx-embedded/apps/octolux/`. If npm install completes successfully but a *different* module is still missing on the next start, the app's `package.json` is incomplete or out of sync with what `server.js` actually imports — file it as a bug against the app rather than working around it locally.\n\n## Do NOT do these\n\n- **Do not `pkill node`** — VS Code Server runs as Node in this container; killing all Node processes kills the editor and effectively ends the container session (only an Adom admin can restart it). Always find the specific Octolux server PID with `pgrep -af 'node.*octolux/server.js'` and kill only that PID.\n- **Do not bounce the container's HTTPS proxy** in response to `ECONNREFUSED 0.0.0.0:8830` — the proxy is working; it's accurately reporting an upstream failure. The fix is on the Octolux server side.\n- **Do not assume Octolux deploy or device-side issues share root causes** with app-server issues — they have separate skills (`octolux-deploy`, `octolux-platform`).\n- **Do not \"fix\" the dashboard by restarting the Hydrogen tab repeatedly** — if `ECONNREFUSED` is reproducing, the tab is doing its job. Diagnose the server first.\n\n## Preventing recurrence\n\nEvery new container hits Symptom 1 on first dashboard open. Options for cutting that recurrence (ordered cheapest → most invasive):\n\n1. **Document it in `~/.claude/CLAUDE.md` for this project.** Add a line under \"Quick Start\" or a new \"First-run setup\" section saying explicitly: *\"Run `cd ~/ntx-embedded/apps/octolux && npm install` before the first `octolux health` on a new container.\"* Cheap, no infrastructure change, surfaces to every Claude thread that loads the project context.\n2. **Auto-install on `octolux health` if `node_modules/` is missing.** Modify the `octolux` CLI's `health` command so that, before reporting \"server not running,\" it checks for `node_modules/ws` and if absent prints `HINT: deps are not installed. Run \\`cd ~/ntx-embedded/apps/octolux && npm install\\` first.` — converts the current bare error into a self-diagnosing one. The CLI already prints a `Hint:` line for the start command; this is the same pattern.\n3. **Add the npm install to the container bootstrap.** The gallia installer (`~/gallia/install.mjs`) runs on every fresh container — adding a step that `npm install`s any `~/ntx-embedded/apps/*` that has a `package.json` would eliminate the symptom entirely. More invasive (changes shared infrastructure), but the right long-term fix.\n4. **Have `server.js` print a useful error before crashing.** Wrap the top-level imports in a try/catch that detects `ERR_MODULE_NOT_FOUND` and prints `MISSING DEPS — run npm install in this directory` before re-throwing. Less helpful than option 2 because users still have to read the log.\n\nOption 1 is what we did today (via this skill itself, which now ships in the wiki). Option 2 is the right next step if the issue keeps biting people.\n\n## Why this happens (architecture)\n\nThe Adom Docker container ships the `~/ntx-embedded/` repo cloned in place but does **not** pre-run `npm install` inside `apps/octolux/`. There is no top-level `package.json` at `~/ntx-embedded/` that could cover it — the Octolux app's `package.json` is local to its own directory.\n\nWhen the user opens the Octolux Dashboard for the first time via `adom-cli hydrogen webview …`, Hydrogen opens a tab against the in-container HTTPS proxy at `…/proxy/8830/dashboard/`. The proxy hits `0.0.0.0:8830`, finds nothing listening, and renders the error page with body `connect ECONNREFUSED 0.0.0.0:8830`.\n\nIf the user follows the `octolux health` hint and runs `node server.js`, Node's ESM resolver fails on the first `import 'ws'` and exits before any port is bound. A `nohup`'d background start swallows the crash output — the user sees the same dashboard error and concludes \"the start command didn't work,\" when in fact the server briefly ran and died.\n\nThe fix is therefore always **(a) read the server log to confirm what actually crashed it, then (b) install the missing piece, then (c) start the server again** — never just \"try the start command harder.\"\n\n## File map\n\n| Path | What |\n|---|---|\n| `~/ntx-embedded/apps/octolux/server.js` | The Node.js app server entry point |\n| `~/ntx-embedded/apps/octolux/package.json` | The app's Node dep manifest |\n| `~/ntx-embedded/apps/octolux/node_modules/` | Created by `npm install` — must exist for server to start |\n| `/tmp/octolux-server.log` | Recommended stdout/stderr redirect for the detached server |\n| `/usr/local/bin/octolux` | CLI for `octolux health`, `octolux projects list`, `octolux deploy …`, etc. |\n| `/usr/local/bin/adom-cli` | Hydrogen webview / screenshot driver used to open the Dashboard tab |\n\n## Related skills\n\n- `octolux-platform` — device-side issues (Weston, Chromium kiosk, boot sequence, audio, GPIO). Use when the HMI device itself misbehaves, not when the app server misbehaves.\n- `octolux-deploy` — push HTML to a device, manage orientation, the deploy API. Use after the server is up and you're trying to ship an interface.\n- `octolux-hmi-design` — design the HTML/CSS for an HMI surface.\n- `adom-workspace-control` — open / refresh the Octolux Dashboard panel from any agent.\n",
  "author": {
    "name": "Caleb",
    "email": "[email protected]"
  },
  "tags": [],
  "license": "proprietary",
  "visibility": {
    "public": true
  },
  "hero": null,
  "sample_prompts": [
    {
      "label": "Dashboard blank",
      "prompt": "The Octolux dashboard webview is blank — diagnose it"
    },
    {
      "label": "Server won't start",
      "prompt": "octolux health says the server isn't running"
    },
    {
      "label": "Port already in use",
      "prompt": "EADDRINUSE 0.0.0.0:8830 when I start the Octolux server"
    },
    {
      "label": "Module not found",
      "prompt": "ERR_MODULE_NOT_FOUND when I start the Octolux server"
    },
    {
      "label": "Silent crash",
      "prompt": "Octolux server starts and dies a few seconds later"
    },
    {
      "label": "Click does nothing",
      "prompt": "Clicking VNC (or Deploy / Stream) on the Octolux dashboard does absolutely nothing — every click, not just the first"
    },
    {
      "label": "Hydrogen 409",
      "prompt": "adom-cli is returning 409 'No browser session is connected' from open-or-refresh"
    },
    {
      "label": "Prevent recurrence",
      "prompt": "How do I stop new containers hitting the Octolux first-run ECONNREFUSED bug?"
    }
  ],
  "discovery_triggers": [
    "octolux server not running",
    "octolux dashboard error",
    "octolux dashboard blank",
    "octolux dashboard won't load",
    "octolux health failing",
    "octolux app server",
    "octolux server.js crash",
    "octolux 8830",
    "ECONNREFUSED 0.0.0.0:8830",
    "octolux econnrefused",
    "octolux ws module not found",
    "ERR_MODULE_NOT_FOUND octolux",
    "octolux first run",
    "octolux fresh container",
    "octolux server log",
    "where do octolux logs go",
    "can't reach octolux dashboard",
    "octolux EADDRINUSE",
    "octolux port already in use",
    "octolux silent crash",
    "octolux vnc button doesn't work",
    "octolux dashboard button does nothing",
    "clicking VNC opens nothing",
    "octolux deploy button broken",
    "octolux section button first click",
    "dashboard tab created but invisible",
    "hydrogen open-or-refresh active-tab",
    "hydrogen browser session disconnected",
    "no browser session is connected",
    "hydrogen 409",
    "hydrogen workspace mutations failing",
    "refresh hydrogen editor tab",
    "octolux dashboard every click does nothing",
    "octolux server returns ok true but nothing happens",
    "adom-cli silent failure",
    "execFile error swallowed",
    "dashboard 502 stripped body",
    "adom proxy body stripping",
    "octolux toast error"
  ],
  "discovery_pitch": "Debug the Octolux Node.js app server (port 8830). Covers six symptoms across startup failures (ECONNREFUSED, missing npm deps, EADDRINUSE, silent crashes) and runtime feature bugs — including the dashboard section-button bugs where (a) the first click creates a tab behind the dashboard and (b) every click silently fails because Hydrogen's workspace session has disconnected and the server's cli() wrapper drops the 409 on the floor. Also documents the Adom HTTPS proxy 5xx-body-stripping gotcha that forces using HTTP 200 with {ok:false}.",
  "metadata": {},
  "created_at": "2026-06-04T16:29:57.968Z",
  "updated_at": "2026-06-04T17:03:36.426Z",
  "source_path": "SKILL.md",
  "sub_skills": [],
  "parent_app": null,
  "readme": "## Known errors this skill resolves\n\nUntil the underlying bugs ship a fix, this skill is the reference for diagnosing and working around the items below. Strike a line off this list when the upstream fix lands.\n\n| | Error / symptom | Symptom # |\n|---|---|---|\n| ☐ | `connect ECONNREFUSED 0.0.0.0:8830` in the Dashboard webview | 1 |\n| ☐ | `octolux health` returns `ERROR: Octolux server is not running` | 1 |\n| ☐ | `Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ws'` from server.js | 1 |\n| ☐ | `ERR_MODULE_NOT_FOUND` for any other npm package from server.js | 1 |\n| ☐ | `Error: listen EADDRINUSE: address already in use 0.0.0.0:8830` | 2 |\n| ☐ | `octolux health` OK but the Dashboard webview is blank | 3 |\n| ☐ | Server starts then dies a few seconds later (silent crash) | 4 |\n| ☐ | Dashboard section button (VNC / Deploy / Stream / Camera / Projects) does nothing on the **first** click | 5 |\n| ☐ | Dashboard section button does nothing on **every** click; server returns `{\"ok\":true,\"action\":\"created\"}` but no tab appears | 6 |\n| ☐ | `adom-cli hydrogen workspace …` returns `409: No browser session is connected. The editor tab may need to be refreshed.` | 6 |\n| ☐ | `add-tab returned 409` in `~/ntx-embedded/apps/octolux/server.js` log | 6 |\n| ☐ | Dashboard API returns `error code: 502` (no JSON body) — Adom HTTPS proxy strips 5xx bodies | 6 |\n\n\n---\n\n---\nname: octolux-app-debug\ndescription: >\n  Debug the Octolux Node.js app server (the thing on port 8830 that serves\n  the Dashboard, Projects, Deploy, VNC, Screencast, Remote sections). Use\n  for both startup failures (dashboard tab won't load, `octolux health` is\n  failing, port 8830 is unbound, server crashes silently) and runtime\n  feature bugs (section buttons on the dashboard not opening, click does\n  nothing on first click, every click silently fails, tab created but\n  invisible, Hydrogen workspace mutations returning 409, server claiming\n  success but no tab actually appearing). Covers a generic diagnostic flow\n  (proxy `ECONNREFUSED` is ALWAYS upstream — read the server log, not the\n  proxy), six documented symptoms with root causes and fixes (including\n  the `cli()` wrapper bug that silently dropped Hydrogen 409s and the Adom\n  HTTPS proxy 5xx-body-stripping gotcha that requires HTTP 200 with\n  `{ok:false}` instead of 5xx), the \"do not do these\" rules (e.g. never\n  `pkill node` — VS Code runs as Node and you will kill the container\n  session), and prevention guidance for cutting recurrence on fresh\n  containers.\n  Scope: the Octolux app server only. NOT device-side issues (Weston,\n  Chromium kiosk, HMI won't boot) — those belong in `octolux-platform`.\n  NOT push-to-device flow — that's `octolux-deploy`.\n  Trigger words: octolux server not running, octolux dashboard error,\n  octolux dashboard blank, octolux dashboard won't load, octolux health\n  failing, octolux app server, octolux server.js crash, octolux 8830,\n  ECONNREFUSED 0.0.0.0:8830, octolux econnrefused, octolux ws module not\n  found, ERR_MODULE_NOT_FOUND octolux, octolux first run, octolux fresh\n  container, octolux app crashes on startup, octolux server log, where do\n  octolux logs go, can't reach octolux dashboard, octolux EADDRINUSE,\n  octolux port already in use, octolux silent crash, octolux vnc button\n  doesn't work, octolux dashboard button does nothing, clicking VNC opens\n  nothing, octolux deploy button broken, octolux section button first\n  click, dashboard tab created but invisible, hydrogen open-or-refresh\n  active-tab, hydrogen browser session disconnected, hydrogen 409, no\n  browser session is connected, hydrogen workspace mutations failing,\n  refresh hydrogen editor tab, octolux dashboard every click does nothing,\n  octolux server returns ok true but nothing happens, adom-cli silent\n  failure, execFile error swallowed, dashboard 502 stripped body, adom\n  proxy body stripping, octolux toast error.\n---\n\n# octolux-app-debug — debug the Octolux app server\n\n> **Scope.** This skill covers the **Octolux Node.js app server** in `~/ntx-embedded/apps/octolux/` — the thing that serves the Dashboard, Projects, Deploy, VNC, Screencast, and Remote sections on port 8830. Device-side issues (HMI won't boot, Chromium kiosk crashes, Weston compositor, audio, GPIO) belong in the `octolux-platform` skill. Deploy / push-to-device flow belongs in `octolux-deploy`.\n\n## When to use this skill\n\n- User reports the Octolux Dashboard webview is blank, shows a proxy error, or won't load.\n- `octolux health` returns anything other than `OK: Octolux server is running`.\n- Nothing is bound to port 8830 (`ss -ltn | awk '$4 ~ /:8830/'` is empty).\n- User says \"the dashboard isn't loading,\" \"the octolux server keeps dying,\" \"port 8830 isn't listening,\" \"ECONNREFUSED on the dashboard.\"\n\n## Diagnostic flow — start here, every time\n\n**The Hydrogen dashboard tab's `ECONNREFUSED 0.0.0.0:8830` is *always* upstream of the proxy.** It never means the proxy itself is broken; it means the server on the other side of the proxy is not listening. Diagnose upstream:\n\n```bash\n# 1. Is the server process alive?\noctolux health\n\n# 2. If not, did it crash on startup? Read the log.\ncat /tmp/octolux-server.log    # or wherever the server's stdout was redirected\n\n# 3. Is anything on the port?\nss -ltn | awk '$4 ~ /:8830/'\n\n# 4. Did npm install actually run?\nls ~/ntx-embedded/apps/octolux/node_modules/ws 2>/dev/null \\\n  && echo \"ws present\" || echo \"ws MISSING — run npm install\"\n```\n\nThe log is the source of truth. `octolux health` only tells you *that* the server is dead, not *why*. Skipping straight to \"restart it\" without reading the log is the #1 mistake — you'll restart it into the same crash and be back where you started.\n\n## Symptoms\n\nEach symptom below has a recognition pattern, the actual root cause, and the fix.\n\n### Symptom 1 — `ECONNREFUSED 0.0.0.0:8830` on the dashboard (and `node_modules/` is missing)\n\nThe fresh-container default. Documented in detail because most first-time users hit it.\n\n**Recognition**\n\n| Layer | What you see |\n|---|---|\n| Webview | Dashboard tab shows only `connect ECONNREFUSED 0.0.0.0:8830` |\n| CLI | `octolux health` → `ERROR: Octolux server is not running` |\n| Process | Nothing on port 8830 |\n| Server log | `Error [ERR_MODULE_NOT_FOUND]: Cannot find package 'ws' imported from .../server.js` |\n\n**Root cause.** `~/ntx-embedded/apps/octolux/node_modules/` was never populated — `ws` and other runtime deps are missing because the container doesn't pre-run `npm install` inside `apps/octolux/`, and there is no top-level `package.json` at `~/ntx-embedded/` that would cover it.\n\n**Fix.**\n\n```bash\ncd ~/ntx-embedded/apps/octolux\nnpm install --no-audit --no-fund\n\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\n\noctolux health\n#   → OK: Octolux server is running (octolux v1.0.0)\n```\n\nThen refresh the Dashboard tab:\n\n```bash\nadom-cli hydrogen webview open-or-refresh --name \"Octolux\" \\\n  --url \"$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')dashboard/\" \\\n  --panel-id \"$(adom-cli hydrogen workspace tabs 2>/dev/null \\\n      | python3 -c 'import sys,json; print(json.load(sys.stdin)[\"tabs\"][0][\"panelId\"])' 2>/dev/null)\"\n```\n\n### Symptom 2 — `EADDRINUSE 0.0.0.0:8830` (or server log says the port is taken)\n\n**Recognition.** `octolux health` says the server isn't running, but `ss -ltn` shows something IS on port 8830. Starting a new server.js gets `Error: listen EADDRINUSE: address already in use 0.0.0.0:8830`.\n\n**Root cause.** A stale `node server.js` from a previous shell, or a different process that grabbed 8830.\n\n**Fix.** Find the specific PID and kill only that one — **never** `pkill node` (see *Do not do these*):\n\n```bash\npgrep -af 'node.*octolux/server.js'\n# → 12345 node /home/adom/ntx-embedded/apps/octolux/server.js\nkill 12345\n# verify the port is free, then start fresh:\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\n```\n\nIf `ss -ltn` shows 8830 held by a non-`node` process, that's a different problem — figure out what it is before killing it.\n\n### Symptom 3 — `octolux health` is OK but the dashboard is still blank\n\n**Recognition.** Server says it's running. `ss` confirms it's listening on 8830. But the webview tab is still empty or stuck.\n\n**Root cause.** Almost never the server. Two likely culprits:\n- The Hydrogen webview is caching the old `ECONNREFUSED` page and hasn't reloaded.\n- The browser-side JS in the dashboard page is failing (a CSP / cross-origin / proxy-rewrite issue).\n\n**Fix.** Refresh the tab via `adom-cli hydrogen webview open-or-refresh` (it does a hard reload). If still blank after that, capture the panel for inspection:\n\n```bash\nadom-cli hydrogen screenshot panel --name \"Octolux\" --reason \"dashboard blank despite healthy server\"\n```\n\nRead the screenshot for browser-side errors (often visible as a small text overlay in the corner) before assuming the server is bad.\n\n### Symptom 4 — server starts, then crashes a few seconds later\n\n**Recognition.** `octolux health` is briefly OK, then immediately starts returning errors again. Or `nohup` shows the process exited shortly after launch.\n\n**Root cause.** The server got past dependency loading and crashed inside its own init code — typically a missing env var, a malformed config file, or an uncaught exception inside a route handler that fires on startup.\n\n**Fix.** Tail the log live to catch the actual error:\n\n```bash\ntail -f /tmp/octolux-server.log\n# in another shell:\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\n```\n\nThen fix whatever the log surfaces — there is no canonical fix here, it depends on the specific exception.\n\n### Symptom 5 — section buttons (VNC / Deploy / Stream / Camera / Projects) do nothing on first click\n\nA runtime feature bug, not a startup failure. The server is healthy but the dashboard's per-device toolbar buttons appear dead on first interaction. Documented 2026-05-29.\n\n**Recognition.** `octolux health` is OK; the dashboard webview loads and renders the device cards. But clicking a section button on a device card — VNC, Deploy, Stream, Camera — or the top-bar Projects button does nothing the first time. The button looks like it depresses, then nothing visible happens. Curling the underlying endpoint directly returns success:\n\n```bash\nPROXY=$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')\ncurl -s -X POST \"${PROXY}dashboard/api/open-app\" \\\n  -H \"Content-Type: application/json\" -d '{\"app\":\"vnc\"}'\n# → {\"ok\":true,\"action\":\"created\",\"tab\":\"Octolux VNC\"}\n```\n\nThe server *does* create the tab. The user just can't see it.\n\n**Root cause.** In `~/ntx-embedded/apps/octolux/server.js`, the `/dashboard/api/open-app` handler creates the section tab via `adom-cli hydrogen webview open-or-refresh` but only foregrounds it (via `active-tab`) when the tab already existed. On a *first* click, the new tab is created in the same Hydrogen pane as the dashboard (the server correctly resolves the dashboard's pane via the `panelState.url` heuristic) but is placed *behind* the active dashboard tab. The dashboard stays foreground, so the user concludes the click did nothing.\n\nThe original code (look around line 723 of `server.js`):\n\n```javascript\n} else {\n  const args = ['hydrogen', 'webview', 'open-or-refresh', '--name', targetName, '--url', appUrl];\n  if (panelId) args.push('--panel-id', panelId);\n  await cli(...args);\n  if (tabExists) await cli('hydrogen', 'workspace', 'active-tab', '--name', targetName);  // ← bug\n  action = tabExists ? 'navigated' : 'created';\n}\n```\n\n**Fix.** Always call `active-tab` after `open-or-refresh`, regardless of `tabExists`:\n\n```javascript\n} else {\n  const args = ['hydrogen', 'webview', 'open-or-refresh', '--name', targetName, '--url', appUrl];\n  if (panelId) args.push('--panel-id', panelId);\n  await cli(...args);\n  // Always foreground the tab — a newly created tab is placed behind whichever tab\n  // is already active in the same pane (i.e. the Dashboard), so the user sees no\n  // change without this explicit activation.\n  await cli('hydrogen', 'workspace', 'active-tab', '--name', targetName);\n  action = tabExists ? 'navigated' : 'created';\n}\n```\n\nThen restart the server with the canonical \"kill specific PID, never `pkill`\" pattern:\n\n```bash\nOCTOLUX_PID=$(pgrep -af 'node.*octolux/server\\.js' | awk 'NR==1{print $1}')\n[ -n \"$OCTOLUX_PID\" ] && kill $OCTOLUX_PID\nsleep 1\nnohup node ~/ntx-embedded/apps/octolux/server.js > /tmp/octolux-server.log 2>&1 &\noctolux health\n```\n\n**Verification.** After the restart, click any section button and confirm `isActive`:\n\n```bash\nadom-cli hydrogen workspace find-tab \"Octolux VNC\"\n# → \"isActive\": true\n```\n\n**Why this was easy to miss.** Once you've clicked a section button once in a session, the tab persists. Future clicks hit the `tabExists` branch and *do* call `active-tab`, so the button \"works\" — only the very first click is dead. If your dev workflow always opens a section before testing, you never see the bug. This is the kind of regression that wants a smoke test that runs against a *fresh* container state where no section tabs exist yet.\n\n**Scope of the bug.** Affects **all six** sections that route through `/dashboard/api/open-app`: VNC, Deploy, Stream (Screencast), Camera, plus the top-bar Projects button. It was reported as a VNC issue, but the fix applies to the whole class.\n\n### Symptom 6 — section buttons do nothing on *every* click because the Hydrogen browser session is disconnected\n\nSame surface symptom as Symptom 5, completely different root cause. If Symptom 5's fix is in place and clicking a section button *still* does nothing — including subsequent clicks, not just the first one — this is the case. Documented 2026-05-29.\n\n**Recognition.** All section buttons silently fail every time, not just the first click. `octolux health` is OK. The server logs `[open-app] failed: …` lines and the response body — when read against `localhost:8830` directly — contains:\n\n```text\nadom-cli hydrogen webview open-or-refresh ... failed:\n  {\"error\": \"add-tab returned 409: ...\"}\n  {\"error\": \"No browser session is connected.\n            The editor tab may need to be refreshed.\",\n   \"hint\":  \"Workspace mutations require a live browser session\n             to take effect. Try refreshing the Hydrogen editor tab.\"}\n```\n\nDiagnostic command:\n\n```bash\nPROXY=$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')\ncurl -s -X POST \"${PROXY}dashboard/api/open-app\" \\\n  -H \"Content-Type: application/json\" -d '{\"app\":\"vnc\"}'\n# → {\"ok\":false, \"error\":\"...409 No browser session...\", \"hint\":\"Refresh...\"}\n```\n\n**Root cause (the environmental one).** Hydrogen workspace mutations (`webview open-or-refresh`, `workspace add-tab`, `workspace active-tab`) require a **live browser-side connection** to Hydrogen — i.e., your editor browser tab open and connected to its workspace WebSocket. When that browser session drops (tab closed, network blip, sleep / wake), every workspace mutation from anywhere in the container fails with HTTP 409 from `hydrogen.adom.inc`.\n\n**Root cause (the server bug that hid it).** The original `cli()` wrapper in `~/ntx-embedded/apps/octolux/server.js` `/dashboard/api/open-app` handler resolved with `{err, stdout}` but callers only destructured `stdout` — so subprocess failures were silently dropped and the handler returned `{\"ok\":true,\"action\":\"created\"}` even when nothing had actually been created. The 409 from Hydrogen was completely invisible to the dashboard until you patched the wrapper.\n\n**Fix part 1 — make `cli()` actually surface failures** (the server change):\n\n```javascript\n// Before — silently drops errors:\nconst cli = (...args) => new Promise(r => {\n  execFile('adom-cli', args, { timeout: 8000 }, (err, stdout) => r({ err, stdout }));\n});\n\n// After — fails loudly when the CLI exited non-zero OR when stdout JSON\n// contains an `error` field (Hydrogen 409 surfaces this way: exit-0,\n// JSON body {\"error\": \"...\"}). Either case rejects so the handler can\n// return a proper error to the dashboard.\nconst cli = (...args) => new Promise((resolve, reject) => {\n  execFile('adom-cli', args, { timeout: 8000 }, (err, stdout, stderr) => {\n    let parsed = null;\n    try { parsed = JSON.parse(stdout || ''); } catch { /* not JSON */ }\n    if (err) {\n      const msg = (parsed && parsed.error) || (stderr && String(stderr).trim()) || err.message;\n      return reject(new Error(`adom-cli ${args.join(' ')} failed: ${msg}`));\n    }\n    if (parsed && parsed.error) {\n      return reject(new Error(`adom-cli ${args.join(' ')} failed: ${parsed.error}`));\n    }\n    resolve({ stdout, parsed });\n  });\n});\n```\n\nWrap the body of the handler in `try { ... } catch (e) { return json(res, 200, { ok: false, error: e.message, hint: ... }); }`. Detect the \"No browser session is connected\" substring and surface a user-facing hint pointing at the editor-tab refresh.\n\n**Fix part 2 — return 200 with `ok:false`, NOT 5xx** (the proxy gotcha):\n\nThe Adom container's HTTPS proxy at `*.adom.cloud` strips response bodies on 5xx (Cloudflare-style stock error page). If the server returns `HTTP 502 { ... }` the dashboard only sees `error code: 502` and the hint never reaches the user. Always return HTTP 200 with `{ ok: false, error: ..., hint: ... }` for application-level failures. The dashboard JS must key off `body.ok`, not `r.ok`:\n\n```javascript\nconst r = await fetch(base + '/api/open-app', { ... });\nconst body = await r.json().catch(() => ({}));\nif (!r.ok || body.ok === false) {\n  showToast(`Couldn't open ${action.toUpperCase()}`, body.error, body.hint);\n}\n```\n\n**Fix part 3 — show the toast on the dashboard** (the visible part):\n\nAdd a small toast renderer to `ui/dashboard/index.html` so the user sees the title + error + hint instead of a button that looks dead. The hint is the key payload — it tells them to refresh the editor tab.\n\n**The user-side recovery action.** Once these patches are in place, the user-side fix is always the same: refresh the Hydrogen editor browser tab (right-click → reload, or `Cmd/Ctrl-R`). That re-establishes the workspace session, and the next click works.\n\n**Why this kept reproducing in our session.** Symptom 5's fix to always-call-active-tab was correct, but Hydrogen had silently disconnected (e.g., the editor tab paused in the background or lost its WebSocket). With the old `cli()` wrapper, every `add-tab` / `open-or-refresh` after that point looked like it succeeded — including on the very first click and every subsequent click — so the surface symptom \"button looks dead\" persisted even though the *original* root cause was fixed. The lesson: **don't ignore subprocess errors**, and **don't rely on 5xx semantics through a body-stripping proxy**.\n\n**Verification.** After applying parts 1+2+3:\n\n```bash\nPROXY=$(echo $VSCODE_PROXY_URI | sed 's/{{port}}/8830/')\ncurl -s -X POST \"${PROXY}dashboard/api/open-app\" \\\n  -H \"Content-Type: application/json\" -d '{\"app\":\"vnc\"}'\n```\n\nWhen Hydrogen is disconnected, the response body must contain `\"ok\":false`, a real `error` string, and `\"hint\":\"Refresh the Hydrogen editor browser tab ...\"`. Status code stays 200 so the proxy passes the body through.\n\n## Other ERR_MODULE_NOT_FOUND variants\n\n`ERR_MODULE_NOT_FOUND` for any package name, not just `ws`, has the same root cause as Symptom 1 (missing `node_modules/`). The fix is identical: `npm install` in `~/ntx-embedded/apps/octolux/`. If npm install completes successfully but a *different* module is still missing on the next start, the app's `package.json` is incomplete or out of sync with what `server.js` actually imports — file it as a bug against the app rather than working around it locally.\n\n## Do NOT do these\n\n- **Do not `pkill node`** — VS Code Server runs as Node in this container; killing all Node processes kills the editor and effectively ends the container session (only an Adom admin can restart it). Always find the specific Octolux server PID with `pgrep -af 'node.*octolux/server.js'` and kill only that PID.\n- **Do not bounce the container's HTTPS proxy** in response to `ECONNREFUSED 0.0.0.0:8830` — the proxy is working; it's accurately reporting an upstream failure. The fix is on the Octolux server side.\n- **Do not assume Octolux deploy or device-side issues share root causes** with app-server issues — they have separate skills (`octolux-deploy`, `octolux-platform`).\n- **Do not \"fix\" the dashboard by restarting the Hydrogen tab repeatedly** — if `ECONNREFUSED` is reproducing, the tab is doing its job. Diagnose the server first.\n\n## Preventing recurrence\n\nEvery new container hits Symptom 1 on first dashboard open. Options for cutting that recurrence (ordered cheapest → most invasive):\n\n1. **Document it in `~/.claude/CLAUDE.md` for this project.** Add a line under \"Quick Start\" or a new \"First-run setup\" section saying explicitly: *\"Run `cd ~/ntx-embedded/apps/octolux && npm install` before the first `octolux health` on a new container.\"* Cheap, no infrastructure change, surfaces to every Claude thread that loads the project context.\n2. **Auto-install on `octolux health` if `node_modules/` is missing.** Modify the `octolux` CLI's `health` command so that, before reporting \"server not running,\" it checks for `node_modules/ws` and if absent prints `HINT: deps are not installed. Run \\`cd ~/ntx-embedded/apps/octolux && npm install\\` first.` — converts the current bare error into a self-diagnosing one. The CLI already prints a `Hint:` line for the start command; this is the same pattern.\n3. **Add the npm install to the container bootstrap.** The gallia installer (`~/gallia/install.mjs`) runs on every fresh container — adding a step that `npm install`s any `~/ntx-embedded/apps/*` that has a `package.json` would eliminate the symptom entirely. More invasive (changes shared infrastructure), but the right long-term fix.\n4. **Have `server.js` print a useful error before crashing.** Wrap the top-level imports in a try/catch that detects `ERR_MODULE_NOT_FOUND` and prints `MISSING DEPS — run npm install in this directory` before re-throwing. Less helpful than option 2 because users still have to read the log.\n\nOption 1 is what we did today (via this skill itself, which now ships in the wiki). Option 2 is the right next step if the issue keeps biting people.\n\n## Why this happens (architecture)\n\nThe Adom Docker container ships the `~/ntx-embedded/` repo cloned in place but does **not** pre-run `npm install` inside `apps/octolux/`. There is no top-level `package.json` at `~/ntx-embedded/` that could cover it — the Octolux app's `package.json` is local to its own directory.\n\nWhen the user opens the Octolux Dashboard for the first time via `adom-cli hydrogen webview …`, Hydrogen opens a tab against the in-container HTTPS proxy at `…/proxy/8830/dashboard/`. The proxy hits `0.0.0.0:8830`, finds nothing listening, and renders the error page with body `connect ECONNREFUSED 0.0.0.0:8830`.\n\nIf the user follows the `octolux health` hint and runs `node server.js`, Node's ESM resolver fails on the first `import 'ws'` and exits before any port is bound. A `nohup`'d background start swallows the crash output — the user sees the same dashboard error and concludes \"the start command didn't work,\" when in fact the server briefly ran and died.\n\nThe fix is therefore always **(a) read the server log to confirm what actually crashed it, then (b) install the missing piece, then (c) start the server again** — never just \"try the start command harder.\"\n\n## File map\n\n| Path | What |\n|---|---|\n| `~/ntx-embedded/apps/octolux/server.js` | The Node.js app server entry point |\n| `~/ntx-embedded/apps/octolux/package.json` | The app's Node dep manifest |\n| `~/ntx-embedded/apps/octolux/node_modules/` | Created by `npm install` — must exist for server to start |\n| `/tmp/octolux-server.log` | Recommended stdout/stderr redirect for the detached server |\n| `/usr/local/bin/octolux` | CLI for `octolux health`, `octolux projects list`, `octolux deploy …`, etc. |\n| `/usr/local/bin/adom-cli` | Hydrogen webview / screenshot driver used to open the Dashboard tab |\n\n## Related skills\n\n- `octolux-platform` — device-side issues (Weston, Chromium kiosk, boot sequence, audio, GPIO). Use when the HMI device itself misbehaves, not when the app server misbehaves.\n- `octolux-deploy` — push HTML to a device, manage orientation, the deploy API. Use after the server is up and you're trying to ship an interface.\n- `octolux-hmi-design` — design the HTML/CSS for an HMI surface.\n- `adom-workspace-control` — open / refresh the Octolux Dashboard panel from any agent.\n"
}