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
+22 -36
View File
@@ -7,7 +7,6 @@
import { h } from '../vdom.js';
import { esc } from '../helpers.js';
import { apiFetch, toast } from '../api.js';
import { modelFetch } from '../model.js';
import { requestUpdate } from '../reactivity.js';
const _actionPending = new Map();
@@ -69,16 +68,16 @@ export function Card(props = {}) {
}
/**
* A Remove button that confirms, deletes via API, toasts, and refreshes models.
* When the response includes a ``synced`` array (list of subsystem names
* that were auto-updated), shows a secondary toast and refreshes those
* models.
* A Remove button that confirms, deletes via API, and toasts. State-store
* models update from the daemon's WS delta — no explicit refresh. When the
* response includes a ``synced`` array (list of subsystem names that were
* auto-updated), appends them to the success toast.
*
* @param {object} props
* @param {string} props.url - API DELETE URL
* @param {string} props.message - Confirmation prompt text
* @param {string} [props.success] - Success toast message (default: 'Removed')
* @param {string|string[]} [props.refresh] - Model name(s) to refresh via modelFetch
* @param {string|string[]} [props.refresh] - Legacy, ignored (accepted for backward compat)
* @param {string} [props.label] - Button text (default: 'Remove')
* @param {object} [props.body] - Optional JSON body to send with DELETE
* @param {string} [props.deleteKey] - Unique ID for pending-delete row styling
@@ -104,31 +103,20 @@ export function ConfirmDelete(props = {}) {
let msg = props.success || 'Removed';
if (synced && synced.length) {
msg += ' (auto-synced: ' + synced.join(', ') + ')';
synced.forEach(s => modelFetch(s));
}
toast(msg, 'success');
if (props.deleteKey) {
_deleting.add(props.deleteKey);
// The WS delta (~50ms) removes the deleted item from
// model.data and re-renders the row away. This timeout
// purges _deleting if the delta is slow or the row was
// already unmounted.
setTimeout(() => _deleting.delete(props.deleteKey), 2000);
}
if (props.refresh) {
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
const promises = names.map(n => modelFetch(n));
if (props.deleteKey && promises.length) {
Promise.all(promises).finally(() => {
_deleting.delete(props.deleteKey);
if (props.onComplete) props.onComplete();
});
}
} else if (props.deleteKey) {
_deleting.delete(props.deleteKey);
}
if (props.onComplete && !props.refresh) {
props.onComplete();
}
if (props.onComplete) props.onComplete();
// No modelFetch — WS delta updates state store models.
} else {
toast(r.error || 'Failed', 'error');
}
@@ -141,11 +129,11 @@ export function ConfirmDelete(props = {}) {
}
/**
* An action button that POSTs to an API endpoint, toasts on result,
* and optionally refreshes models. Supports toggle labels for on/off buttons.
* When the response includes a ``synced`` array (list of subsystem names
* that were auto-updated), shows a secondary toast and refreshes those
* models.
* An action button that POSTs to an API endpoint and toasts on result.
* Supports toggle labels for on/off buttons. State-store models update from
* the daemon's WS delta — no explicit refresh. When the response includes a
* ``synced`` array (list of subsystem names that were auto-updated), appends
* them to the success toast.
*
* @param {object} props
* @param {string} props.url - API URL
@@ -157,7 +145,8 @@ export function ConfirmDelete(props = {}) {
* @param {boolean} [props.condition] - Toggle condition for labelOn/labelOff
* @param {string} [props.successMsg] - Success toast message
* @param {string} [props.errorType] - Toast type for errors (default: 'error')
* @param {string|string[]} [props.refresh] - Model name(s) to refresh via modelFetch
* @param {string|string[]} [props.refresh] - Legacy, ignored (accepted for backward compat)
* @param {function} [props.onSuccess] - Callback after the success toast
* @param {string} [props.cls] - Button CSS classes (default: 'btn btn-outline')
* @param {boolean} [props.disabled] - Disabled state
*/
@@ -187,13 +176,10 @@ export function ActionButton(props = {}) {
if (synced && synced.length) {
if (msg) msg += ' ';
msg += '(auto-synced: ' + synced.join(', ') + ')';
synced.forEach(s => modelFetch(s));
}
if (msg) toast(msg, 'success');
if (props.refresh) {
const names = Array.isArray(props.refresh) ? props.refresh : [props.refresh];
names.forEach(n => modelFetch(n));
}
if (props.onSuccess) props.onSuccess();
// No modelFetch — WS delta updates state store models.
} else {
toast(resp.error || 'Failed', props.errorType || 'error');
}
@@ -324,7 +310,7 @@ export function ServiceStatus(props = {}) {
* @param {string} props.removeUrl - API DELETE URL
* @param {string} props.removeMessage - Confirmation prompt text
* @param {string} [props.removeSuccess] - Success toast message
* @param {string|string[]} [props.removeRefresh] - Model name(s) to refresh
* @param {string|string[]} [props.removeRefresh] - Legacy, ignored (accepted for backward compat)
* @param {string} [props.removeLabel] - Delete button label (default: 'Remove')
* @param {object} [props.removeBody] - Optional JSON body to send with DELETE
* @param {string} [props.editCls] - Override classes for edit button (default: 'btn btn-sm btn-outline')