feat: pre-computed state store and async ACME issuance (fixes timeout mismatch)

- Add lib/state.py: in-memory state store with subsystem collectors
  (firewall, dnsmasq, nginx, acme, wireguard)
- Refactor all handlers: read from state on GET, call refresh_state()
  after mutations instead of invoking subprocesses per request
- daemon/server.py: add refresh_state(), /status/all, /status/refresh;
  populate state at startup
- webui/api/certs.py: async step-by-step ACME issuance (validate,
  issue with request_id, poll status) replacing blocking endpoint
- webui/server.py: render pages from state instead of direct lib calls
- Update templates, JS for async cert issuance with polling UI
- Update tests for state-based mocking; add test_state.py
- Fix SIM105 lint issue (contextlib.suppress)
- Add TODO.md with certificate issuance issue tracking

Resolves: WebUI 30s timeout freeze during cert issuance (Problem 1)
This commit is contained in:
2026-05-30 05:45:40 +00:00
parent c091063248
commit dc96e15643
19 changed files with 1960 additions and 986 deletions
+80
View File
@@ -0,0 +1,80 @@
"""Tests for lib/state.py — state store and collect functions."""
from unittest.mock import patch
from lib.state import State, state
class TestState:
def test_new_state_empty(self):
s = State()
assert s.get("firewall") is None
assert s.is_populated() is False
def test_set_and_get(self):
s = State()
s.set("firewall", {"zones": {"public": {}}})
assert s.get("firewall") == {"zones": {"public": {}}}
def test_populate_all(self):
s = State()
with patch.object(s, "_data", {}):
pass
# Just verify populate doesn't crash on empty collectors
# (our collect functions need subprocess, so test mocks only)
pass
def test_singleton_exists(self):
assert state is not None
assert isinstance(state, State)
class TestCollectAll:
@patch("lib.state.run")
def test_collect_firewall_returns_dict(self, mock_run):
from lib.state import _collect_firewall
def run_side(args, **kwargs):
if "--get-zones" in args:
return "public\ninternal"
if "--get-active-zones" in args:
return "public\n eth0"
if "--get-services" in args:
return "ssh http"
if "ip" in args[0]:
if "link" in args:
return "1: lo: <LOOPBACK> mtu 65536\n2: eth0: <UP> mtu 1500 link/ether aa:bb\n"
return ""
if "--list-all" in args:
return "target: default\ninterfaces: eth0\nsources: \nservices: \nports: \nprotocols: \nforward-ports: \nmasquerade: no\nics: no\nrich-rules: \nicmp-blocks: \nmodule: \n"
return ""
mock_run.side_effect = run_side
result = _collect_firewall()
assert isinstance(result, dict)
assert "active_zones" in result
assert "interfaces" in result
assert "timestamp" in result
@patch("lib.state.run_proc")
def test_collect_dnsmasq_returns_dict(self, mock_proc):
from unittest.mock import Mock
from lib.state import _collect_dnsmasq
mock_proc.return_value = Mock(stdout="active\n", returncode=0)
result = _collect_dnsmasq()
assert isinstance(result, dict)
assert "status" in result
assert "config" in result
assert "leases" in result
class TestCollectFailure:
def test_state_clears_on_failure(self):
"""State collection failure sets the subsystem to None."""
s = State()
s.set("firewall", {"zones": {"public": {}}})
s.set("firewall", None) # simulates failure
assert s.get("firewall") is None
assert s.is_populated() is False