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:
2026-08-20 01:38:00 +00:00
parent 9c9f92ad04
commit 332d14e37d
45 changed files with 2819 additions and 496 deletions
+4 -7
View File
@@ -1,4 +1,4 @@
import { html, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete } from '/static/hoover/index.js';
import { html, PageHeader, Badge, StatusDot, Card, Table, renderGuard, enc, $val, apiFetch, toast, definePage, getModel, ActionButton, DataTableSection, SectionTitle, ActionGroup, QuickModal, ConfirmDelete } from '/static/hoover/index.js';
const addFwd = QuickModal({
title: 'Add Port Forward',
@@ -21,7 +21,6 @@ const addFwd = QuickModal({
validate: (b) => !b.zone || !b.port || !b.proto ? 'Zone, port, and proto are required' : null,
successMsg: 'Forward rule added',
},
refresh: 'firewall',
});
export default definePage({
@@ -37,7 +36,7 @@ export default definePage({
const cfg = state.firewall.data?.config || {};
const zoneData = cfg.zones || {};
const sIface = (state.firewall.data?.state || {}).interfaces || [];
const sIface = state.firewall.data?.interfaces || [];
// With nftables, masquerade is propagated to the public zone at runtime for
// POSTROUTING to work. The config-side masquerade flag indicates which
// zones source NAT traffic (LAN / internal), not where traffic exits (WAN).
@@ -92,8 +91,7 @@ export default definePage({
cls="btn btn-sm btn-outline"
labelOn="Disable" labelOff="Enable" condition=${masq}
body=${() => ({ zone, enable: !masq })}
successMsg=${'Masquerade ' + (masq ? 'disabled' : 'enabled') + ' on ' + zone}
refresh="firewall" />
successMsg=${'Masquerade ' + (masq ? 'disabled' : 'enabled') + ' on ' + zone} />
</td>
</tr>`;
});
@@ -115,8 +113,7 @@ export default definePage({
url=${'/api/firewall/forward-port/' + enc(zone) + '/' + port + '/' + enc(proto)}
deleteKey=${zone + '/' + port + '/' + proto}
message=${'Remove forward ' + zone + ':' + port + '/' + proto + '?'}
success="Rule removed"
refresh="firewall" />
success="Rule removed" />
</td>
</tr>`);
});