Web console
nodes/web_console serves the board’s administration UI over HTTP, so that reflashing, reading the system over, watching node health and calling a service need nothing on the client but a browser. It is the answer to “everything here needs ssh, a workstation with the bundle, and someone who knows the incantations”.
Four views, mobile-first: Info (the landing page), Update, Health, Services.
It deliberately does not:
- Mark a slot good. The console reports what the bootloader says – slot, tries left, whether it has been marked – and stops there.
redline-mark-good.servicedoes the marking on the next boot, gated onredline-dashboard.service, so “good” keeps meaning the cluster painted a frame rather than the web server started. - Edit configuration. Deferred: validation is `dashboard –check –config
`, not a reimplemented YAML validator.
No authentication
Anything that can reach the port can drive the console, reflash included. The board sits on a trusted private network, so there is no token and no login. An earlier version had a bearer token; it was removed rather than left as a setting nobody used. token_file is now an unknown key, so a config that still asks for authentication stops the node instead of serving an open console its operator believes is protected. If the network stops being trusted, the token code is in git history.
Endpoints
| Route | ||
|---|---|---|
GET / | the page and its assets | |
GET /api/system | os-release, uptime, load, memory, filesystems, interfaces, temperatures | |
GET /api/update/status | RAUC operation, slots, boot entries, what is staged | |
POST /api/update/bundle | uploads a bundle, streamed to disk | |
POST /api/update/install | asks RAUC to install what is staged | |
GET /api/update/events | install progress (SSE) | |
POST /api/update/mark-good | marks the running slot good | |
GET /api/services | services offered on the bus | |
GET /api/schema | every schema name | |
GET /api/schema/<name> | describes one, for building a request | |
POST /api/call | calls a service |
/api/call reports what actually happened rather than flattening everything to 200:
200 | a responder replied |
400 | the body was malformed, or the fields did not fit the request schema; error or errors says which |
502 | the call failed |
504 | nobody answered |
timeout_ms is optional (2000 by default) and clamped to 100..10000.
The Services view builds its form from /api/schema/<name>, which is pub_sub::describeSchema(): exact types and integer bounds, enum values, nested structs, unions, lists, the schema’s defaults and its doc comments. So an enum is a dropdown, a bool a checkbox, an integer a numeric input bounded to its type (the phone keyboard follows), a float a decimal input, Data a hex field, a struct a nested block, a union a picker for its arm and a list rows to add and remove. A field left empty is left out of the request, so the schema’s default applies; the default is shown as the placeholder. 64-bit integers go out as exact JSON integers rather than through a JS Number.
Replies are rendered from the response schema’s description the same way: a summary line (the call’s status, HTTP code and time), any errors as a list, then each reply’s fields in declaration order with their doc comments on hover – enums by name, floats to six significant digits with the stored value on hover, Data as hex, nested structs and lists indented. A reply with the tree’s ok/message convention gets an ok/refused line at the top. The raw JSON is under a disclosure for anything the rendering does not show.
Reflash
One upload at a time: a second while one is in flight gets 409, as does an upload while RAUC is installing. The upload is pre-flighted against std::filesystem::space (507 when short, with the numbers); the bundle already staged counts as free, since the upload replaces it, and is removed first only when that is what makes room. The body is written to /data/updates/.incoming-XXXXXX.raucb (mkstemps, so O_EXCL), fsynced and closed – either failing fails the upload – then rename()d into place and the directory fsynced. Running out of space mid-upload is 507, not 400. The temporary is unlinked on any failure, and startup sweeps stale .incoming-*.
RAUC is reached over D-Bus by name (de.pengutronix.rauc.Installer), with no generated interface code: the XML ships only in rauc-dev, which is not on the image.
A board whose updater is broken still serves the rest of the console – system information and health are exactly what someone diagnosing that wants – so a missing RAUC is reported rather than fatal. rauc.service is D-Bus activated, so nothing owning the name is normal until the first call: rauc_available means only that the bus was reached (retried every few seconds if it was not), and the status call itself reports, in error and in the node’s rauc health check, when RAUC does not answer.
The browser will lose its connection when the install ends in a reboot. That is expected, and the page retries rather than erroring.
Health straight from the bus
The Health view reads the bus itself. redline.js and redline.wasm (built by tools/build-wasm.sh from wasm/, and by the image build on the board) hold a zenoh-pico session to zenohd’s ws/ listener on port 7446 of the same host, subscribe to nodes/*/health and the @redline/node identities, and classify with node_health::HealthTable – the table HealthMonitor wraps in inspect and the Qt app – so the verdicts are the node’s own code.
There is no server-side fallback, on purpose: it would mean the console watching every node’s health all the time for a page nobody may be looking at. When zenohd is unreachable the view says so and retries with backoff (2 s doubling to 30 s); from there inspect health over ssh and systemctl status redline-zenohd are the way in.
The session exists only while Health is showing and the tab is visible; leaving closes it, as leaving any view stops its polling.
Two things the module imposes on the page. Every bus* call can return a Promise (pico sleeps under ASYNCIFY), and two in flight at once corrupt the module, so one loop in app.js owns it. And the browser speaks zenoh without a WebSocket subprotocol: Emscripten asks for binary by default, zenohd answers with none, and browsers then fail the handshake – the reason this path once looked like a transport that never got an InitAck.
Config
configs/web_console/web_console.yaml:
bind_address: 0.0.0.0 # the code's default is 127.0.0.1
port: 8080
asset_dir: "" # empty: next to the executable
upload_dir: "${REDLINE_DATA_DIR}/updates"
rauc_bus: system # "session" only for tools/rauc_stub
bind_address is widened here rather than in the code so that a developer running this on a laptop is not exposed by accident.
On the target
An instance of the redline-node@.service template, with a drop-in that carries RequiresMountsFor=/data and creates /data/updates. Enabled through REDLINE_ENABLED_NODES on the machine.
Asset-only iteration needs no image rebuild: scp web/ to /data and point asset_dir at it through /data/nodes/web_console.args.
The page’s files are served with Cache-Control: no-cache and an ETag hashed from their content, so a browser checks on every load and gets a 304 only when the bytes match. Not by modification time: every file on the image carries the build’s fixed epoch (2011), so an mtime-based validator never changes between releases, and browsers kept running the previous release’s app.js after an update. A browser that cached the page before this fix still holds that old copy as fresh; one hard reload clears it.
Running it without the hardware
./build/nodes/web_console/web_console --config configs/web_console/web_console.yaml
- RAUC:
tools/rauc_stubis a fakede.pengutronix.rauc.Installerin C, with good/fail/busy modes, reached by settingrauc_bus: session. - The bus:
tools/bus-sandbox.shruns a command in a network namespace wherelohas multicast, which this workstation’s does not. - A service to call:
tools/fake_serviceoffersnodes/fake_backlight/set_brightness, since every real service-offering node wants hardware.
Not done yet
- Never built under Yocto, never run on the bench. Everything above is verified on a desktop; the recipe changes are reasoned-about.
- No authentication, no TLS, no firewall, and it runs as root like everything else on this image.
User=redline-webwithProtectSystem=strictis cheap functionally (RAUC’s D-Bus policy allowscontext="default"with no polkit) and is the obvious next step. - A plain node cannot be aimed at a router without recompiling.
PUB_SUB_NO_DISCOVERYis the only environment hook, and--connect/--modelive inlibs/cli, which plain nodes do not use. Fine while multicast works onlo; not fine oncezenohdbinds one interface under an ACL. - The direct Health path needs the wasm module on the board, and the image does not build it yet; until it does, every board serves Health via the console. It is proven on a desktop against
zenohdintools/bus-sandbox.sh, not on the bench. - Live plotting (what
scopedoes) is not here. The module already has a genericbusSubscribe(); what is missing is a canvas.