Skip to content

feat(server): manage the fleet from the browser - #1812

Open
clonea1 wants to merge 5 commits into
ruvnet:mainfrom
clonea1:contrib/server-node-management
Open

clonea1 wants to merge 5 commits into
ruvnet:mainfrom
clonea1:contrib/server-node-management

Conversation

@clonea1

@clonea1 clonea1 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Reconfiguring a node meant a USB cable and a serial console, or hand-written
curl against an address nobody recorded. This adds a Nodes tab that lists the
fleet and lets an operator read and change node configuration and see firmware
state, using the address each node reports on its own packets.

The server proxies rather than exposing the key. The firmware gates every
mutating endpoint (POST /config, /ota, /calibrate) behind a pre-shared key
checked in constant time. A browser must never hold that key -- a PSK
delivered to a web page is a PSK published to anything that can read the page
or its traffic -- so the server reads it from --ota-psk-file, keeps it in
memory, and never logs it or returns it from any endpoint.

FAILS CLOSED. Without --ota-psk-file, node management is disabled and only the
read-only fleet views are served. The key is never written to disk by the
server and never leaves it.

SECURITY NOTE FOR REVIEWERS. Once an operator does supply the key, the server's
web UI becomes the real trust boundary: anyone who can reach it can reconfigure
or reflash the fleet, because the server holds the credential on their behalf.
The firmware's authentication is intact but is no longer the control that
matters. This is a deliberate trade for a single-operator LAN deployment and it
is off by default, but a multi-user deployment wants an authentication layer in
front of the server before enabling it.


Rebased onto current main before opening: staged before today's seven merges, so it needed replaying to avoid reading as a revert of them. Clean rebase, no files deleted.

Joe and others added 3 commits September 4, 2026 15:02
Nothing in the system knew where a node lived. Nodes are configured with the
server's address and push to it, so the reverse mapping existed only inside
the datagrams themselves and was thrown away on arrival. Finding node 3 to
manage it meant scanning all 254 addresses for an open :8032 and asking each
responder its node_id -- and at a 1 s connect timeout that missed two of nine,
because a congested node answers slowly.

The UDP receiver already has the source address; it is now recorded on
NodeState at all three ingestion sites (CSI frames, vitals, sync packets) and
returned as `ip` on /api/v1/nodes.

Observed, never configured, so it follows a node across DHCP leases with no
intervention -- which matters, since node 3 changed address twice today.

This is the prerequisite for managing nodes from the web UI: the server cannot
proxy a config or firmware request to a node whose address it does not know.

Co-Authored-By: claude-flow <ruv@ruv.net>
(cherry picked from commit bb77643)
Reconfiguring a node meant a USB cable and a serial console, or hand-written
curl against an address nobody recorded. This adds a Nodes tab that lists the
fleet and lets an operator read and change node configuration and see firmware
state, using the address each node reports on its own packets.

The server proxies rather than exposing the key. The firmware gates every
mutating endpoint (POST /config, /ota, /calibrate) behind a pre-shared key
checked in constant time. A browser must never hold that key -- a PSK
delivered to a web page is a PSK published to anything that can read the page
or its traffic -- so the server reads it from --ota-psk-file, keeps it in
memory, and never logs it or returns it from any endpoint.

FAILS CLOSED. Without --ota-psk-file, node management is disabled and only the
read-only fleet views are served. The key is never written to disk by the
server and never leaves it.

SECURITY NOTE FOR REVIEWERS. Once an operator does supply the key, the server's
web UI becomes the real trust boundary: anyone who can reach it can reconfigure
or reflash the fleet, because the server holds the credential on their behalf.
The firmware's authentication is intact but is no longer the control that
matters. This is a deliberate trade for a single-operator LAN deployment and it
is off by default, but a multi-user deployment wants an authentication layer in
front of the server before enabling it.

Co-Authored-By: claude-flow <ruv@ruv.net>
The node table forced a Manage click for the two fields you most want when
scanning. Die temperature and firmware version are exactly what you look for to
find the odd node out of nine, and needing a click each to compare them defeats
the point of having a table. Both are now columns.

They are fetched differently on purpose:

- Temperature comes from `/api/v1/mesh`, which the server already collects for
  the whole fleet: one request, no per-node fan-out, no credentials in the
  browser. Coloured at the firmware's own warn threshold so the cell means what
  the node means. A node with no reading shows a dash, not 0 -- "0 C" and
  "sensor never installed" are different states, and conflating them would hide
  the failure actually worth seeing.
- Firmware has no fleet-wide endpoint, so it is one request per node through
  the existing `/api/v1/nodes/:id/firmware` proxy. Deliberately NOT awaited:
  the table paints from data it already has and versions fill in as they land,
  so one slow or unreachable node cannot delay the rows that are fine. Cached
  across refreshes so a node that answered once keeps its version on screen
  while briefly unreachable.

Scoped to this file. Our tree also carries a Tools menu listing several
diagnostic pages, but those pages are not in this repository yet, so bringing
the menu across would produce dead entries. Each page's menu line belongs in
the PR that adds that page.

Co-Authored-By: claude-flow <ruv@ruv.net>
@clonea1
clonea1 force-pushed the contrib/server-node-management branch from 13d2164 to 67d2b61 Compare September 4, 2026 21:14
Joe and others added 2 commits September 10, 2026 09:23
…r gateway

MEASURED 2026-09-09 on the live fleet: five of nine nodes had their recorded
address replaced by 172.18.0.1 -- the sink container's own bridge gateway --
at some point during a 13 h uptime. Every management proxy for those nodes
then failed: config pushes, firmware status, and the on-node log all resolved
to Terry's gateway rather than the board, and answered "node N at 172.18.0.1
did not answer" with nothing to say why.

The addresses were CORRECT at startup and rotted while running, and a restart
restored all nine. That is the tell that this is learned state going bad, not
routing: every node is provisioned to 192.168.1.29 and every node answers
directly on :8032, so nothing about the network was wrong.

last_src_ip is written from three sites that all read the same UDP socket, so
the server was faithfully recording a source that genuinely arrives as the
gateway. A CSI node reaches the sink through a published port from the LAN and
can never legitimately appear to originate at the container's own gateway, so
that source is Docker's, not a node's.

All three sites now go through record_node_src_ip, which refuses loopback and
unspecified outright, and refuses to REPLACE a known address with one from
Docker's 172.16.0.0/12 pool. It only declines to overwrite, never to learn, so
a fleet genuinely deployed on that range still resolves on first contact, and
a node that moves on the LAN is still followed.

Tests cover the observed failure and both directions of the guard, and were
negative-controlled: disabling it fails exactly the two tests that describe
the bug while the two "must still work" cases keep passing. 483 pass.

Co-Authored-By: claude-flow <ruv@ruv.net>
Claude-Session: https://claude.ai/code/session_01PVWMiHQifoYXL7uL3bphrZ

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant