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:
2026-08-21 02:10:05 +00:00
parent 30b51ad7d3
commit 55309cfd86
18 changed files with 791 additions and 19 deletions
+27 -1
View File
@@ -1962,7 +1962,8 @@ Aggregate pending changes across all subsystems. Useful for the dashboard to sho
| Field | Type | Description |
|-------|------|-------------|
| `subsystems` | `object` | Map of subsystem name to pending status |
| `firewall` | `object` | `{ needs_apply, change_count, changes: [{summary, detail}] }` |
| `dnsmasq` / `nginx` / `wireguard` / `networkd` | `object` | `{ pending_changes, summary, changes: [{summary, detail}] }` |
| `total_changes` | `number` | Total count of pending changes across all subsystems |
---
@@ -1984,6 +1985,31 @@ Apply pending changes for all subsystems in dependency order.
---
#### Cancel All Pending Changes
```
POST /api/status/cancel-all
```
Revert pending changes for all subsystems to the last applied
configuration. Restores each pending subsystem's `config.json` from its
recorded `_last_applied_config` snapshot, discarding unapplied edits.
Subsystems without a recorded baseline (config never applied) are
reported as skipped and left untouched. No live-system commands run —
only the declarative config files are written.
**Request Body:** none.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `cancelled` | `[string, ...]` | Subsystems reverted to their last applied config |
| `skipped` | `object` | Map of subsystem label → reason (e.g. "No baseline recorded (never applied)") |
| `errors` | `object` | Map of subsystem label → error message |
---
#### Refresh State
```
+2
View File
@@ -538,6 +538,8 @@ Both `/api/firewall/zones/<name>/services` and `/api/firewall/config/apply` reco
**Management-lockout guard.** The firewalld *default zone* is the catch-all for interfaces with no explicit assignment (typically the WAN), and it carries the management plane (nginx https) plus remote recovery (ssh). Changing the default zone's service set so that **neither `https` nor `ssh`** remains raises `409 Conflict` — from `POST /firewall/zones/<name>/services` and `POST /firewall/config/apply` — before any mutation runs. Send `"force": true` in the request body to override (the UI shows a confirm dialog with this effect on the Zones page). If the default zone cannot be determined, the guard fails closed.
**Applied baseline.** Like the other config-backed subsystems, a successful apply records `_last_applied_hash` and `_last_applied_config` (the meta-stripped config snapshot) inside `config.json`. They are internal bookkeeping — ignored by all parsing, hashing, and UI surfaces — and let the aggregate cancel action (`POST /api/status/cancel-all`) revert this file to the last applied state. Configs that have never been applied have no baseline and are skipped by cancel.
## Networkd (IP Configuration)
**File**: `config/network/config.json`
+33
View File
@@ -1172,6 +1172,39 @@ Table({
})
```
### Apply / Cancel
`components/applyconfirm.js` — cross-subsystem apply/cancel buttons with a
shared expandable-subsystems modal. Both fetch `/api/status/pending` to
populate the modal rows (`buildRows()`; `SUBSYSTEM_LIST` order: firewall,
dnsmasq, nginx, wireguard, networkd).
#### `ApplyConfirm(props)`
Button that opens the confirmation modal listing pending subsystems, then
POSTs `/api/status/apply-all`. When `props.pending` is false it renders a
disabled "synced" button that toasts on click.
**Parameters:** `pending` (bool), `label`, `syncedLabel`, `cls`,
`successMsg`, `refresh` (legacy, ignored).
#### `CancelConfirm(props)`
Button that opens the confirmation modal listing the subsystems that
would be reverted ("Restores the listed subsystems to their last applied
configuration, discarding changes saved since the last apply"), then
POSTs `/api/status/cancel-all`. Success toast appends skipped-subsystem
details when the response has a non-empty `skipped` map; errors from the
response are toasted separately. State-store models update from the
daemon's WS delta — no explicit `modelFetch`.
**Parameters:** `label` (default `'Cancel All Changes'`), `cls`
(default `'btn btn-danger'`).
```javascript
CancelConfirm({ cls: 'btn btn-sm btn-danger' })
```
### Modal
#### `openModal(renderFn)`
+11
View File
@@ -17,6 +17,17 @@ return annotation references them.
- A subsystem whose collection failed holds `null`/`None` in the state
store — WS snapshots and deltas skip `null` payloads so a failed
collector never overwrites good client data.
- Config-backed subsystems record their applied baseline inside the config
file itself: `_last_applied_config` (the full merged config at last
apply) and `_last_applied_hash` (its SHA-256). A hash subsystem's
`status.pending_changes` is true when the current (merged) config hash
differs from the recorded hash; `status.pending_diff` lists the field
changes since that snapshot. All apply operations (including firewall
`config_apply`) re-stamp the baseline. These bookkeeping keys are
internal and stripped from every state/API config payload. Canceling
pending changes (`POST /api/status/cancel-all`) restores a pending
config file from its snapshot; a subsystem with no recorded baseline
(never applied) is reported as skipped, not reset.
## State shape summary