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:
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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')
|
||||
|
||||
@@ -217,7 +217,8 @@ export function formModal(inner, title, fields, actions) {
|
||||
* @param {string[]} props.selected - Currently selected values
|
||||
* @param {string} props.fieldKey - JSON key for the field
|
||||
* @param {string} [props.successMsg] - Success toast message
|
||||
* @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.confirm] - (body) => string|null; confirm gate, see apiSubmit
|
||||
* @returns {function} () => void, calls openModal
|
||||
*/
|
||||
export function MultiSelectModal(props = {}) {
|
||||
@@ -242,6 +243,7 @@ export function MultiSelectModal(props = {}) {
|
||||
}),
|
||||
successMsg: props.successMsg || 'Updated',
|
||||
refresh: props.refresh,
|
||||
confirm: props.confirm,
|
||||
closeModal: () => closeModal(),
|
||||
}),
|
||||
],
|
||||
@@ -263,7 +265,7 @@ export function MultiSelectModal(props = {}) {
|
||||
* @param {function} [props.submit.body] - (data) => object
|
||||
* @param {function} [props.submit.validate] - (body) => string|null
|
||||
* @param {string|function} [props.submit.successMsg] - Toast message or (data) => string
|
||||
* @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.handler] - Custom submit handler override (bypasses apiSubmit)
|
||||
* @param {string} [props.submitLabel] - Submit button label (default: 'Submit')
|
||||
* @returns {function} (data) => void, calls openModal
|
||||
|
||||
Reference in New Issue
Block a user