- dashboard.html: Fix zones, leases, wg_status, cert key names, add services var
- server.py: Pass services to dashboard template via _get_service_status()
- lib/acme.py: Fix dead third date format (%Y%m%d%H%M%z) using astimezone(UTC)
- lib/wireguard.py: Add -- separator to cp command to match sudoers rule
- lib/nginx.py: Replace shallow dict.copy() with {**...} for DEFAULT_SSL
- AGENTS.md: Update test count 149 -> 154
- docs/api.md: Rename cert field expiry -> expires_at
3.5 KiB
Vacuum Wall — Agent Instructions
What This Is
SSL proxy / firewall appliance. Python 3 Flask WebUI behind nginx reverse proxy.
Deploys on Debian 13 (trixie). Target system: /home/wall/vacuum-wall.
Architecture
Client ──→ nginx (SSL + basic auth) ──→ Flask (127.0.0.1:9090)
Flask ──→ lib/*.py ──→ sudo <cmd> ──→ system service
webui/server.py— Flask app entry point. Only file that creates theapp.webui/api/*.py— Flask blueprints, one per subsystem. Routes prefix/api/<subsystem>/.lib/*.py— Backend modules. Wrap system commands viasubprocess.run(["sudo", ...]).data/— Declarative JSON configs (source of truth). Generated.confindata/nginx/sites-enabled/.system/— System file templates.systemd/(service units installed to/etc/systemd/system/),sudoers.d/,nginx/.
Project uses .venv. Install deps with pip install -e . (from pyproject.toml). __init__.py files in webui/ and lib/ are intentionally empty — no sys.path boilerplate needed.
Fixed Path
Every module hardcodes /home/wall/vacuum-wall. Changing it requires updating lib/*.py, system/systemd/*.service, install.sh, and system/sudoers.d/vacuum-wall.
Local Dev
.venv/bin/python webui/server.py # binds 127.0.0.1:9090
In production the systemd unit runs as the vacuum-wall system user (NoNewPrivileges, ProtectSystem=strict, loopback-only networking).
Blueprint ↔ lib Mapping (Naming Is Not 1:1)
| Blueprint | URL prefix | Backend module |
|---|---|---|
webui/api/firewall |
/api/firewall/ |
lib.firewall |
webui/api/dhcp |
/api/dhcp/ |
lib.dnsmasq |
webui/api/proxy |
/api/proxy/ |
lib.nginx |
webui/api/certs |
/api/certs/ |
lib.acme |
webui/api/wireguard |
/api/wireguard/ |
lib.wireguard |
Privileged Operations
lib/ modules call sudo for everything that touches system services. Whitelist is system/sudoers.d/vacuum-wall.
Pattern for mutations: write JSON → render native config → sudo <cmd> to apply.
Adding a new privileged command requires a sudoers entry and the lib/ code.
API Response Contract
- Success:
{"ok": true, "data": <value>}— helper_ok(data) - Error:
{"ok": false, "error": "msg"}— helper_error(msg, code=400) - HTTP codes:
400bad request,404not found,500internal failure - Full spec:
docs/api.md
Page Routes vs API
server.py serves HTML pages with Jinja templates. All data is wrapped in _safely(fn, default) so page routes never 500 — they render with fallback values instead.
Deploy
install.sh is the single deploy script. Run as root, requires MGMT_DOMAIN, MGMT_PASS, ACME_EMAIL env vars.
Lint and Tests
Linter / formatter: Ruff (ruff check + ruff format). Config in pyproject.toml under [tool.ruff].
Tests: pytest in tests/. Run with python -m pytest. Tests mock out subprocess calls (firewalld, acme.sh, WireGuard, nginx, dnsmasq) — no system services required.
.venv/bin/ruff check lib/ webui/ tests/ # lint
.venv/bin/ruff format lib/ webui/ tests/ # format
.venv/bin/python -m pytest tests/ -v # test (154 tests)
Install dev tooling with pip install -e ".[dev]".
Docs
docs/ contains the authoritative reference. docs/architecture.md covers request flow, zone model, and data directory layout in detail.