ws: migrate push stream to data streaming
- daemon: send full snapshot on connect; versions/tick now carry the full state of one subsystem (subsystem + data); no legacy updated/subsystems payloads; refresh_state and POST /status/refresh broadcast per-subsystem versions with data - client: modelSet() patches models in place; onMessage/topic refresh retired; 3s initial-load fallback via new POST /api/status/refresh - schema: lib/schema.py TypedDicts + hoover/schema.js defaults + docs/state-model.md as single source of truth for state shapes - system: poll at 1s, volatile metrics registered, dashboard uses a dedicated system model (status model removed) - firewall: refuse to strip both https and ssh from the default zone (409, force override via UI confirm); set_zone_services persists services to the declarative config; collector exposes default_zone - UI: pages migrate to flat state shapes; post-mutation modelFetch refreshes removed (WS delta covers it) - tests: ws snapshot/delta/broadcast, refresh-state, schema types, model-set/js ws handler and reconnect fallback
This commit is contained in:
+49
-7
@@ -1947,6 +1947,40 @@ Apply pending changes for all subsystems in dependency order.
|
||||
| `applied` | `[string, ...]` | List of subsystems that were applied |
|
||||
| `errors` | `[object, ...]` | Any errors encountered during apply |
|
||||
|
||||
---
|
||||
|
||||
#### Refresh State
|
||||
|
||||
```
|
||||
POST /api/status/refresh
|
||||
```
|
||||
|
||||
Re-collect state from the daemon, optionally filtered by subsystem. Proxies the daemon's `POST /status/refresh`, which populates the state store for the requested subsystems, replies with their current state, and broadcasts a `versions` WS delta for each so all connected viewers stay in sync.
|
||||
|
||||
**Request Body** (optional — `{}` or omitted refreshes all subsystems):
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|-------|------|----------|-------------|
|
||||
| `subsystems` | `[string, ...]` | No | Subsystem names to refresh (e.g., `["firewall"]`) |
|
||||
|
||||
**Response (`data`):**
|
||||
|
||||
| Field | Type | Description |
|
||||
|-------|------|-------------|
|
||||
| — | `object` | Map of the requested subsystem name(s) to its full state dict (`null` = collector not populated / failed) |
|
||||
|
||||
**Example:**
|
||||
|
||||
```json
|
||||
// Request
|
||||
{"subsystems": ["firewall"]}
|
||||
|
||||
// Response
|
||||
{"ok": true, "data": {"firewall": {"config": {...}, "zones": {...}, "active_zones": {...}, "timestamp": "..."}}}
|
||||
```
|
||||
|
||||
Returns HTTP `500` if the daemon is unreachable.
|
||||
|
||||
### Sysctl
|
||||
|
||||
#### Set Kernel Parameter
|
||||
@@ -2069,14 +2103,22 @@ Returns HTTP `404` if the log file does not exist.
|
||||
|
||||
## WebSocket Protocol
|
||||
|
||||
The daemon exposes a WebSocket at `/ws` (port 9091) for real-time state change notifications. On connect, the server sends:
|
||||
The daemon exposes a WebSocket at `/ws` (port 9091) for real-time state streaming. After authentication, the server pushes a full state snapshot on connect and then per-subsystem deltas — the client patches models in place (`modelSet`) with no HTTP round-trip.
|
||||
|
||||
```json
|
||||
{"type": "init", "versions": {"firewall": 0, "dnsmasq": 0, ...}}
|
||||
```
|
||||
### Handshake Authentication
|
||||
|
||||
The JWT **access** token travels as the **raw `Sec-WebSocket-Protocol` subprotocol name** (the bundled client sends the bare token, no `Bearer ` prefix — subprotocol names must be valid RFC 6455 tokens). The daemon additionally accepts a legacy `Bearer <token>` subprotocol (non-browser clients) and an `X-Auth-Token` header fallback. The token is validated without session binding (browsers cannot send custom headers on the WebSocket handshake) but with the jti revocation check. A missing or invalid token yields HTTP `401` and no socket is opened.
|
||||
|
||||
### Message Types
|
||||
|
||||
- **`versions`** — Structural state change. `updated` contains subsystem names whose version counters changed. Triggers full re-fetch.
|
||||
- **`tick`** — Volatile-only change (stats, counters, DHCP IPs). `subsystems` contains affected subsystem names. Triggers lightweight per-subsystem re-fetch.
|
||||
- **`notify`** — Single-topic notification. `topic` is the subsystem name.
|
||||
| Type | Sent | Fields | Meaning |
|
||||
|------|------|--------|---------|
|
||||
| `snapshot` | On connect (after auth) | `data: {subsystem: state\|null, …}` | Full state for every subsystem. `null` = collector not populated / failed — clients skip those entries. |
|
||||
| `versions` | Structural change | `subsystem`, `data` | The full state of the one changed subsystem (zone added, config changed, …). Version counter bumped; data pushed. |
|
||||
| `tick` | Volatile-only change | `subsystem`, `data` | The full state of the one changed subsystem (stats/counters/DHCP IPs). No version bump. |
|
||||
|
||||
There is no legacy `updated` dict or `subsystems` array — each data-carrying message names a single `subsystem` and carries its full `data`.
|
||||
|
||||
### Manual Refresh
|
||||
|
||||
`POST /api/status/refresh` re-collects state (optionally filtered by a `subsystems` array) and broadcasts a `versions` delta for each requested subsystem. It is the HTTP fallback the client uses for the initial load (3s timer) and reconnect recovery. See the [Status API — Refresh State](#refresh-state) section for the full request/response contract.
|
||||
Reference in New Issue
Block a user