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
+9 -12
View File
@@ -10,7 +10,6 @@ import { h } from '../vdom.js';
import { html } from '../html.js';
import { reactive } from '../reactivity.js';
import { apiFetch, toast } from '../api.js';
import { modelFetch } from '../model.js';
import { openModal, closeModal, modalVNodes, isModalProcessing, setModalProcessing, refreshModals } from './modal.js';
export const SUBSYSTEM_LIST = [
@@ -56,9 +55,10 @@ ${hasPending ? html`<span class="apply-expand-icon${isExpanded ? ' expanded' : '
}
/**
* POST apply-all, toast result, close modal, refresh models.
* POST apply-all, toast result, close modal. State-store models update from
* the daemon's WS delta — no explicit refresh.
*/
async function doApply(successMsg, refreshTargets) {
async function doApply(successMsg) {
if (isModalProcessing()) return;
setModalProcessing(true);
try {
@@ -66,10 +66,7 @@ async function doApply(successMsg, refreshTargets) {
if (resp.ok) {
toast(successMsg, 'success');
closeModal();
if (refreshTargets) {
const names = Array.isArray(refreshTargets) ? refreshTargets : [refreshTargets];
names.forEach(n => modelFetch(n));
}
// No modelFetch — WS delta updates all affected subsystems.
} else {
toast(resp.error || 'Apply failed', 'error');
}
@@ -82,7 +79,7 @@ async function doApply(successMsg, refreshTargets) {
/**
* Fetch pending state, then open the confirmation modal.
*/
async function openApplyModal(successMsg, refreshTargets) {
async function openApplyModal(successMsg) {
const pendingResp = await apiFetch('/api/status/pending');
if (!pendingResp.ok) {
toast(pendingResp.error || 'Could not fetch pending changes', 'error');
@@ -109,7 +106,7 @@ async function openApplyModal(successMsg, refreshTargets) {
modalVNodes(inner, html`<div>
<h2 class="modal-title">Confirm: Apply All Changes</h2>
<div class="modal-body">${rows}</div>
<div class="apply-modal-actions"><button class="btn btn-outline" onClick="${() => closeModal()}">Cancel</button><button class="btn btn-primary" onClick="${() => doApply(successMsg, refreshTargets)}">Apply All</button></div>
<div class="apply-modal-actions"><button class="btn btn-outline" onClick="${() => closeModal()}">Cancel</button><button class="btn btn-primary" onClick="${() => doApply(successMsg)}">Apply All</button></div>
</div>`);
});
}
@@ -123,7 +120,7 @@ async function openApplyModal(successMsg, refreshTargets) {
* @param {string} [props.syncedLabel] - Synced button text (default: 'Synced')
* @param {string} [props.cls] - Button CSS classes (default: 'btn btn-primary' when pending, 'btn btn-outline' when synced)
* @param {string} [props.successMsg] - Success toast message (default: 'All changes applied')
* @param {string|string[]} [props.refresh] - Model name(s) to refresh after apply
* @param {string|string[]} [props.refresh] - Legacy, ignored (accepted for backward compat)
*/
export function ApplyConfirm(props = {}) {
const label = props.label || 'Apply';
@@ -139,7 +136,7 @@ export function ApplyConfirm(props = {}) {
toast(successMsg || 'All synced', 'info');
return;
}
openApplyModal(successMsg, props.refresh);
openApplyModal(successMsg);
},
}, props.pending ? label : syncedLabel);
}
}