Skip to content

feat(cluster): LAN auto-discovery + unified read-only fleet view (v2.5)#18

Merged
haliskilic merged 4 commits into
mainfrom
feat-cluster
Jun 11, 2026
Merged

feat(cluster): LAN auto-discovery + unified read-only fleet view (v2.5)#18
haliskilic merged 4 commits into
mainfrom
feat-cluster

Conversation

@haliskilic

Copy link
Copy Markdown
Owner

What

Install ser2net on several machines on the same LAN, open any one of them, and see every node's mappings in one read-only table — each row tagged with the host it belongs to (name + IP). Requested feature: nodes find each other and show each other's mappings.

How it works

  • Discovery — signed UDP broadcast beacons (no mDNS). Each node broadcasts a beacon every 5s on cluster.discovery_port (default 41750), signed with HMAC-SHA256 over a shared cluster key. Only same-key nodes trust each other; mismatched/forged beacons are dropped. Peers expire after 20s of silence. (app/engine/cluster.py)
  • Trust = shared pre-shared key, opt-in, off by default (needs enabled + key). Set the same key on every node under Settings → LAN cluster.
  • Aggregation is server-side. The browser only talks to the node it logged into: session-authed GET /api/cluster/status fans out to each discovered peer's GET /api/cluster/local (guarded by the shared key, not a session), then renders the unified table. Remote rows are read-only with an Open link to that host's UI; local mappings stay fully managed on the normal dashboard.

Scope

Read-only federated view (per the agreed design). Remote control (start/stop/edit a peer's mappings from one screen) is the next ROADMAP item.

Tests

tests/test_cluster.py (added to the portable suite): beacon sign/verify, wrong-key + forged-beacon rejection, self-ignore, TTL expiry, key-guarded endpoint (403/403/200), and the aggregated table showing host + mapping. Full suite: 32/32 pass.

Notes

  • New config fields (cluster, instance_id) round-trip through config.json; absent on old configs → cluster simply stays off.
  • Version bumped to 2.5.0; README (TR/EN) + ROADMAP updated.

Halis KILIÇ added 4 commits June 11, 2026 21:54
Nodes on the same network discover each other via signed UDP broadcast
beacons (HMAC over a shared key; no mDNS) and one node aggregates every
node's mappings into a single read-only table on the dashboard, each row
tagged with its host (name + IP).

- app/engine/cluster.py: ClusterDiscovery — asyncio UDP beacon/listener,
  HMAC-signed beacons, TTL-pruned peer table, server-side peer fetch.
- config: ClusterSettings (enabled/key/discovery_port/advertise_ip) +
  persisted instance_id; opt-in, off by default (needs enable + shared key).
- web: GET /api/cluster/local (shared-key guarded, in PUBLIC_PATHS) and
  GET /api/cluster/status (session-authed, fans out to peers); Settings ->
  LAN cluster section; dashboard panel + _cluster_body.html unified table.
- tests/test_cluster.py: beacon sign/verify, wrong-key/forged rejection,
  self-ignore, TTL expiry, key-guarded endpoint, aggregated table.
- docs: README (TR/EN) + ROADMAP; version bumped to 2.5.0.
A wedged test must fail loudly with its partial output instead of hanging the
CI job to the 6-hour runner limit. Caps each test at 240s (longest real test
~7s) and adds a missing request timeout in the cluster test.
…3.12

Python 3.12 changed asyncio.Server.wait_closed() to block until every active
connection finishes (3.11 returned immediately). The Modbus gateway/poll tests
called bare `await server.wait_closed()` with a live handler connection still
open, hanging py3.12 CI to the 6-hour runner limit (3.10/3.11/3.13 passed).
Wrap each wait_closed() in asyncio.wait_for(timeout=2), matching how the engine
(modbus_gateway.py / bridge.py) already guards its own shutdown. Pre-existing on
main; surfaced now that run_all enforces a per-test timeout.
@haliskilic
haliskilic merged commit 2be1533 into main Jun 11, 2026
26 of 27 checks passed
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