docs: update documentation and project structure

- Update AGENTS.md, README.md, and docs/* with revisions
- Refactor lib/acme.py and lib/state.py
- Add tests for acme module
- Remove install.sh and restart-services.sh (moved to scripts/)
- Normalize vendor files (acme.sh, htm.js)
This commit is contained in:
2026-07-02 14:41:02 +00:00
parent b4d13c4bd5
commit fb39af126a
15 changed files with 291 additions and 9019 deletions
+140 -42
View File
@@ -376,6 +376,22 @@ Toggle masquerade (source NAT) for a zone.
| `zone` | `string` | Zone name |
| `masquerade` | `boolean` | Whether masquerade is now enabled |
### State
#### Get Firewall State
```
GET /api/firewall/state
```
Return current firewall state from the daemon state store. Provides live firewall state data including active zones, services, and interfaces as polled by the daemon.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `object` | Firewall state data from the state collector |
### Info
#### Available Services
@@ -476,12 +492,7 @@ POST /api/dhcp/apply
Write the in-memory configuration to `/etc/dnsmasq.d/vacuum-wall.conf` and reload the dnsmasq service.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `applied` | `boolean` | Always `true` on success |
| `synced` | `[string, ...]` | Subsystems that were automatically updated by the sync event bus |
**Response:** `data` is `null` on success.
### Status
@@ -777,20 +788,6 @@ Returns HTTP `400` if the domain is already configured.
---
#### Get Domain Details
```
GET /api/proxy/domains/<domain>
```
Return the configuration for a single proxy domain.
**Response (`data`):** Domain name plus backend configuration fields.
Returns HTTP `404` if the domain is not configured.
---
#### Update Domain
```
@@ -902,15 +899,35 @@ Return details for a single certificate.
Returns HTTP `404` if no certificate is found for the domain.
### Validation
#### Validate Certificate Issuance
```
POST /api/certs/validate
```
Run pre-flight checks before certificate issuance. Verifies domain format, ACME account registration, DNS resolution, and port 80 accessibility.
**Request Body:**
| Field | Type | Required | Description |
|-------|------|----------|-------------|
| `domain` | `string` | Yes | Domain to validate |
**Response (`data`):** Validation results object with per-check status.
Returns HTTP `400` if the domain is missing.
### Operations
#### Issue Certificate
#### Start Certificate Issuance
```
POST /api/certs/issue
POST /api/certs/issue/start
```
Request a new certificate for a domain. Returns HTTP `500` if issuance fails.
Create a new certificate issuance request. Issuance runs asynchronously in the background.
**Request Body:**
@@ -920,12 +937,30 @@ Request a new certificate for a domain. Returns HTTP `500` if issuance fails.
| `email` | `string` | No | ACME contact email — **deprecated**, ignored in favor of the registered account email |
| `webroot` | `string` | No | Custom webroot path for HTTP-01 validation |
**Response:** `data` is `null` on success.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `request_id` | `string` | Unique identifier for polling issuance status |
Returns HTTP `400` if the domain is missing. Returns HTTP `409` if a valid certificate already exists for the domain (renew instead). An ACME account must be registered before issuance (verified by the `account_registered` blocking check in the validation pipeline).
---
#### Poll Certificate Issuance Status
```
GET /api/certs/issue/<request_id>
```
Poll the status of a certificate issuance request started by `POST /api/certs/issue/start`.
**Response (`data`):** Issuance status object containing progress, logs, and result.
Returns HTTP `404` if the request ID is not found. The frontend uses `poll()` to repeatedly fetch this endpoint until issuance completes or fails.
---
#### Renew Certificate
```
@@ -1125,12 +1160,7 @@ POST /api/wireguard/apply
Write the current configuration to `wg0.conf` and bring the tunnel up.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `applied` | `boolean` | Always `true` on success |
| `synced` | `[string, ...]` | Subsystems that were automatically updated by the sync event bus |
**Response:** `data` is `null` on success.
---
@@ -1429,12 +1459,54 @@ Suggest firewalld zone assignments for configured interfaces based on heuristics
|-------|------|-------------|
| `data.zones` | `object` | Map of interface name to suggested zone (`"lan"`, `"wan"`, `"management"`) |
Returns HTTP `500` if the value cannot be verified after write.
---
## Status API
Endpoints prefixed with `/api/status/...`. Aggregate status across all subsystems.
### Pending Changes
#### Check All Pending Changes
```
GET /api/status/pending
```
Aggregate pending changes across all subsystems. Useful for the dashboard to show which subsystems need configuration applied.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `subsystems` | `object` | Map of subsystem name to pending status |
| `total_changes` | `number` | Total count of pending changes across all subsystems |
---
#### Apply All Pending Changes
```
POST /api/status/apply-all
```
Apply pending changes for all subsystems in dependency order.
**Response (`data`):**
| Field | Type | Description |
|-------|------|-------------|
| `applied` | `[string, ...]` | List of subsystems that were applied |
| `errors` | `[object, ...]` | Any errors encountered during apply |
### Sysctl
#### Set Kernel Parameter
```
POST /api/sysctl/set
POST /api/network/sysctl/set
```
Set a sysctl kernel parameter value via `sysctl -w`, then verify by reading it back.
@@ -1459,7 +1531,7 @@ Returns HTTP `500` if the value cannot be verified after write.
## Logs API
Endpoints prefixed with `/api/logs/...`. These endpoints **do not** follow the standard JSON `{"ok": true, "data": ...}` response contract — they return HTML `<div>` elements directly. Errors are rendered inline as `(error reading ...)` text rather than returning JSON error responses.
Endpoints prefixed with `/api/logs/...`. Return log lines as JSON strings, wrapped in the standard `{"ok": true, "data": ...}` response contract.
### System Journal
@@ -1469,9 +1541,13 @@ Endpoints prefixed with `/api/logs/...`. These endpoints **do not** follow the s
GET /api/logs/journal
```
Return recent system journal entries as rendered HTML log lines.
Return recent system journal entries.
**Response:** HTML fragment of `<div class="log-line">` elements.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `string` | Raw journal log text |
### Nginx Logs
@@ -1481,9 +1557,15 @@ Return recent system journal entries as rendered HTML log lines.
GET /api/logs/nginx/access
```
Return recent nginx access log entries as rendered HTML.
Return recent nginx access log entries.
**Response:** HTML fragment of `<div class="log-line">` elements.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `string` | Raw access log text |
Returns HTTP `404` if the log file does not exist.
---
@@ -1493,9 +1575,15 @@ Return recent nginx access log entries as rendered HTML.
GET /api/logs/nginx/error
```
Return recent nginx error log entries as rendered HTML.
Return recent nginx error log entries.
**Response:** HTML fragment of `<div class="log-line">` elements.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `string` | Raw error log text |
Returns HTTP `404` if the log file does not exist.
### Dnsmasq Log
@@ -1505,9 +1593,13 @@ Return recent nginx error log entries as rendered HTML.
GET /api/logs/dnsmasq
```
Return recent dnsmasq journal entries as rendered HTML.
Return recent dnsmasq journal entries.
**Response:** HTML fragment of `<div class="log-line">` elements.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `string` | Raw dnsmasq journal text |
### Application Log
@@ -1517,9 +1609,15 @@ Return recent dnsmasq journal entries as rendered HTML.
GET /api/logs/app
```
Return recent application log entries as rendered HTML.
Return recent application log entries.
**Response:** HTML fragment of `<div class="log-line">` elements.
**Response:**
| Field | Type | Description |
|-------|------|-------------|
| `data` | `string` | Raw application log text |
Returns HTTP `404` if the log file does not exist.
---
+3 -3
View File
@@ -54,13 +54,13 @@ Vacuum Wall uses two distinct system users bridged by a shared group:
This design isolates privilege escalation entirely within the daemon, so a compromised Flask process cannot invoke sudo directly. The `lib/` modules no longer contain sudo calls; all privileged command execution lives in `daemon/handlers/*.py`.
**Dev mode variant**: When `install.sh --dev` is used, the repo owner (e.g., `wall`) becomes the WebUI user. The project directory remains owned by the repo owner, preserving git operations and code editing. The daemon user (`vacuum-walld`) has the repo owner's primary group as its own primary group, granting read access to project files. All subdirectories carry the setgid bit (`g+s`) so new files inherit the group regardless of the creator's primary group.
**Dev mode variant**: When `scripts/install.sh --dev` is used, the repo owner (e.g., `wall`) becomes the WebUI user. The project directory remains owned by the repo owner, preserving git operations and code editing. The daemon user (`vacuum-walld`) has the repo owner's primary group as its own primary group, granting read access to project files. All subdirectories carry the setgid bit (`g+s`) so new files inherit the group regardless of the creator's primary group.
The `lib/` modules auto-discover the project root at runtime via `Path(__file__).resolve().parent.parent`. This works because `install.sh` performs an editable pip install (`pip install -e .`), keeping module files in the project directory rather than copying them to `site-packages/`.
The `lib/` modules auto-discover the project root at runtime via `Path(__file__).resolve().parent.parent`. This works because `scripts/install.sh` performs an editable pip install (`pip install -e .`), keeping module files in the project directory rather than copying them to `site-packages/`.
## Install-Time Templating
System configuration files in `system/` are Jinja2 templates rendered by `install.sh` at install time:
System configuration files in `system/` are Jinja2 templates rendered by `scripts/install.sh` at install time:
- **`systemd/vacuum-wall.service`**, **`systemd/vacuum-walld.service`**, **`systemd/vacuum-wall-acme.service`** — `{{ USER_NAME }}`, `{{ USER_DAEMON_NAME }}`, `{{ USER_GROUP }}`, `{{ PROJECT_DIR }}`, `{{ ACME_HOME }}` are substituted to produce the final systemd unit files installed to `/etc/systemd/system/`. The `PROJECT_DIR` template variable is set from the `INSTALL_DIR` environment variable (defaults to the repo root).
- **`sudoers.d/vacuum-walld`** — `{{ USER_DAEMON_NAME }}` is substituted to produce the sudoers whitelist for the daemon user.
+9 -9
View File
@@ -25,13 +25,13 @@ Download the Vacuum Wall repository onto the target machine, then run the instal
# Production: all env vars
MGMT_DOMAIN=wall.example.com \
MGMT_PASS="strongpassword" \
./install.sh --user vacuum-wall
./scripts/install.sh --user vacuum-wall
# Dev mode: CLI flags, auto-detects repo owner
./install.sh --dev --mgmt-pass strongpassword
./scripts/install.sh --dev --mgmt-pass strongpassword
# mDNS (LAN-only, no DNS record needed)
./install.sh --mgmt-domain vacuum-wall.local --mgmt-pass strongpass
./scripts/install.sh --mgmt-domain vacuum-wall.local --mgmt-pass strongpass
```
### Options
@@ -51,7 +51,7 @@ All settings that can be passed as an environment variable also have a CLI flag
| `--lan-ifaces` | `LAN_IFACES` | No | LAN interface names, comma-separated. Auto-detected from non-loopback, non-WAN interfaces. |
| `--force-venv` | — | No | Force recreation of the Python virtual environment. |
Run `./install.sh --help` for full usage.
Run `./scripts/install.sh --help` for full usage.
---
@@ -71,7 +71,7 @@ In dev mode, the ownership model preserves the developer's ability to work with
### Running the Installer in Dev Mode
```bash
./install.sh --dev --mgmt-pass strongpassword
./scripts/install.sh --dev --mgmt-pass strongpassword
```
The script detects the repo owner (e.g., `wall`), creates the `vacuum-walld` daemon user with the repo owner's primary group, and sets up the ownership model described above.
@@ -88,7 +88,7 @@ You can deploy Vacuum Wall in a container or at any custom path. Use `--path` (o
```bash
# Docker volume mount example
./install.sh --path /app/vacuum-wall --user ww-app \
./scripts/install.sh --path /app/vacuum-wall --user ww-app \
--mgmt-domain proxy.internal --mgmt-pass strongpassword
```
@@ -96,7 +96,7 @@ The systemd service unit files and sudoers whitelist are rendered from Jinja2 te
---
## What install.sh Does
## What scripts/install.sh Does
The installer performs the following steps automatically:
@@ -130,7 +130,7 @@ The installer performs the following steps automatically:
### Idempotent Re-Runs
`install.sh` is fully idempotent and safe to run multiple times. Re-running the script:
`scripts/install.sh` is fully idempotent and safe to run multiple times. Re-running the script:
- Skips the Python venv (use `--force-venv` to rebuild)
- Restarts `vacuum-walld`, `vacuum-wall`, and reloads `nginx` to pick up changes
@@ -138,7 +138,7 @@ The installer performs the following steps automatically:
- Preserves existing `config.json` files (skips initial write if file exists)
- Safely updates `htpasswd` (uses update mode instead of create mode)
This makes it safe for development workflows: simply run `bash install.sh` again to update an existing installation.
This makes it safe for development workflows: simply run `bash scripts/install.sh` again to update an existing installation.
---
+8 -5
View File
@@ -43,22 +43,24 @@ WireGuard support provides server-side VPN tunnel management. Peers are added th
## Quick Start
To install Vacuum Wall on a Debian 13 system, run `install.sh` as root with required settings (CLI flags or environment variables):
To install Vacuum Wall on a Debian 13 system, run `scripts/install.sh` as root with required settings (CLI flags or environment variables):
```bash
# Production
./install.sh --mgmt-pass yourpassword
./scripts/install.sh --mgmt-pass yourpassword
# Development (auto-detects your user)
./install.sh --dev --mgmt-pass yourpassword
./scripts/install.sh --dev --mgmt-pass yourpassword
```
After installation, access the management interface at `https://<hostname>.local` using the credentials you configured. The `install.sh` script auto-detects the system hostname, network interfaces, and provisions nginx, authentication, an initial self-signed certificate, and all services. Run `./install.sh --help` for all options.
After installation, access the management interface at `https://<hostname>.local` using the credentials you configured. The `scripts/install.sh` script auto-detects the system hostname, network interfaces, and provisions nginx, authentication, an initial self-signed certificate, and all services. Run `./scripts/install.sh --help` for all options.
## Project Structure
```
├── install.sh # Deployment script (renders Jinja2 templates)
├── scripts/ # Utility scripts
│ ├── install.sh # Deployment script (renders Jinja2 templates)
│ └── update-vendor.sh # Download vendored libraries (acme.sh, htm)
├── pyproject.toml # Project metadata + dependencies
├── .venv/ # Python virtual environment
├── config/ # Declarative JSON configuration (source of truth)
@@ -136,6 +138,7 @@ After installation, access the management interface at `https://<hostname>.local
│ ├── config.md
│ └── hoover.md # Hoover SPA framework
└── scripts/ # Utility scripts
├── install.sh # Deployment script (renders Jinja2 templates)
└── update-vendor.sh # Download vendored libraries (acme.sh, htm)
```