refactor: introduce two-user daemon architecture with socket-based communication

- Add daemon/ module with aiohttp server, sync client, and handler registry
- Add daemon/handlers/ for privileged operations (acme, dnsmasq, firewall, logs, nginx, wireguard)
- Add system/acme-deploy.py, vacuum-walld sudoers and systemd service
- Update API routes to use daemon client instead of lib/ directly
- Update lib/, tests/, and webui/ for new architecture
- Update docs and deployment scripts
This commit is contained in:
2026-05-27 23:38:23 +00:00
parent 5ac69dfa7e
commit 200e078bc5
39 changed files with 4671 additions and 1810 deletions
+29 -60
View File
@@ -1,57 +1,17 @@
"""
webui/api/logs.py - Log viewing API blueprint.
"""Log viewing API blueprint.
Serves log content to the /logs page via HTMX endpoints:
/api/logs/journal — systemd journal for vacuum-wall
/api/logs/nginx/access — nginx access log tail
/api/logs/nginx/error — nginx error log tail
/api/logs/dnsmasq — systemd journal for dnsmasq
/api/logs/app — Vacuum Wall application log file
Serves log content to the /logs page via HTMX endpoints through vacuum-walld.
"""
import logging
import subprocess
from pathlib import Path
from flask import Blueprint, render_template_string
from daemon.client import get
logger = logging.getLogger(__name__)
bp = Blueprint("logs", __name__)
PROJECT_DIR = Path(__file__).resolve().parent.parent
APP_LOG_FILE = PROJECT_DIR / "data" / "logs" / "vacuum-wall.log"
_MAX_LINES = 200
def _tail_file(path: str, n: int = _MAX_LINES) -> str:
"""Return the last *n* lines of a file."""
try:
with open(path) as f:
lines = f.readlines()
return "".join(lines[-n:])
except FileNotFoundError:
return "(log file not found)\n"
except PermissionError:
return "(permission denied)\n"
def _sudo_journalctl(unit: str, n: int = _MAX_LINES) -> str:
"""Run ``sudo journalctl -u <unit> --no-pager -n <n>`` and return output."""
try:
result = subprocess.run(
["sudo", "journalctl", "-u", unit, "--no-pager", "-n", str(n)],
capture_output=True,
text=True,
timeout=10,
)
output = result.stdout.strip()
return output if output else f"(no journal entries for {unit})\n"
except (subprocess.TimeoutExpired, FileNotFoundError) as exc:
return f"(error reading journal: {exc})\n"
_LOG_LINE_TEMPLATE = """\
{% for line in lines %}
<div class="log-line{% if 'ERROR' in line %} log-error{% elif 'WARN' in line %} log-warn{% endif %}">{{ line | e }}</div>
@@ -59,41 +19,50 @@ _LOG_LINE_TEMPLATE = """\
def _render_log_lines(text: str) -> str:
"""Render raw log text into HTML fragment with line-by-line coloring."""
lines = text.rstrip("\n").split("\n") if text.strip() else []
return render_template_string(_LOG_LINE_TEMPLATE, lines=lines)
# ---------------------------------------------------------------------------
# Endpoints
# ---------------------------------------------------------------------------
@bp.route("/journal")
def journal():
text = _sudo_journalctl("vacuum-wall")
return _render_log_lines(text)
try:
text = get("/logs/journal")
return _render_log_lines(text)
except RuntimeError:
return _render_log_lines("(error reading journal)\n")
@bp.route("/nginx/access")
def nginx_access():
text = _tail_file("/var/log/nginx/access.log")
return _render_log_lines(text)
try:
text = get("/logs/nginx/access")
return _render_log_lines(text)
except RuntimeError:
return _render_log_lines("(log file not found)\n")
@bp.route("/nginx/error")
def nginx_error():
text = _tail_file("/var/log/nginx/error.log")
return _render_log_lines(text)
try:
text = get("/logs/nginx/error")
return _render_log_lines(text)
except RuntimeError:
return _render_log_lines("(log file not found)\n")
@bp.route("/dnsmasq")
def dnsmasq():
text = _sudo_journalctl("dnsmasq")
return _render_log_lines(text)
try:
text = get("/logs/dnsmasq")
return _render_log_lines(text)
except RuntimeError:
return _render_log_lines("(error reading journal)\n")
@bp.route("/app")
def app_log():
text = _tail_file(str(APP_LOG_FILE))
return _render_log_lines(text)
try:
text = get("/logs/app")
return _render_log_lines(text)
except RuntimeError:
return _render_log_lines("(log file not found)\n")