fix htmx refactor route mismatches and remaining TODO items
- wireguard: POST /peers with JSON encoding (was /add-peer) - rules: delete by rule_id in URL path (was JSON body); pass rule objects with id from server; add hx-disable to initial render - nat: port forward delete uses URL path params to match blueprint - nat: masquerade toggle uses native hx-post/hx-vals (was inline fetch) - app.js renderers updated to use URL path deletes for rules and forwards - remove TODO.md
This commit is contained in:
@@ -0,0 +1,99 @@
|
||||
"""
|
||||
webui/api/logs.py - 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
|
||||
"""
|
||||
|
||||
import logging
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
from flask import Blueprint, render_template_string
|
||||
|
||||
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>
|
||||
{% endfor %}"""
|
||||
|
||||
|
||||
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)
|
||||
|
||||
|
||||
@bp.route("/nginx/access")
|
||||
def nginx_access():
|
||||
text = _tail_file("/var/log/nginx/access.log")
|
||||
return _render_log_lines(text)
|
||||
|
||||
|
||||
@bp.route("/nginx/error")
|
||||
def nginx_error():
|
||||
text = _tail_file("/var/log/nginx/error.log")
|
||||
return _render_log_lines(text)
|
||||
|
||||
|
||||
@bp.route("/dnsmasq")
|
||||
def dnsmasq():
|
||||
text = _sudo_journalctl("dnsmasq")
|
||||
return _render_log_lines(text)
|
||||
|
||||
|
||||
@bp.route("/app")
|
||||
def app_log():
|
||||
text = _tail_file(str(APP_LOG_FILE))
|
||||
return _render_log_lines(text)
|
||||
Reference in New Issue
Block a user