www-spa

May 26, 2026 · View on GitHub

Web UI for ZHAC — a Preact 10 + Vite 5 single-page app that talks to the S3 firmware over a single WebSocket (/ws). Built artefact is bundled into the S3 firmware's SPIFFS partition.

Features

  • 9 pages: Devices, Device detail, Groups, Scripts, Rules, Settings, Logs, Metrics, Alerts.
  • Shared WS client with request/reply + push-event handling.
  • Built-in Lua editor (CodeMirror 6, lazy-loaded).
  • Signals-based state (@preact/signals) — no Redux, no Context mess.
  • Single-chunk editor bundle (manual rollupOptions.manualChunks) so embedded httpd's socket pool doesn't get exhausted on first editor open.

Development

git clone https://github.com/zhac-project/www-spa.git
cd www-spa
npm ci
npm run dev          # Vite dev server on localhost:5173,
                     # proxies /ws → ws://localhost:8080

You need the S3 firmware running (or a WS stub) on localhost:8080 for the UI to have something to talk to.

Build for flashing

npm run build        # output in dist/

The S3 firmware's SPIFFS partition is generated from dist/.

Tree

www-spa/
├── src/
│   ├── pages/           (9 pages, 1 file per page)
│   ├── components/      (shared UI primitives)
│   ├── editor/          (CodeMirror theme, autocomplete, linter)
│   ├── stores/          (@preact/signals state containers)
│   ├── ws/              (WS client + envelope/reply matching)
│   ├── main.jsx
│   └── styles.css
├── public/
├── tools/
│   └── gen-zhac-completions.js  (parses docs/LUA_API.md → completions)
├── package.json
├── vite.config.js
└── index.html

Protocol

WebSocket only — no REST (one exception: POST /api/scripts/:name for Lua-script upload, which exceeds the httpd WS frame cap). Request/reply envelope:

client → { id, cmd, args }
server → { id, ok, data }   (success)
server → { id, ok: false, err: { code, msg } }   (error)

Push events (no id): device.added, device.updated, device.removed, attr.bulk, alert.*.

See WS_API.md in the zhac-docs repo for the full command list.

Routing

Hash-based, no router library. URLs look like #/devices, #/device/<ieee>, #/settings, etc. The hashchange listener in stores/ui.js keeps the ui.activePage signal and the URL in sync, so back/forward, manual hash edits, and bookmarks all replay the right page. navigate(page, { ieee }) updates location.hash; the re-entrant hashchange path then commits the signal.

Environment variables

VariableUsed byPurpose
ZHAC_LUA_DOCtools/gen-zhac-completionsAbsolute path to LUA_API.md. Overrides auto-discovery.

If LUA_API.md cannot be located, the generator emits an empty completions module and prints a warning instead of failing the build — fresh checkouts without a sibling zhac-docs/ clone still build.

Content Security Policy

The SPA does NOT ship a <meta http-equiv="Content-Security-Policy"> because a meta-tag CSP cannot cover WebSocket (connect-src for the ws:// / wss:// data path) on Chromium. CSP MUST be emitted by the S3 httpd as a response header on / and /assets/*. Recommended minimum (applied in the S3 firmware's esp_http_server config):

Content-Security-Policy:
    default-src 'self';
    connect-src 'self' ws: wss:;
    script-src  'self';
    style-src   'self' 'unsafe-inline';
    img-src     'self' data:;
    base-uri    'none';
    frame-ancestors 'none';

'unsafe-inline' is required by CodeMirror 6's inline style injection and cannot currently be tightened without forking CM. Everything user- authored (rule names, Lua source, log messages) is rendered via JSX text nodes — there is no dangerouslySetInnerHTML anywhere in src/.

Open-source readiness (deferred)

  • Dark theme (prefers-color-scheme: dark). The :root palette is light-only today; deferred because it requires re-vetting every Badge / Card / log-viewer colour pair against WCAG AA.
  • i18n. All strings are hardcoded English. Deferred — wire-up requires picking an i18n approach and adding extraction tooling.

License

GNU AGPL v3 or later. See LICENSE.

Contributing

See CONTRIBUTING.md. All contributions require signing CLA.md.

Versioning

Releases tagged vYYYYMMDDVV. See zhac-platform README for the scheme.