ws: migrate push stream to data streaming
- daemon: send full snapshot on connect; versions/tick now carry the full state of one subsystem (subsystem + data); no legacy updated/subsystems payloads; refresh_state and POST /status/refresh broadcast per-subsystem versions with data - client: modelSet() patches models in place; onMessage/topic refresh retired; 3s initial-load fallback via new POST /api/status/refresh - schema: lib/schema.py TypedDicts + hoover/schema.js defaults + docs/state-model.md as single source of truth for state shapes - system: poll at 1s, volatile metrics registered, dashboard uses a dedicated system model (status model removed) - firewall: refuse to strip both https and ssh from the default zone (409, force override via UI confirm); set_zone_services persists services to the declarative config; collector exposes default_zone - UI: pages migrate to flat state shapes; post-mutation modelFetch refreshes removed (WS delta covers it) - tests: ws snapshot/delta/broadcast, refresh-state, schema types, model-set/js ws handler and reconnect fallback
This commit is contained in:
+34
-8
@@ -12,6 +12,7 @@ from datetime import UTC, datetime
|
||||
from pathlib import Path
|
||||
from typing import Any, ClassVar
|
||||
|
||||
from lib import schema
|
||||
from lib.common import _APPLY_HASH_KEY, config_hash, load_json, run, run_proc
|
||||
from lib.firewall import (
|
||||
_parse_active_zones,
|
||||
@@ -36,7 +37,7 @@ _DEFAULT_POLL_INTERVALS: dict[str, int] = {
|
||||
"wireguard": 10,
|
||||
"dnsmasq": 10,
|
||||
"networkd": 10,
|
||||
"system": 30,
|
||||
"system": 1,
|
||||
# nginx/acme state derives from config files (and lazy in-place migration
|
||||
# can rewrite them without a mutation); poll so drift self-heals.
|
||||
"nginx": 60,
|
||||
@@ -125,6 +126,17 @@ class State:
|
||||
"""
|
||||
return self._data.get(subsystem)
|
||||
|
||||
def get_snapshot(self) -> dict[str, dict[str, Any] | None]:
|
||||
"""Return all subsystem state dicts.
|
||||
|
||||
Used for the initial WS snapshot on connect.
|
||||
|
||||
Returns:
|
||||
Dict mapping every subsystem name to its state data
|
||||
(``None`` when not populated or the last collection failed).
|
||||
"""
|
||||
return {name: self._data.get(name) for name in self.SUBSYSTEMS}
|
||||
|
||||
def set(self, subsystem: str, data: dict[str, Any] | None) -> None:
|
||||
"""Set state data for *subsystem*.
|
||||
|
||||
@@ -424,7 +436,7 @@ def _fp_to_str(fp: dict[str, Any]) -> str:
|
||||
return "/".join(parts)
|
||||
|
||||
|
||||
def _collect_firewall() -> dict[str, Any]:
|
||||
def _collect_firewall() -> schema.FirewallState:
|
||||
"""Return the complete current state of firewalld.
|
||||
|
||||
Returns:
|
||||
@@ -433,6 +445,7 @@ def _collect_firewall() -> dict[str, Any]:
|
||||
"""
|
||||
active_raw = run(["firewall-cmd", "--get-active-zones"], sudo=True)
|
||||
active = _parse_active_zones(active_raw)
|
||||
default_zone = run(["firewall-cmd", "--get-default-zone"], sudo=True).strip()
|
||||
services = run(["firewall-cmd", "--get-services"], sudo=True).split() or []
|
||||
link_out = run(["ip", "-o", "link", "show"], sudo=True)
|
||||
addr_out = run(["ip", "-o", "addr", "show"], sudo=True)
|
||||
@@ -505,6 +518,7 @@ def _collect_firewall() -> dict[str, Any]:
|
||||
# Pending changes
|
||||
full_state = {
|
||||
"active_zones": active,
|
||||
"default_zone": default_zone,
|
||||
"interfaces": ifaces,
|
||||
"available_services": services,
|
||||
"zones": zones,
|
||||
@@ -517,6 +531,7 @@ def _collect_firewall() -> dict[str, Any]:
|
||||
|
||||
return {
|
||||
"active_zones": active,
|
||||
"default_zone": default_zone,
|
||||
"interfaces": ifaces,
|
||||
"available_services": services,
|
||||
"zones": zones,
|
||||
@@ -544,7 +559,7 @@ register_volatile(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _collect_dnsmasq() -> dict[str, Any]:
|
||||
def _collect_dnsmasq() -> schema.DnsmasqState:
|
||||
"""Collect dnsmasq status, config, and leases.
|
||||
|
||||
Returns:
|
||||
@@ -645,7 +660,7 @@ register_collector("dnsmasq", _collect_dnsmasq)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _collect_nginx() -> dict[str, Any]:
|
||||
def _collect_nginx() -> schema.NginxState:
|
||||
"""Collect nginx config and domains list.
|
||||
|
||||
Returns:
|
||||
@@ -846,7 +861,7 @@ def _get_acme_email() -> str:
|
||||
return _read_acme_email()
|
||||
|
||||
|
||||
def _collect_acme() -> dict[str, Any]:
|
||||
def _collect_acme() -> schema.AcmeState:
|
||||
"""Collect ACME certificate list and email.
|
||||
|
||||
Returns:
|
||||
@@ -883,7 +898,7 @@ register_collector("acme", _collect_acme)
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _collect_wireguard() -> dict[str, Any]:
|
||||
def _collect_wireguard() -> schema.WgState:
|
||||
"""Collect WireGuard config, per-class status, and peers.
|
||||
|
||||
Returns:
|
||||
@@ -1129,7 +1144,7 @@ register_volatile(
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
|
||||
def _collect_networkd() -> dict[str, Any]:
|
||||
def _collect_networkd() -> schema.NetworkdState:
|
||||
"""Collect networkd interface state from networkctl.
|
||||
|
||||
Returns:
|
||||
@@ -1212,7 +1227,7 @@ def _parse_meminfo() -> dict[str, Any]:
|
||||
return info
|
||||
|
||||
|
||||
def _collect_system() -> dict[str, Any]:
|
||||
def _collect_system() -> schema.SystemState:
|
||||
"""Collect system-wide metrics: CPU load, memory, network traffic.
|
||||
|
||||
Reads from /proc and /sys — no subprocess needed.
|
||||
@@ -1297,6 +1312,17 @@ def _collect_system() -> dict[str, Any]:
|
||||
|
||||
|
||||
register_collector("system", _collect_system)
|
||||
register_volatile(
|
||||
"system",
|
||||
frozenset(
|
||||
{
|
||||
"load",
|
||||
"memory",
|
||||
"swap",
|
||||
"traffic",
|
||||
}
|
||||
),
|
||||
)
|
||||
|
||||
|
||||
__all__ = [
|
||||
|
||||
Reference in New Issue
Block a user