Add state management, WebSocket polling, html.js templating, and refactor pages
- lib/state.py: per-subsystem collectors with versioned state store - daemon/server.py: state refresh on request, batch routing updates - webui/static/hoover/html.js: new html tag template helper via htm.js - webui/static/hoover/websocket.js: real-time state change notifications - webui/static/hoover/vdom.js: VDOM improvements for keyed diff - All frontend pages refactored to use html templates - Add tests for state management and polling - Update docs and AGENTS.md
This commit is contained in:
+17
-1
@@ -1502,4 +1502,20 @@ GET /api/logs/app
|
||||
|
||||
Return recent application log entries as rendered HTML.
|
||||
|
||||
**Response:** HTML fragment of `<div class="log-line">` elements.
|
||||
**Response:** HTML fragment of `<div class="log-line">` elements.
|
||||
|
||||
---
|
||||
|
||||
## WebSocket Protocol
|
||||
|
||||
The daemon exposes a WebSocket at `/ws` (port 9091) for real-time state change notifications. On connect, the server sends:
|
||||
|
||||
```json
|
||||
{"type": "init", "versions": {"firewall": 0, "dnsmasq": 0, ...}}
|
||||
```
|
||||
|
||||
### 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.
|
||||
@@ -81,6 +81,30 @@ Vacuum Wall uses a declarative configuration model. Persistent user-facing confi
|
||||
| networkd | `config/network/config.json` | `data/networkd/` | `/etc/systemd/network/50-<name>.network` | The JSON file defines per-interface static addresses, routes, DNS, DHCP, and link settings. Each entry renders to a `50-<name>.network` INI file. Stale files are cleaned on apply. Public DNS servers are auto-synced to dnsmasq upstreams. |
|
||||
| ACME | N/A (`~/.acme.sh/` managed by acme.sh) | `data/acme/` | Certificate and key files | acme.sh manages its own state, renewal scheduling, and account keys. Vacuum Wall triggers issuance and renewal but does not maintain independent ACME state. |
|
||||
|
||||
#### Background Polling
|
||||
|
||||
The daemon runs background polling tasks for subsystems with external runtime state. Each subsystem has a configurable interval and a two-layer diff (structural vs volatile) to minimize unnecessary broadcasts.
|
||||
|
||||
| Subsystem | Interval | Rationale |
|
||||
|-----------|----------|-----------|
|
||||
| firewall | 30s | Most expensive collector (6+ subprocess calls) |
|
||||
| wireguard | 10s | Peer connections/handshakes change frequently |
|
||||
| dnsmasq | 10s | Lease file + service status |
|
||||
| networkd | 10s | Interface up/down, DHCP address changes |
|
||||
|
||||
nginx and acme are not polled — they have no external runtime state.
|
||||
|
||||
**Two-layer diff:** Each poll cycle classifies changes as:
|
||||
- **Structural change** (zones added, peers removed, config changed): triggers `bump()` + broadcast `{"type": "versions", ...}` → full UI re-load
|
||||
- **Volatile change only** (transfer counters, DHCP-assigned IPs): sends `{"type": "tick", "subsystems": [...]}` → lightweight per-subsystem re-fetch
|
||||
- **No change**: silence
|
||||
|
||||
Volatile fields per subsystem: `wireguard` (peer transfer/handshake stats), `firewall` (DHCP-assigned IPs), `networkd` (DHCP addresses, link metrics). Defined per collector via `register_volatile()`.
|
||||
|
||||
Poll intervals are configurable via `VACUUM_WALL_POLL_INTERVALS` env var (`firewall:30,wireguard:10,...`).
|
||||
|
||||
On collector failure during a poll, no broadcast is sent (avoids noisy ticks). State data is set to `None`.
|
||||
|
||||
## Directory Structure
|
||||
|
||||
### Config — Declarative Settings
|
||||
|
||||
@@ -261,6 +261,59 @@ h('#comp', { component: MyPage, key: '/dashboard' }, [])
|
||||
|
||||
**Children flattening:** `null`, `undefined`, and `false` children are filtered out. String and number primitives are automatically converted to text VNodes.
|
||||
|
||||
### HTM (Tagged HTML Templates)
|
||||
|
||||
Hoover ships with **htm** for JSX-like template syntax using tagged template literals. Import and use:
|
||||
|
||||
```javascript
|
||||
import { html, Badge, ConfirmDelete } from '/static/hoover/index.js';
|
||||
|
||||
// Instead of:
|
||||
h('div', { class: 'card' },
|
||||
h('h3', { style: 'color:red' }, 'Title'),
|
||||
h('button', { 'on:click': handler }, 'Click')
|
||||
)
|
||||
|
||||
// Write:
|
||||
html`<div class="card">
|
||||
<h3 style="color:red">Title</h3>
|
||||
<button onClick=${handler}>Click</button>
|
||||
</div>`
|
||||
```
|
||||
|
||||
**Event naming:** Use camelCase `onClick=${fn}` — the adapter translates events to Hoover's `on:click` convention. Any attribute starting with `on` followed by a capital letter (e.g., `onSubmit`, `onChange`) is converted.
|
||||
|
||||
**Component syntax:** Use `<${Component}>` syntax for inline components:
|
||||
|
||||
```javascript
|
||||
html`<${Badge} text=${val} variant="info" />`
|
||||
html`<${ConfirmDelete} url=${url} message=${msg} success="Deleted" refresh="firewall" />`
|
||||
```
|
||||
|
||||
**Interpolation:** Values are interpolated with `${...}`. Use `esc()` for user-controlled text:
|
||||
|
||||
```javascript
|
||||
html`<tr key=${item.id}>
|
||||
<td>${esc(item.name)}</td>
|
||||
<td>${item.value}</td>
|
||||
</tr>`
|
||||
```
|
||||
|
||||
**Spread attributes:** Use `...${props}` to spread an object as props:
|
||||
|
||||
```javascript
|
||||
html`<${Badge} ...${badgeProps} />`
|
||||
```
|
||||
|
||||
**Boolean attributes:** Use `html`<${Badge} readonly />`` for boolean attributes.
|
||||
|
||||
**Coexistence with `h()`:** Both `h` and `html` are exported from the barrel. Use whichever is clearer for the given context. Simple elements are often shorter with `h()`, while complex nested structures benefit from `html`.
|
||||
|
||||
**Limitations:**
|
||||
- No `<Badge>...</Badge>` closing syntax — must use self-closing `<${Badge} ... />` or full `<${Badge} ... ></${Badge}>` syntax
|
||||
- No control flow (`if/for`) in templates — use JavaScript conditionals and `.map()` before interpolation
|
||||
- `esc()` is still required for user-controlled text to prevent XSS
|
||||
|
||||
### Props
|
||||
|
||||
| Prop | Behavior |
|
||||
|
||||
Reference in New Issue
Block a user