ui: amber pending-edit markers for unapplied config changes

Add hoover/dirty.js: line-matching helpers that flag UI rows/cards
edited (saved to config) but not yet applied, consuming the pending
state the daemon already streams — status.pending_diff for hash
subsystems, firewall pending zone+type for firewalld. Visual language
is amber (.config-dirty + PendingDot), distinct from the red
.pending-delete; orphanInfo surfaces removed entries (e.g. WireGuard
peers) on their container table. Wired into the backends, dhcp,
interfaces, nat, proxy, rules, wireguard, and zones pages; Card and
Table gain cls/title props. Covered by 27 node tests
(tests/test-dirty.js).
This commit is contained in:
2026-09-01 20:17:15 +00:00
parent 75b86fd60d
commit 89b64960f3
14 changed files with 649 additions and 61 deletions
+11 -7
View File
@@ -1,4 +1,4 @@
import { h, html, PageHeader, Badge, Empty, Table, renderGuard, renderGuardMulti, ConfirmDelete, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup } from '/static/hoover/index.js';
import { h, html, PageHeader, Badge, Empty, Table, renderGuard, renderGuardMulti, ConfirmDelete, esc, enc, $val, apiFetch, toast, definePage, getModel, modelFetch, ActionButton, ActionGroup, PendingDot, dirtySet, dirtyInfo } from '/static/hoover/index.js';
import { openModal, closeModal, isModalProcessing, setModalProcessing, refreshModals } from '/static/hoover/components/modal.js';
import { _deleting } from '/static/hoover/components/data.js';
@@ -260,18 +260,22 @@ export default definePage({
return {
backends: getModel('backends'),
dnsmasq: getModel('dnsmasq'),
nginx: getModel('nginx'),
};
},
render(state) {
const guard = renderGuardMulti('Backends', 'Reusable proxy backend templates', state.backends);
if (guard) return guard;
const set = dirtySet(state.nginx.data?.status);
const backends = state.backends.data || {};
const entries = Object.entries(backends);
const rows = entries.map(([name, b]) =>
html`<tr key=${name} class=${_deleting.has(name) ? 'pending-delete' : ''}>
<td><strong>${esc(name)}</strong></td>
const rows = entries.map(([name, b]) => {
const info = dirtyInfo(set, 'backends.' + name);
const cls = (_deleting.has(name) ? 'pending-delete' : '') + (info.class ? ' ' + info.class : '');
return html`<tr key=${name} class=${cls || undefined} title=${info.title || undefined}>
<td>${info.dirty ? PendingDot({}) : ''}<strong>${esc(name)}</strong></td>
<td>${esc(b.label || name)}</td>
<td>${Object.keys(b.paths || {}).length}</td>
<td>
@@ -291,11 +295,11 @@ export default definePage({
message=${'Remove backend ' + enc(name) + '?'}
success="Backend removed"
onComplete=${() => modelFetch('backends')}
label="Delete" />`
label="Delete" />`}
}
</td>
</tr>`
);
</tr>`;
});
const actions = ActionGroup(
h('button', { class: 'btn btn-primary', 'on:click': () => openBackendModal(state) }, 'Add Backend'),