state: applied-config snapshots + per-field pending diffs

- 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
This commit is contained in:
2026-08-21 00:59:19 +00:00
parent a77cee821b
commit 30b51ad7d3
14 changed files with 525 additions and 62 deletions
+23
View File
@@ -134,6 +134,23 @@ class DnsmasqDhcpLease(TypedDict):
interface: str
class PendingChange(TypedDict):
"""One field-level difference between the applied config and the current
saved config (see `lib.common.deep_diff`).
Attributes:
path: Dotted (or indexed) path to the changed field.
action: "added", "removed", or "changed".
old: Value in the last applied config (None when added).
new: Value in the current config (None when removed).
"""
path: str
action: str
old: Any
new: Any
class DnsmasqStatus(TypedDict):
"""Dnsmasq service status snapshot.
@@ -142,12 +159,15 @@ class DnsmasqStatus(TypedDict):
config_file_exists: Whether the rendered .conf is on disk.
active_leases: Count of currently active leases.
pending_changes: Whether the config is dirty vs the applied state.
pending_diff: Field-level changes since the last apply (empty when
up to date or when no applied snapshot is recorded).
"""
service_active: bool
config_file_exists: bool
active_leases: int
pending_changes: bool
pending_diff: list[PendingChange]
class DnsmasqState(TypedDict):
@@ -316,6 +336,8 @@ class WgStatus(TypedDict):
peers: Legacy single-interface runtime peers.
classes: Per-access-class runtime status (keyed by class name).
pending_changes: Whether the config is dirty vs the applied state.
pending_diff: Field-level changes since the last apply (empty when
up to date or when no applied snapshot is recorded).
"""
up: bool
@@ -323,6 +345,7 @@ class WgStatus(TypedDict):
peers: list[WgStatusPeer]
classes: dict[str, WgClassStatus]
pending_changes: bool
pending_diff: list[PendingChange]
class WgPeer(TypedDict, total=False):