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 7beba44b4b
19 changed files with 1957 additions and 986 deletions
+45 -32
View File
@@ -8,7 +8,7 @@ from typing import Any
from jinja2 import Environment, FileSystemLoader
from daemon.server import NotFoundError, registry
from daemon.server import NotFoundError, refresh_state, registry
from lib.common import ensure_dirs, load_json, run, run_proc, save_json
logger = logging.getLogger(__name__)
@@ -48,7 +48,11 @@ DEFAULT_CONFIG: dict[str, Any] = {
"ssl": {**DEFAULT_SSL},
}
_NGINX_TAGS = {"nginx"}
def _get_state() -> dict[str, Any] | None:
from lib.state import state as state_store
return state_store.get("nginx")
def _get_config() -> dict[str, Any]:
@@ -122,9 +126,7 @@ def _write_ssl_snippet() -> None:
def _test_config() -> tuple[bool, str]:
result = run_proc(
["nginx", "-t"], sudo=True, check=False
)
result = run_proc(["nginx", "-t"], sudo=True, check=False)
ok = result.returncode == 0
output = (result.stderr or result.stdout or "").strip()
if not output and ok:
@@ -133,9 +135,7 @@ def _test_config() -> tuple[bool, str]:
def _reload_nginx() -> None:
result = run_proc(
["nginx", "-s", "reload"], sudo=True, check=False
)
result = run_proc(["nginx", "-s", "reload"], sudo=True, check=False)
if result.returncode != 0:
logger.error("nginx reload failed: %s", result.stderr.strip())
else:
@@ -210,20 +210,35 @@ def _write_htpasswd(user: str, password: str) -> None:
os.replace(tmp, HTPASSWD_FILE)
@registry.register("GET", "/nginx/config", cache_tags=_NGINX_TAGS)
def _get_nginx_state() -> dict[str, Any]:
ng = _get_state()
if ng is None:
return {}
return ng
# ---------------------------------------------------------------------------
# Routes
@registry.register("GET", "/nginx/config")
def get_config(_request: Any, _body: Any) -> dict[str, Any]:
ng = _get_nginx_state()
if ng:
return ng.get("config", {})
return _get_config()
@registry.register("POST", "/nginx/config", invalidate=_NGINX_TAGS)
@registry.register("POST", "/nginx/config")
def save_config_handler(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
if not body:
raise ValueError("Request body required")
_save_config(body)
refresh_state(["nginx"])
return {"config_saved": True}
@registry.register("PATCH", "/nginx/config", invalidate=_NGINX_TAGS)
@registry.register("PATCH", "/nginx/config")
def patch_config(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
if not body:
raise ValueError("Request body required")
@@ -232,27 +247,19 @@ def patch_config(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
current = _get_config()
merged = deep_merge(current, body)
_save_config(merged)
refresh_state(["nginx"])
return {"config_saved": True}
@registry.register("GET", "/nginx/domains", cache_tags=_NGINX_TAGS)
@registry.register("GET", "/nginx/domains")
def get_domains(_request: Any, _body: Any) -> list[dict[str, Any]]:
cfg = _get_config()
result: list[dict[str, Any]] = []
for name, dom in cfg.get("domains", {}).items():
site = SITES_DIR / f"{name}.conf"
result.append(
{
"domain": name,
"backend": dom.get("backend", {}),
"online": site.exists(),
"force_ssl": dom.get("force_ssl", True),
}
)
return result
ng = _get_nginx_state()
if ng:
return ng.get("domains", [])
return []
@registry.register("POST", "/nginx/domains/add", invalidate=_NGINX_TAGS)
@registry.register("POST", "/nginx/domains/add")
def add_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
if not body:
raise ValueError("Request body required")
@@ -285,10 +292,11 @@ def add_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
entry["headers"] = extra_headers
cfg["domains"][domain] = entry
_save_config(cfg)
refresh_state(["nginx"])
return {"domain": domain}
@registry.register("DELETE", "/nginx/domains/remove", invalidate=_NGINX_TAGS)
@registry.register("DELETE", "/nginx/domains/remove")
def remove_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
if not body:
raise ValueError("Request body required")
@@ -303,10 +311,11 @@ def remove_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
site = SITES_DIR / f"{domain}.conf"
if site.exists():
site.unlink()
refresh_state(["nginx"])
return {"domain": domain}
@registry.register("POST", "/nginx/domains/update", invalidate=_NGINX_TAGS)
@registry.register("POST", "/nginx/domains/update")
def update_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
if not body:
raise ValueError("Request body required")
@@ -324,10 +333,11 @@ def update_domain(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
else:
entry[key] = val
_save_config(cfg)
refresh_state(["nginx"])
return {"domain": domain}
@registry.register("POST", "/nginx/apply", invalidate=_NGINX_TAGS)
@registry.register("POST", "/nginx/apply")
def apply(_request: Any, _body: Any) -> dict[str, Any]:
_write_ssl_snippet()
_write_all_sites()
@@ -336,22 +346,24 @@ def apply(_request: Any, _body: Any) -> dict[str, Any]:
if not ok:
raise RuntimeError(f"nginx config test failed: {msg}")
_reload_nginx()
refresh_state(["nginx"])
return {"applied": True}
@registry.register("POST", "/nginx/test", invalidate=_NGINX_TAGS)
@registry.register("POST", "/nginx/test")
def test(_request: Any, _body: Any) -> dict[str, Any]:
valid, output = _test_config()
return {"valid": valid, "output": output}
@registry.register("POST", "/nginx/ssl-apply", invalidate=_NGINX_TAGS)
@registry.register("POST", "/nginx/ssl-apply")
def ssl_apply(_request: Any, _body: Any) -> dict[str, Any]:
_write_ssl_snippet()
refresh_state(["nginx"])
return {"applied": True}
@registry.register("POST", "/nginx/management", invalidate=_NGINX_TAGS)
@registry.register("POST", "/nginx/management")
def set_management_proxy(_request: Any, body: dict[str, Any] | None) -> dict[str, Any]:
if not body:
raise ValueError("Request body required")
@@ -373,6 +385,7 @@ def set_management_proxy(_request: Any, body: dict[str, Any] | None) -> dict[str
_save_config(cfg)
if auth_user and auth_pass:
_write_htpasswd(auth_user, auth_pass)
refresh_state(["nginx"])
return {"domain": domain}