""" server.py - Vacuum Wall management WebUI entry point. Serves the Flask application on 127.0.0.1:9090. Nginx terminates SSL and enforces basic authentication before proxying to this port. """ import logging import os from datetime import datetime from flask import Flask, render_template from lib.acme import get_email, list_certs from lib.dnsmasq import get_config as dnsmasq_config from lib.dnsmasq import get_lease_table from lib.dnsmasq import get_status as dnsmasq_status from lib.firewall import ( config_get, config_pending, get_active_zones, get_interfaces, get_zone_info, ) from lib.nginx import get_config as nginx_config from lib.nginx import get_domains from lib.wireguard import get_config as wg_config from lib.wireguard import status as wg_status from webui.api.certs import bp as certs_bp from webui.api.dhcp import bp as dhcp_bp from webui.api.firewall import bp as firewall_bp from webui.api.proxy import bp as proxy_bp from webui.api.wireguard import bp as wireguard_bp # --------------------------------------------------------------------------- # App factory # --------------------------------------------------------------------------- app = Flask(__name__) app.config["SECRET_KEY"] = os.urandom(32).hex() app.register_blueprint(firewall_bp, url_prefix="/api/firewall") app.register_blueprint(dhcp_bp, url_prefix="/api/dhcp") app.register_blueprint(proxy_bp, url_prefix="/api/proxy") app.register_blueprint(certs_bp, url_prefix="/api/certs") app.register_blueprint(wireguard_bp, url_prefix="/api/wireguard") # --------------------------------------------------------------------------- # Jinja2 custom filters # --------------------------------------------------------------------------- @app.template_filter("timestamp") def timestamp_filter(value): """Convert an ISO timestamp string to a human-readable date.""" if not value: return "" try: dt = datetime.fromisoformat(str(value).replace("Z", "+00:00")) return dt.strftime("%Y-%m-%d %H:%M:%S") except (ValueError, TypeError): return str(value) @app.template_filter("bytes") def bytes_filter(value): """Format a byte count to a human-readable string (KB / MB / GB).""" try: num = float(value) except (ValueError, TypeError): return str(value) if num < 0: return "0 B" for unit in ("B", "KB", "MB", "GB", "TB"): if abs(num) < 1024: return f"{num:.1f} {unit}" num /= 1024 return f"{num:.1f} PB" @app.template_filter("duration") def duration_filter(value): """Format a duration in seconds to a human-readable string.""" try: total = int(float(value)) except (ValueError, TypeError): return str(value) if total < 0: return "0s" parts = [] days, remainder = divmod(total, 86400) hours, remainder = divmod(remainder, 3600) minutes, seconds = divmod(remainder, 60) if days: parts.append(f"{days}d") if hours: parts.append(f"{hours}h") if minutes: parts.append(f"{minutes}m") parts.append(f"{seconds}s") return " ".join(parts) @app.template_filter("json_pretty") def json_pretty_filter(value): """Pretty-print a JSON-serialisable value for debug displays.""" import json try: return json.dumps(value, indent=2, default=str) except (TypeError, ValueError): return str(value) # --------------------------------------------------------------------------- # Page routes # --------------------------------------------------------------------------- logger = logging.getLogger(__name__) def _safely(fn, default=None): """Call *fn* and return *default* on any exception.""" try: return fn() except Exception as exc: logger.warning("WebUI data load failed: %s", exc) return default def _get_service_status(dnsmasq_info, wg_info): """Build a service status dict for the dashboard template.""" services = {} if dnsmasq_info: services["Dnsmasq"] = { "running": dnsmasq_info.get("service_active", False), } if wg_info: services["WireGuard"] = { "running": wg_info.get("up", False), } return services @app.route("/") def dashboard(): active_zones = _safely(get_active_zones, {}) interfaces = _safely(get_interfaces, []) dnsmasq = _safely(dnsmasq_status, {}) domains = _safely(get_domains, []) certs = _safely(list_certs, []) wg = _safely(wg_status, {}) return render_template( "dashboard.html", active_zones=active_zones, interfaces=interfaces, dnsmasq=dnsmasq, domains=domains, certs=certs, wg_status=wg, services=_get_service_status(dnsmasq, wg), firewall_config=_safely(config_get, {}), firewall_pending=_safely(config_pending, {}), ) @app.route("/interfaces") def interfaces_page(): firewall_config = _safely(config_get, {}) firewall_pending = _safely(config_pending, {}) return render_template( "interfaces.html", interfaces=_safely(get_interfaces, []), active_zones=_safely(get_active_zones, {}), firewall_config=firewall_config, firewall_pending=firewall_pending, ) @app.route("/zones") def zones_page(): firewall_config = _safely(config_get, {}) firewall_pending = _safely(config_pending, {}) zones_data = {} for name in _safely(get_active_zones, {}): zones_data[name] = _safely(lambda n=name: get_zone_info(n), {}) return render_template( "zones.html", zones=zones_data, interfaces=_safely(get_interfaces, []), services=_safely( lambda: __import__( "lib.firewall", fromlist=["get_services"] ).get_services(), [], ), firewall_config=firewall_config, firewall_pending=firewall_pending, ) @app.route("/rules") def rules_page(): zones = list(_safely(get_active_zones, {}).keys()) return render_template("rules.html", zones=zones) @app.route("/nat") def nat_page(): zones = {} for name in _safely(get_active_zones, {}): zones[name] = _safely(lambda n=name: get_zone_info(n), {}) return render_template("nat.html", zones=zones) @app.route("/dhcp") def dhcp_page(): return render_template( "dhcp.html", config=_safely(dnsmasq_config, {}), status=_safely(dnsmasq_status, {}), leases=_safely(get_lease_table, []), ) @app.route("/proxy") def proxy_page(): return render_template( "proxy.html", domains=_safely(get_domains, []), config=_safely(nginx_config, {}) ) @app.route("/certs") def certs_page(): return render_template( "certs.html", certs=_safely(list_certs, []), email=_safely(get_email, "") ) @app.route("/wireguard") def wireguard_page(): return render_template( "wireguard.html", config=_safely(wg_config, {}), status=_safely(wg_status, {}) ) @app.route("/logs") def logs_page(): return render_template("logs.html") if __name__ == "__main__": app.run(host="127.0.0.1", port=9090)