The OTBR frontend currently uses poll-based data fetching:
- Status and topology data are only loaded when the user navigates to a panel
- Device discovery polls
/api/actions/{id}every 500ms until completion - Diagnostic queries run 3-concurrent with per-device 500ms polling
- No live updates — the dashboard is static until the user manually refreshes
This means the user has no visibility into network changes (devices joining/leaving, link quality shifts, leader changes) unless they repeatedly click through panels.
- Push Thread network state changes to the browser in real time
- Stream progress for long-running operations (discovery, diagnostics)
- Keep the dashboard panel alive with current data (no manual refresh)
- Maintain air-gapped operation (no external dependencies)
Browser --> otbr-web (port 80) --> static responses
Browser --> otbr-agent REST (port 8081) --> JSON:API responses
Browser --> otbr-webui Fastify (port 80) --> static files + REST proxy + WebSocket
|
|-- proxy --> otbr-agent REST (port 8081)
|-- subprocess --> ot-ctl
|-- WebSocket /ws --> poll REST API, push diffs to clients
The Fastify backend serves static files, proxies /api/* to otbr-agent:8081,
provides /api/ot/* endpoints via ot-ctl subprocess, and runs a WebSocket
server at /ws for real-time push.
All messages are JSON over WebSocket. Each message has a type field.
// Subscribe to specific event types (sent after connect)
{
"type": "subscribe",
"topics": ["state", "devices", "properties", "events"]
}
// Request immediate refresh of a data type
{
"type": "refresh",
"topic": "devices"
}
// Trigger full topology discovery (replaces REST polling loop)
{
"type": "discover",
"options": {
"maxAge": 30,
"timeout": 15
}
}
// Request diagnostics for a specific device
{
"type": "diagnose",
"device": "1a2b3c4d5e6f7890",
"types": [0, 1, 2, 5, 6, 7, 8, 9, 14, 15, 16, 17, 19, 34, 35]
}Client Backend (Fastify)
| |
|---- WS connect ----------------->|
| |-- poll REST API
|<--- { type: "state", ... } ----|
|<--- { type: "devices", ... } --|
|<--- { type: "properties" } ----|
| |
|---- { type: "subscribe" } ----->|-- register topic filters
| |
| ... poll detects change ... |
|<--- { type: "state", ... } ----|
| |
|---- { type: "discover" } ------>|-- POST /api/actions
|<--- { type: "progress" } ------|-- poll action status
|<--- { type: "progress" } ------|-- ...
|<--- { type: "devices", ... } --|-- GET /api/devices
| |
|---- WS close ------------------>|-- cleanup subscription
The frontend handles reconnection with exponential backoff:
Initial delay: 1s
Max delay: 30s
Backoff factor: 2x
Jitter: +/-500ms
On reconnect, the server sends a full state snapshot automatically.
All existing REST fetch() calls remain. WebSocket is additive:
- If the backend WebSocket is not reachable -> UI works exactly as a traditional REST app (manual navigation triggers fetches)
- If WebSocket connects -> data arrives proactively, panels show fresh data before the user even navigates to them
- Discovery/diagnostics use WebSocket when available, fall back to REST polling when not
- WebSocket listens on the same port 80 as the HTTP server (same origin)
- No authentication on WebSocket (matches existing REST API)
- Read-only push for most messages;
discoveranddiagnosecommands are proxied to the REST API which has its own access controls - All hardened with systemd sandboxing
-
Poll interval tuning: 5s default for REST polling — is this acceptable for the "real-time" feel, or should it be shorter for certain data (e.g., 2s for device list)?
-
Multiple clients: Max 5 concurrent WebSocket connections. If memory is a concern on RPi5, this can be lowered.
-
HTTPS/WSS: If TLS is added later, the WebSocket upgrades to WSS automatically (same port, same TLS cert).