Add state management, WebSocket polling, html.js templating, and refactor pages

- lib/state.py: per-subsystem collectors with versioned state store
- daemon/server.py: state refresh on request, batch routing updates
- webui/static/hoover/html.js: new html tag template helper via htm.js
- webui/static/hoover/websocket.js: real-time state change notifications
- webui/static/hoover/vdom.js: VDOM improvements for keyed diff
- All frontend pages refactored to use html templates
- Add tests for state management and polling
- Update docs and AGENTS.md
This commit is contained in:
2026-06-23 21:11:45 +00:00
parent 5025dfaf30
commit 5ba0f31767
26 changed files with 1193 additions and 495 deletions
+28 -28
View File
@@ -122,7 +122,7 @@ class TestCheckNginxRunning:
patch("subprocess.run", return_value=mock_result),
patch.object(Path, "is_file", return_value=False),
):
passed, msg = _check_nginx_running()
passed, _ = _check_nginx_running()
assert passed is False
def test_via_pid_file(self):
@@ -131,27 +131,27 @@ class TestCheckNginxRunning:
def run_side_effect(cmd, **kwargs):
raise FileNotFoundError()
pid_file = Path("/var/run/nginx.pid")
with patch("subprocess.run", side_effect=run_side_effect):
with patch.object(Path, "is_file") as mock_is_file:
with patch.object(Path, "read_text", return_value="1234\n"):
with (
patch("subprocess.run", side_effect=run_side_effect),
patch.object(Path, "read_text", return_value="1234\n"),
):
def fake_is_file(self):
if self == Path("/var/run/nginx.pid"):
return True
if str(self) == "/proc/1234/status":
return True
return Path(self).is_file()
def fake_is_file(self):
if self == Path("/var/run/nginx.pid"):
return True
if str(self) == "/proc/1234/status":
return True
return Path(self).is_file()
with patch.object(Path, "is_file", fake_is_file):
passed, msg = _check_nginx_running()
assert passed is True
with patch.object(Path, "is_file", fake_is_file):
passed, _ = _check_nginx_running()
assert passed is True
class TestCheckNginxConfig:
def test_valid_config(self):
with patch("lib.nginx.test_config", return_value=(True, "test passed")):
passed, msg = _check_nginx_config()
passed, _ = _check_nginx_config()
assert passed is True
def test_invalid_config(self):
@@ -175,7 +175,7 @@ class TestCheckFirewallPort80:
with (
patch("lib.common.run_proc", side_effect=proc_side_effect),
):
passed, msg = _check_firewall_port_80()
passed, _ = _check_firewall_port_80()
assert passed is True
def test_blocked_by_firewall(self):
@@ -213,7 +213,7 @@ class TestCheckAcmeHomeWritable:
acme_dir = tmp_path / "acme"
acme_dir.mkdir()
with patch("daemon.handlers.acme._ACME_HOME", acme_dir):
passed, msg = _check_acme_home_writable()
passed, _ = _check_acme_home_writable()
assert passed is True
@@ -233,7 +233,7 @@ class TestCheckAcmeHomeWritable_Permissions:
acme_dir.chmod(0o444)
try:
with patch("daemon.handlers.acme._ACME_HOME", acme_dir):
passed, msg = _check_acme_home_writable()
passed, _ = _check_acme_home_writable()
assert passed is False
finally:
acme_dir.chmod(0o755)
@@ -254,7 +254,7 @@ class TestCheckOpensslAvailable:
def test_not_found(self):
with patch("shutil.which", return_value=None):
passed, msg = _check_openssl_available()
passed, _ = _check_openssl_available()
assert passed is False
@@ -268,7 +268,7 @@ class TestCheckPort80Listening:
mock_sock.__exit__ = MagicMock(return_value=False)
with patch("socket.socket", return_value=mock_sock):
passed, msg = _check_port_80_listening()
passed, _ = _check_port_80_listening()
assert passed is True
def test_not_listening(self):
@@ -280,7 +280,7 @@ class TestCheckPort80Listening:
mock_sock.__exit__ = MagicMock(return_value=False)
with patch("socket.socket", return_value=mock_sock):
passed, msg = _check_port_80_listening()
passed, _ = _check_port_80_listening()
assert passed is False
@@ -301,7 +301,7 @@ class TestCheckAcmeAccount:
),
patch("subprocess.run", return_value=MagicMock(returncode=0, stdout="ok")),
):
passed, msg = _check_acme_account()
passed, _ = _check_acme_account()
assert passed is True
def test_via_account_conf(self, tmp_path):
@@ -320,7 +320,7 @@ class TestCheckAcmeAccount:
patch("daemon.handlers.acme._ACME_HOME", acme_dir),
patch("subprocess.run", side_effect=run_side_effect),
):
passed, msg = _check_acme_account()
passed, _ = _check_acme_account()
assert passed is True
def test_not_configured(self, tmp_path):
@@ -336,7 +336,7 @@ class TestCheckAcmeAccount:
patch("daemon.handlers.acme._ACME_HOME", acme_dir),
patch("subprocess.run", side_effect=run_side_effect),
):
passed, msg = _check_acme_account()
passed, _ = _check_acme_account()
assert passed is False
@@ -351,7 +351,7 @@ class TestCheckDnsPublic:
patch("socket.gethostbyname", return_value="192.168.1.1"),
patch("subprocess.run", return_value=mock_result),
):
passed, msg = _check_dns_public("example.com")
passed, _ = _check_dns_public("example.com")
assert passed is True
def test_does_not_resolve(self):
@@ -362,7 +362,7 @@ class TestCheckDnsPublic:
patch("socket.gethostbyname", return_value="192.168.1.1"),
patch("subprocess.run", return_value=mock_result),
):
passed, msg = _check_dns_public("example.com")
passed, _ = _check_dns_public("example.com")
assert passed is False
@@ -502,7 +502,7 @@ class TestValidate:
result = _validate("example.com")
assert result["ready"] is False
nginx_check = [c for c in result["checks"] if c["name"] == "nginx_running"][0]
nginx_check = next(c for c in result["checks"] if c["name"] == "nginx_running")
assert nginx_check["passed"] is False
assert nginx_check["blocking"] is True
@@ -566,7 +566,7 @@ class TestValidate:
result = _validate("example.com")
assert result["ready"] is True
dns_pub = [c for c in result["checks"] if c["name"] == "dns_public"][0]
dns_pub = next(c for c in result["checks"] if c["name"] == "dns_public")
assert dns_pub["passed"] is False
assert dns_pub["blocking"] is False