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
+20 -20
View File
@@ -9,13 +9,13 @@ export default definePage({
wireguard: getModel('wireguard'),
acme: getModel('acme'),
nginx: getModel('nginx'),
status: getModel('status'),
system: getModel('system'),
};
},
render(state) {
const guard = renderGuardMulti('Dashboard', 'System overview',
state.firewall, state.network, state.dnsmasq, state.wireguard,
state.acme, state.nginx, state.status);
state.acme, state.nginx, state.system);
if (guard) return guard;
// Extract data
@@ -33,19 +33,22 @@ export default definePage({
const allCerts = state.acme.data?.certs || [];
const expiringCerts = allCerts.filter(c => c.expired || (c.days_remaining !== undefined && c.days_remaining <= 30));
// System metrics from status model
const sysMetrics = state.status.data?.metrics || {};
const sysLoad = sysMetrics.load || {};
const sysMem = sysMetrics.memory || {};
const sysSwap = sysMetrics.swap || {};
const sysTraffic = sysMetrics.traffic || {};
// System metrics from the system model
const sysLoad = state.system.data?.load || {};
const sysMem = state.system.data?.memory || {};
const sysSwap = state.system.data?.swap || {};
const sysTraffic = state.system.data?.traffic || {};
// Pending changes
const pend = state.status.data?.pending || {};
const totalChanges = pend.total_changes || 0;
const pendKeys = ['firewall', 'dnsmasq', 'nginx', 'wireguard', 'networkd'].filter(k =>
k === 'firewall' ? (pend[k]?.needs_apply) : (pend[k]?.pending_changes)
);
// Pending changes — derived from the config-backed subsystem models.
// Firewall uses pending.needs_apply (config_pending() output); all
// others use status.pending_changes. `system` is metrics-only.
const pendKeys = ['firewall', 'dnsmasq', 'nginx', 'wireguard', 'networkd'].filter(k => {
const model = getModel(k === 'networkd' ? 'network' : k);
const d = model.data || {};
if (k === 'firewall') return !!d.pending?.needs_apply;
return !!d.status?.pending_changes;
});
const totalChanges = pendKeys.length;
const pendLabels = { firewall: 'Firewall', dnsmasq: 'DHCP', nginx: 'Proxy', wireguard: 'WireGuard', networkd: 'Network' };
// Build merged interface list
@@ -53,13 +56,10 @@ export default definePage({
const ifaces = allNames.map(name => {
const fw = fwIfaces.find(f => f.name === name);
const netEntry = netIfaces[name] || {};
// /api/network/interfaces returns {config, runtime} per interface —
// state fields (state, addresses, mac) live under runtime.
const runtime = netEntry.runtime || {};
const traffic = sysTraffic[name] || {};
const ips = fw ? [...(fw.ips || []), ...(fw.ipv6 || [])] : [];
const addrs = runtime.addresses || [];
const isUp = ['routable', 'degraded', 'carrier'].some(s => (runtime.state || '').startsWith(s));
const addrs = netEntry.addresses || [];
const isUp = ['routable', 'degraded', 'carrier'].some(s => (netEntry.state || '').startsWith(s));
return {
name,
mac: fw?.mac || null,
@@ -91,7 +91,7 @@ export default definePage({
<div class="card-body">
<p class="text-sm">Unapplied changes in: ${pendKeys.map(k => pendLabels[k]).join(', ')}</p>
<${ActionButton} url="/api/status/apply-all" label="Apply All Changes"
successMsg="All changes applied" refresh="status"
successMsg="All changes applied"
cls="btn btn-sm btn-primary" />
</div>
</div>`