- move state collectors from lib/state.py to daemon/collectors/ (7
modules, registration side-effect; daemon/server.py imports the
package before the first populate())
- webui/api: new daemon_route() decorator factory in common.py
collapses the try/except daemon-proxy boilerplate in all 8
blueprints (rules/params/body/transform keep responses identical)
- firewall: interface-coverage invariant — config is the source of
truth for zone interfaces (absent key = empty, no hands-off
zones); pure validate_coverage() enforced at save (400) and apply
(409, force: true overrides), top-level `unmanaged` exemption
- lib: get_config() reads are now pure (no dir creation or writes);
new lib/bootstrap.py creates runtime dirs and persists the
one-shot nginx legacy migration at daemon start, after
system_import (lib.nginx.migrate_config_file)
- lib/common: compute_pending() apply-bookkeeping helper
- daemon: emit_and_refresh() handler helper; refresh_state(bump=) so
/status/refresh no longer bumps versions (poll/mutation only)
- acme: move --log last so acme.sh never treats a real arg as the
log-file argument
- docs: AGENTS.md, config.md, state-model.md, api.md updated;
HARDEN.md dropped (plan implemented); apply-confirm force wording
Tests: 917 passed; ruff check + format clean.
- 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
- lib/common: stamp_applied() now records a _last_applied_config
snapshot alongside the hash; strip_apply_meta() centralizes
bookkeeping-key stripping; deep_diff() reports field-level changes
- state collectors (dnsmasq/nginx/wireguard/networkd) expose
pending_diff so the dashboard can show exactly which fields
changed since the last apply (wireguard diff excludes
private_key paths)
- dashboard pending-changes card renders per-change lines with a
generic fallback when no snapshot is recorded
- firewall: firewalld built-in zones no longer flagged as
unmanaged; public-zone masquerade skipped in pending changes
since apply drives it via nftables propagation
- schema: PendingChange TypedDict; pending_diff on DnsmasqStatus /
WgStatus; tests in test_common.py, test_firewall.py, test_state.py