# State Model Reference Authoritative reference for the shapes returned by the daemon's pre-computed state store (`lib/state.py`), collected per subsystem and pushed over the WebSocket (snapshot on connect, per-subsystem deltas after every change). Python schemas live in `lib/schema.py` (TypedDicts); each collector's return annotation references them. ## Shared notes - Every collector return carries a top-level `timestamp` (ISO-8601). - Subsystems with a declarative config expose pending state as a status dict: `status: {"pending_changes": bool}`, **except firewall**, which uses `pending: {config_pending() result}`. - 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. ## State shape summary `state_store.get()` returns: | Subsystem | Poll | Volatile fields | Top-level keys | |---|---|---|---| | `firewall` | 30s | `interfaces[].ips`, `interfaces[].ipv6` | `config`, `active_zones`, `interfaces`, `available_services`, `zones`, `rich_rules`, `pending`, `timestamp` | | `dnsmasq` | 10s | *(none)* | `config`, `status`, `leases`, `timestamp` | | `nginx` | 60s | *(none)* | `config`, `domains`, `status`, `timestamp` | | `acme` | 300s | *(none)* | `certs`, `email`, `account`, `timestamp` | | `wireguard` | 10s | `status.peers[].transfer_received`/`.transfer_sent`/`.latest_handshake` and the same three under `status.classes[].peers[]` | `config`, `status`, `peers`, `timestamp` | | `networkd` | 10s | `interfaces[].addresses` | `config`, `interfaces`, `status`, `timestamp` | | `system` | 1s | `load`, `memory`, `swap`, `traffic` | `load`, `memory`, `swap`, `traffic`, `timestamp` | Poll intervals are overridable via `VACUUM_WALL_POLL_INTERVALS` (`subsystem:seconds,subsystem:seconds`). ## Firewall Top-level `FirewallState`: ``` { config: {}, // config/firewall/config.json active_zones: {zone: [iface]}, // zones with assigned interfaces interfaces: [ // ip link/addr parsing {name, mac, state, mtu, ips, ipv6, zone} ], available_services: [str], // firewall-cmd --get-services zones: {zone: zoneDict}, // --list-all-zones; hyphenated keys, // may carry "sources", "ports", // "protocols", "forward-ports", "ics", // "icmp-blocks", "module", "rich-rules" rich_rules: {zone: [str]}, // raw firewalld rich-rule strings, // re-derived from zones (NO ids — // deletion-by-id uses config.zones[].rich_rules) pending: { // config_pending() (lib/firewall.py) pending: [...], needs_apply: bool, unmanaged_zones: {zone: {interfaces: [...]}} }, timestamp: str, } ``` Notes: - `interfaces[].ips` / `interfaces[].ipv6` hold `"ip/prefix"` strings (IPv6 list is separate). - The zone dict's rich-rules key is HYPHENATED (`"rich-rules"`); `state.rich_rules` is the snake_case top-level re-derivation. ## Dnsmasq ``` { config: {}, // config/dnsmasq/config.json, deep-merged status: { service_active: bool, config_file_exists: bool, active_leases: int, pending_changes: bool }, leases: [ {expires, mac, ip, hostname, interface} // expires = ISO-8601 or "" ], timestamp: str, } ``` ## Nginx ``` { config: {}, // config/nginx/config.json domains: [ // flattened: one entry per domain+path {domain, path, backend, online, force_ssl, backend_name, cert, [is_management], [is_websocket]} ], status: {pending_changes: bool}, timestamp: str, } ``` ## ACME ``` { certs: [ // list_certs(); extra keys possible {domain, expiry, renewed, status, days_remaining, ...} ], email: str, account: {registered, email, ca, key_length}, timestamp: str, } ``` ## WireGuard ``` { config: {}, // private_key stripped from interface // AND every access class status: { up: bool, // true when ANY managed iface is up interface: {}, peers: [], // legacy single interface (wg0) classes: {class: {up, interface, peers}}, // per wg- pending_changes: bool }, peers: [ // config peers, private keys stripped {name, public_key, endpoint, allowed_ips, persistent_keepalive, preshared_key, ...} ], timestamp: str, } ``` Runtime peers (`status.peers[]`, `status.classes[].peers[]`) carry: `public_key`, `endpoint`, `allowed_ips`, `latest_handshake`, `transfer_received`, `transfer_sent`, `persistent_keepalive`. ## Networkd Matches `parse_networkctl_status()` output (lib/network.py) exactly: ``` { config: {}, // config/network/config.json interfaces: {iface: { // flat runtime entry per interface; addresses: ["ip/prefix"], // a single combined addresses list gateway, dns: [str], mac, // (no ipv6_addresses/routes keys) state, link} }, status: {pending_changes: bool}, timestamp: str, } ``` The parser does not filter `lo`; clients that don't want it filter client-side. ## System Metrics only — no config, no pending state. ``` { load: {load1, load5, load15}, memory: {total, available, used, used_pct}, // bytes; 0-100 swap: {total, used, used_pct}, // bytes; 0-100 traffic: {iface: {rx_bytes, tx_bytes, rx_packets, tx_packets}}, timestamp: str, } ``` All four metric fields are volatile (1s tick cadence); structural diffs only fire on interface-set changes.