status: cancel-all reverts pending changes to last applied config
- lib.common.revert_to_applied(): restore a config file from its
_last_applied_config snapshot (stamped hash); no baseline -> skip with
reason, file untouched
- firewall config_apply now stamps the applied baseline like the other
subsystems; GET /firewall/config and the state collector strip the
internal _last_applied_* keys
- POST /status/cancel-all + /api/status/cancel-all: revert pending
subsystems, {cancelled, skipped, errors}, partial-failure safe
- dashboard: "Cancel All Changes" button with confirm modal
(CancelConfirm, reuses the pending-changes modal rows); the pending
changes card is hidden entirely when nothing is pending
- tests: revert_to_applied, status_cancel_all, firewall stamping/meta
stripping, /api/status/cancel-all route, node tests for CancelConfirm;
firewall _config_apply tests no longer write the real repo config
- docs: api.md, state-model.md, config.md, hoover.md
This commit is contained in:
@@ -140,3 +140,91 @@ export function ApplyConfirm(props = {}) {
|
||||
},
|
||||
}, props.pending ? label : syncedLabel);
|
||||
}
|
||||
|
||||
/**
|
||||
* POST cancel-all, toast result, close modal. State-store models update from
|
||||
* the daemon's WS delta — no explicit refresh.
|
||||
*/
|
||||
async function doCancelAll() {
|
||||
if (isModalProcessing()) return;
|
||||
setModalProcessing(true);
|
||||
try {
|
||||
const resp = await apiFetch('/api/status/cancel-all', { method: 'POST' });
|
||||
if (resp.ok) {
|
||||
const data = resp.data || {};
|
||||
let msg = 'Pending changes cancelled';
|
||||
const nSkipped = Object.keys(data.skipped || {}).length;
|
||||
if (nSkipped) {
|
||||
msg += ` (${nSkipped} skipped: ` +
|
||||
Object.entries(data.skipped).map(([k, v]) => `${k} — ${v}`).join('; ') + ')';
|
||||
}
|
||||
toast(msg, nSkipped ? 'warning' : 'success', nSkipped ? 8000 : undefined);
|
||||
const errs = data.errors || {};
|
||||
const nErrs = Object.keys(errs).length;
|
||||
if (nErrs) {
|
||||
toast('Cancel failed for: ' +
|
||||
Object.entries(errs).map(([k, v]) => `${k} — ${v}`).join('; '),
|
||||
'error', 8000);
|
||||
}
|
||||
closeModal();
|
||||
// No modelFetch — WS delta updates all affected subsystems.
|
||||
} else {
|
||||
toast(resp.error || 'Cancel failed', 'error');
|
||||
}
|
||||
} finally {
|
||||
setModalProcessing(false);
|
||||
refreshModals();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch pending state, then open the cancel confirmation modal.
|
||||
*/
|
||||
async function openCancelModal() {
|
||||
const pendingResp = await apiFetch('/api/status/pending');
|
||||
if (!pendingResp.ok) {
|
||||
toast(pendingResp.error || 'Could not fetch pending changes', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
const pendingData = pendingResp.data || {};
|
||||
const totalChanges = pendingData.total_changes || 0;
|
||||
|
||||
const expanded = reactive({});
|
||||
|
||||
openModal((inner) => {
|
||||
if (totalChanges === 0) {
|
||||
modalVNodes(inner, html`<div>
|
||||
<h2 class="modal-title">Confirm: Cancel All Changes</h2>
|
||||
<div class="apply-no-changes">No pending changes to cancel.</div>
|
||||
<div class="apply-modal-actions"><button class="btn btn-outline" onClick="${() => closeModal()}">Close</button></div>
|
||||
</div>`);
|
||||
return;
|
||||
}
|
||||
|
||||
const rows = buildRows(pendingData, expanded);
|
||||
|
||||
modalVNodes(inner, html`<div>
|
||||
<h2 class="modal-title">Confirm: Cancel All Changes</h2>
|
||||
<p>Restores the listed subsystems to their last applied configuration, discarding changes saved since the last apply.</p>
|
||||
<div class="modal-body">${rows}</div>
|
||||
<div class="apply-modal-actions"><button class="btn btn-outline" onClick="${() => closeModal()}">Keep Changes</button><button class="btn btn-danger" onClick="${() => doCancelAll()}">Cancel All Changes</button></div>
|
||||
</div>`);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Cancel button with cross-subsystem confirmation modal.
|
||||
*
|
||||
* @param {object} props
|
||||
* @param {string} [props.label] - Button text (default: 'Cancel All Changes')
|
||||
* @param {string} [props.cls] - Button CSS classes (default: 'btn btn-danger')
|
||||
*/
|
||||
export function CancelConfirm(props = {}) {
|
||||
const label = props.label || 'Cancel All Changes';
|
||||
|
||||
return h('button', {
|
||||
class: props.cls !== undefined ? props.cls : 'btn btn-danger',
|
||||
'on:click': () => openCancelModal(),
|
||||
}, label);
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ export { Badge, StatusDot, Empty, Card, ConfirmDelete, Table, ActionButton, cert
|
||||
export { openModal, closeModal, closeAllModals, modalVNodes, refreshModals, formModal, MultiSelectModal, QuickModal, isModalProcessing, setModalProcessing } from './components/modal.js';
|
||||
|
||||
/* ── UI Components: Apply ────────────────────────────────────── */
|
||||
export { ApplyConfirm } from './components/applyconfirm.js';
|
||||
export { ApplyConfirm, CancelConfirm } from './components/applyconfirm.js';
|
||||
|
||||
/* ── UI Components: Toast ────────────────────────────────────── */
|
||||
export { ToastContainer } from './components/toast.js';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton, fmtBytes } from '/static/hoover/index.js';
|
||||
import { html, PageHeader, definePage, getModel, renderGuardMulti, ServiceStatus, StatCard, Table, Badge, ActionButton, CancelConfirm, fmtBytes } from '/static/hoover/index.js';
|
||||
|
||||
// Render a single firewall change as "current → new".
|
||||
// `live` is the currently applied value; `config` is the target value it
|
||||
@@ -144,7 +144,7 @@ export default definePage({
|
||||
meta=${expiringCerts.length ? expiringCerts.length + ' expiring' : 'All valid'} />
|
||||
</div>`;
|
||||
|
||||
// ── Pending changes ──
|
||||
// ── Pending changes (hidden entirely when nothing is pending) ──
|
||||
const pendingCard = pendingBlocks.length > 0
|
||||
? html`<div class="card">
|
||||
<div class="card-header">Pending Changes <span style="margin-left:8px"><${Badge} text=${String(totalChanges)} variant="warning" /></span></div>
|
||||
@@ -157,15 +157,15 @@ export default definePage({
|
||||
</ul>
|
||||
</li>`)}
|
||||
</ul>
|
||||
<${ActionButton} url="/api/status/apply-all" label="Apply All Changes"
|
||||
successMsg="All changes applied"
|
||||
cls="btn btn-sm btn-primary" />
|
||||
<div style="display:flex;gap:8px;flex-wrap:wrap">
|
||||
<${ActionButton} url="/api/status/apply-all" label="Apply All Changes"
|
||||
successMsg="All changes applied"
|
||||
cls="btn btn-sm btn-primary" />
|
||||
<${CancelConfirm} cls="btn btn-sm btn-danger" />
|
||||
</div>
|
||||
</div>
|
||||
</div>`
|
||||
: html`<div class="card">
|
||||
<div class="card-header">Pending Changes</div>
|
||||
<div class="card-body"><${Badge} text="All configured" variant="success" /></div>
|
||||
</div>`;
|
||||
: null;
|
||||
|
||||
// ── System resources ──
|
||||
const memPct = sysMem.used_pct || 0;
|
||||
|
||||
Reference in New Issue
Block a user