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
+11 -10
View File
@@ -1,4 +1,4 @@
import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js';
import { html, PageHeader, Table, renderGuardMulti, enc, $val, apiFetch, toast, definePage, getModel, StatusText, QuickModal, ZoneSelect } from '/static/hoover/index.js';
async function changeZone(name, zone, state) {
const r = await apiFetch('/api/firewall/zones/' + enc(zone) + '/interfaces', {
@@ -7,8 +7,7 @@ async function changeZone(name, zone, state) {
});
if (r.ok) {
toast(name + ' \u2192 ' + zone, 'success');
modelFetch('firewall');
modelFetch('network');
// No modelFetch — daemon broadcasts both subsystems via WS delta.
} else {
toast(r.error || 'Failed', 'error');
}
@@ -33,7 +32,6 @@ const cfgModalFn = QuickModal({
}),
successMsg: 'Config saved',
},
refresh: ['firewall', 'network'],
});
export default definePage({
@@ -49,8 +47,11 @@ export default definePage({
const fwZones = state.firewall.data?.zones || {};
const netData = state.network.data?.interfaces || {};
const zones = fwZones.available || [];
const activeZones = fwZones.active || {};
const zones = Object.keys(fwZones);
const activeZones = state.firewall.data?.active_zones || {};
// Per-interface config lives in the top-level config (flat runtime
// entries carry no per-interface config).
const netCfgIfaces = state.network.data?.config?.interfaces || {};
// Loopback has no networkd config to manage — show real NICs only.
const ifaces = Object.entries(netData).filter(([name]) => name !== 'lo').map(([name, entry]) => {
@@ -63,11 +64,11 @@ export default definePage({
}
return {
name,
mac: entry?.runtime?.mac || null,
ips: [...(entry?.config?.addresses || []), ...(entry?.runtime?.addresses || [])],
state: (entry?.runtime?.state || '').startsWith('routable') || (entry?.runtime?.state || '').startsWith('carrier') ? 'up' : 'down',
mac: entry?.mac || null,
ips: [...(netCfgIfaces[name]?.addresses || []), ...(entry?.addresses || [])],
state: (entry?.state || '').startsWith('routable') || (entry?.state || '').startsWith('carrier') ? 'up' : 'down',
zone,
config: entry?.config || {},
config: netCfgIfaces[name] || {},
};
});