docs: add l4d2 implementation plans

Capture the agreed host-library and web-app architecture, contracts, and execution tasks so implementation can proceed with minimal ambiguity.
This commit is contained in:
mwiegand 2026-04-23 00:25:52 +02:00
commit 8ebf033e19
No known key found for this signature in database
2 changed files with 1488 additions and 0 deletions

View file

@ -0,0 +1,739 @@
# L4D2 Host Library v1 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a Python `l4d2` host library and `l4d2ctl` CLI with exactly five commands (`install`, `initialize`, `start`, `stop`, `delete`) plus read APIs needed by the local web app.
**Architecture:** Runtime paths are hard-coded under `/opt/l4d2`. Write operations are imperative and fail-fast with no lock manager, no rollback, and no preflight checks. CLI behavior remains raw/stderr-first, while library internals additionally expose callback-based streaming and read APIs (`get_instance_status`, `stream_instance_logs`) for the web app.
**Tech Stack:** Python 3.12+, Typer, PyYAML, pytest, subprocess, systemd user units, fuse-overlayfs.
---
## Scope and Contracts
- Command surface is fixed in v1:
- `l4d2ctl install`
- `l4d2ctl initialize <name> -f <spec.yaml>`
- `l4d2ctl start <name>`
- `l4d2ctl stop <name>`
- `l4d2ctl delete <name>`
- CLI `<name>` is the server identity source (YAML does not carry identity semantics).
- Hard-coded runtime paths:
- `/opt/l4d2/installation`
- `/opt/l4d2/overlays/<overlay>`
- `/opt/l4d2/instances/<name>`
- `/opt/l4d2/runtime/<name>/{upper,work,merged}`
- Spec fields:
- required: `port`
- optional: `overlays`, `arguments`, `config` (all default to `[]`)
- unknown keys ignored
- Overlay order is preserved from YAML, and the first overlay has highest precedence.
- `initialize` always writes `server.cfg`; if `config` is empty/missing, `server.cfg` is empty.
- `delete` is no-op success when instance/runtime directories are already missing.
- CLI errors: print raw subprocess stderr and exit with subprocess return code.
- Additional read APIs for web app (no extra CLI commands):
- `get_instance_status(name)`
- `stream_instance_logs(name, lines=200, follow=True)`
## Planned File Layout
- `components/l4d2-host-lib/pyproject.toml`
- `components/l4d2-host-lib/src/l4d2host/cli.py`
- `components/l4d2-host-lib/src/l4d2host/spec.py`
- `components/l4d2-host-lib/src/l4d2host/process.py`
- `components/l4d2-host-lib/src/l4d2host/steam_install.py`
- `components/l4d2-host-lib/src/l4d2host/systemd_user.py`
- `components/l4d2-host-lib/src/l4d2host/fs/base.py`
- `components/l4d2-host-lib/src/l4d2host/fs/fuse_overlayfs.py`
- `components/l4d2-host-lib/src/l4d2host/instances.py`
- `components/l4d2-host-lib/src/l4d2host/status.py`
- `components/l4d2-host-lib/src/l4d2host/logs.py`
- `components/l4d2-host-lib/src/l4d2host/templates/l4d2@.service`
- `components/l4d2-host-lib/tests/test_cli.py`
- `components/l4d2-host-lib/tests/test_spec.py`
- `components/l4d2-host-lib/tests/test_process.py`
- `components/l4d2-host-lib/tests/test_install.py`
- `components/l4d2-host-lib/tests/test_initialize.py`
- `components/l4d2-host-lib/tests/test_lifecycle.py`
- `components/l4d2-host-lib/tests/test_status.py`
- `components/l4d2-host-lib/tests/test_logs.py`
- `components/l4d2-host-lib/README.md`
### Task 1: Scaffold package and CLI entrypoint
**Files:**
- Create: `components/l4d2-host-lib/pyproject.toml`
- Create: `components/l4d2-host-lib/src/l4d2host/__init__.py`
- Create: `components/l4d2-host-lib/src/l4d2host/cli.py`
- Test: `components/l4d2-host-lib/tests/test_cli.py`
- [ ] **Step 1: Write failing CLI help test**
```python
from typer.testing import CliRunner
from l4d2host.cli import app
def test_help_lists_v1_commands():
result = CliRunner().invoke(app, ["--help"])
assert result.exit_code == 0
for command in ["install", "initialize", "start", "stop", "delete"]:
assert command in result.output
```
- [ ] **Step 2: Run test and verify failure**
Run: `pytest components/l4d2-host-lib/tests/test_cli.py -q`
Expected: FAIL with missing package/app.
- [ ] **Step 3: Implement pyproject script and minimal CLI app**
```toml
[project.scripts]
l4d2ctl = "l4d2host.cli:app"
```
```python
import typer
app = typer.Typer(no_args_is_help=True)
def _todo() -> None:
raise typer.Exit(code=1)
@app.command()
def install() -> None:
_todo()
@app.command()
def initialize(name: str, spec: str = typer.Option(..., "--spec", "-f")) -> None:
_todo()
@app.command()
def start(name: str) -> None:
_todo()
@app.command()
def stop(name: str) -> None:
_todo()
@app.command()
def delete(name: str) -> None:
_todo()
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-host-lib/tests/test_cli.py -q`
Expected: PASS.
- [ ] **Step 5: Commit scaffold**
```bash
git add components/l4d2-host-lib
git commit -m "feat(l4d2): scaffold package and v1 CLI entrypoint"
```
### Task 2: Implement YAML spec parser
**Files:**
- Create: `components/l4d2-host-lib/src/l4d2host/spec.py`
- Test: `components/l4d2-host-lib/tests/test_spec.py`
- [ ] **Step 1: Write failing parser tests**
```python
import pytest
from pathlib import Path
from l4d2host.spec import load_spec
def test_minimal_spec_parses(tmp_path: Path):
p = tmp_path / "server.yaml"
p.write_text("port: 27015\noverlays: [standard]\n")
spec = load_spec(p)
assert spec.port == 27015
assert spec.overlays == ["standard"]
def test_defaults_are_empty_lists(tmp_path: Path):
p = tmp_path / "server.yaml"
p.write_text("port: 27015\n")
spec = load_spec(p)
assert spec.overlays == []
assert spec.arguments == []
assert spec.config == []
def test_missing_port_fails(tmp_path: Path):
p = tmp_path / "server.yaml"
p.write_text("overlays: [standard]\n")
with pytest.raises((KeyError, ValueError)):
load_spec(p)
def test_unknown_keys_ignored(tmp_path: Path):
p = tmp_path / "server.yaml"
p.write_text("port: 27015\nfoo: bar\n")
spec = load_spec(p)
assert spec.port == 27015
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-host-lib/tests/test_spec.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement parser and dataclass**
```python
from dataclasses import dataclass, field
from pathlib import Path
import yaml
@dataclass
class InstanceSpec:
port: int
overlays: list[str] = field(default_factory=list)
arguments: list[str] = field(default_factory=list)
config: list[str] = field(default_factory=list)
def load_spec(path: Path) -> InstanceSpec:
raw = yaml.safe_load(path.read_text()) or {}
return InstanceSpec(
port=int(raw["port"]),
overlays=[str(x) for x in raw.get("overlays", [])],
arguments=[str(x) for x in raw.get("arguments", [])],
config=[str(x) for x in raw.get("config", [])],
)
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-host-lib/tests/test_spec.py -q`
Expected: PASS.
- [ ] **Step 5: Commit parser**
```bash
git add components/l4d2-host-lib/src/l4d2host/spec.py components/l4d2-host-lib/tests/test_spec.py
git commit -m "feat(l4d2): add spec parser with required port and permissive fields"
```
### Task 3: Build streaming process runner with callbacks
**Files:**
- Create: `components/l4d2-host-lib/src/l4d2host/process.py`
- Test: `components/l4d2-host-lib/tests/test_process.py`
- [ ] **Step 1: Write failing process tests**
```python
import pytest
from l4d2host.process import run_command
def test_callbacks_receive_lines():
out, err = [], []
run_command(["python3", "-c", "import sys; print('ok'); print('warn', file=sys.stderr)"], on_stdout=out.append, on_stderr=err.append)
assert out == ["ok"]
assert err == ["warn"]
def test_nonzero_exit_raises():
with pytest.raises(Exception):
run_command(["python3", "-c", "import sys; sys.exit(7)"])
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-host-lib/tests/test_process.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement process runner**
```python
from dataclasses import dataclass
import subprocess
import threading
import sys
@dataclass
class CommandResult:
returncode: int
stdout: str
stderr: str
def run_command(cmd, *, on_stdout=None, on_stderr=None, passthrough=False):
stdout_lines, stderr_lines = [], []
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
def pump(stream, sink, callback, out_stream):
for raw in iter(stream.readline, ""):
line = raw.rstrip("\n")
sink.append(line)
if callback:
callback(line)
if passthrough:
print(line, file=out_stream)
t_out = threading.Thread(target=pump, args=(proc.stdout, stdout_lines, on_stdout, sys.stdout), daemon=True)
t_err = threading.Thread(target=pump, args=(proc.stderr, stderr_lines, on_stderr, sys.stderr), daemon=True)
t_out.start(); t_err.start()
returncode = proc.wait()
t_out.join(); t_err.join()
result = CommandResult(returncode=returncode, stdout="\n".join(stdout_lines), stderr="\n".join(stderr_lines))
if returncode != 0:
raise subprocess.CalledProcessError(returncode=returncode, cmd=cmd, output=result.stdout, stderr=result.stderr)
return result
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-host-lib/tests/test_process.py -q`
Expected: PASS.
- [ ] **Step 5: Commit process runner**
```bash
git add components/l4d2-host-lib/src/l4d2host/process.py components/l4d2-host-lib/tests/test_process.py
git commit -m "feat(l4d2): add callback-capable streaming process runner"
```
### Task 4: Implement install command with callback passthrough
**Files:**
- Create: `components/l4d2-host-lib/src/l4d2host/steam_install.py`
- Test: `components/l4d2-host-lib/tests/test_install.py`
- [ ] **Step 1: Write failing install tests**
```python
import subprocess
import pytest
from l4d2host.steam_install import SteamInstaller
def test_windows_then_linux(monkeypatch):
calls = []
monkeypatch.setattr("l4d2host.steam_install.run_command", lambda cmd, **kwargs: calls.append(cmd))
SteamInstaller().install_or_update()
assert "windows" in calls[0]
assert "linux" in calls[1]
def test_fail_fast_on_first_failure(monkeypatch):
calls = []
def fake(cmd, **kwargs):
calls.append(cmd)
if len(calls) == 1:
raise subprocess.CalledProcessError(returncode=1, cmd=cmd)
monkeypatch.setattr("l4d2host.steam_install.run_command", fake)
with pytest.raises(subprocess.CalledProcessError):
SteamInstaller().install_or_update()
assert len(calls) == 1
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-host-lib/tests/test_install.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement installer**
```python
from pathlib import Path
from l4d2host.process import run_command
class SteamInstaller:
def __init__(self, install_dir: Path = Path("/opt/l4d2/installation"), steamcmd: str = "steamcmd"):
self.install_dir = install_dir
self.steamcmd = steamcmd
def install_or_update(self, *, on_stdout=None, on_stderr=None, passthrough=False) -> None:
for platform in ("windows", "linux"):
run_command([
self.steamcmd,
"+force_install_dir", str(self.install_dir),
"+login", "anonymous",
"+@sSteamCmdForcePlatformType", platform,
"+app_update", "222860", "validate",
"+quit",
], on_stdout=on_stdout, on_stderr=on_stderr, passthrough=passthrough)
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-host-lib/tests/test_install.py -q`
Expected: PASS.
- [ ] **Step 5: Commit install command**
```bash
git add components/l4d2-host-lib/src/l4d2host/steam_install.py components/l4d2-host-lib/tests/test_install.py
git commit -m "feat(l4d2): implement callback-aware install command"
```
### Task 5: Implement initialize and systemd template management
**Files:**
- Create: `components/l4d2-host-lib/src/l4d2host/systemd_user.py`
- Create: `components/l4d2-host-lib/src/l4d2host/instances.py`
- Create: `components/l4d2-host-lib/src/l4d2host/templates/l4d2@.service`
- Test: `components/l4d2-host-lib/tests/test_initialize.py`
- [ ] **Step 1: Write failing initialize tests**
```python
from pathlib import Path
from l4d2host.instances import initialize_instance
def test_initialize_writes_files(tmp_path: Path):
spec = tmp_path / "spec.yaml"
spec.write_text("port: 27015\noverlays: [a,b]\nconfig: [\"sv_consistency 1\"]\n")
initialize_instance("alpha", spec, root=tmp_path)
assert (tmp_path / "instances/alpha/instance.env").exists()
assert (tmp_path / "instances/alpha/server.cfg").exists()
def test_empty_config_writes_empty_server_cfg(tmp_path: Path):
spec = tmp_path / "spec.yaml"
spec.write_text("port: 27015\n")
initialize_instance("alpha", spec, root=tmp_path)
assert (tmp_path / "instances/alpha/server.cfg").read_text() == ""
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-host-lib/tests/test_initialize.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement initialize flow**
```python
from pathlib import Path
from l4d2host.spec import load_spec
from l4d2host.systemd_user import ensure_template_unit, daemon_reload
def initialize_instance(name: str, spec_path: Path, *, root: Path = Path("/opt/l4d2"), on_stdout=None, on_stderr=None) -> None:
spec = load_spec(spec_path)
instance_dir = root / "instances" / name
runtime_dir = root / "runtime" / name
instance_dir.mkdir(parents=True, exist_ok=True)
(runtime_dir / "upper").mkdir(parents=True, exist_ok=True)
(runtime_dir / "work").mkdir(parents=True, exist_ok=True)
(runtime_dir / "merged").mkdir(parents=True, exist_ok=True)
lowerdirs = [str(root / "overlays" / overlay) for overlay in spec.overlays] + [str(root / "installation")]
(instance_dir / "instance.env").write_text(
f"L4D2_PORT={spec.port}\n"
f"L4D2_ARGS={' '.join(spec.arguments)}\n"
f"L4D2_LOWERDIRS={':'.join(lowerdirs)}\n"
)
(instance_dir / "server.cfg").write_text("\n".join(spec.config) + ("\n" if spec.config else ""))
ensure_template_unit()
daemon_reload(on_stdout=on_stdout, on_stderr=on_stderr)
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-host-lib/tests/test_initialize.py -q`
Expected: PASS.
- [ ] **Step 5: Commit initialize logic**
```bash
git add components/l4d2-host-lib/src/l4d2host/{instances.py,systemd_user.py,templates/l4d2@.service} components/l4d2-host-lib/tests/test_initialize.py
git commit -m "feat(l4d2): implement initialize flow and systemd user template management"
```
### Task 6: Implement start/stop/delete lifecycle commands
**Files:**
- Create: `components/l4d2-host-lib/src/l4d2host/fs/base.py`
- Create: `components/l4d2-host-lib/src/l4d2host/fs/fuse_overlayfs.py`
- Modify: `components/l4d2-host-lib/src/l4d2host/instances.py`
- Test: `components/l4d2-host-lib/tests/test_lifecycle.py`
- [ ] **Step 1: Write failing lifecycle tests**
```python
from pathlib import Path
from l4d2host.instances import start_instance, stop_instance, delete_instance
def test_start_order(monkeypatch):
calls = []
monkeypatch.setattr("l4d2host.instances.run_command", lambda cmd, **kwargs: calls.append(cmd))
start_instance("alpha")
assert calls[0][0] == "fuse-overlayfs"
assert calls[1][:3] == ["systemctl", "--user", "start"]
def test_delete_missing_is_noop(tmp_path: Path):
delete_instance("missing", root=tmp_path)
assert True
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-host-lib/tests/test_lifecycle.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement lifecycle methods with callbacks**
```python
def start_instance(name: str, *, root: Path = Path("/opt/l4d2"), on_stdout=None, on_stderr=None, passthrough=False) -> None:
instance_dir = root / "instances" / name
runtime_dir = root / "runtime" / name
env = {
line.split("=", 1)[0]: line.split("=", 1)[1]
for line in (instance_dir / "instance.env").read_text().splitlines()
if "=" in line
}
run_command([
"fuse-overlayfs",
"-o",
f"lowerdir={env['L4D2_LOWERDIRS']},upperdir={runtime_dir/'upper'},workdir={runtime_dir/'work'}",
str(runtime_dir / "merged"),
], on_stdout=on_stdout, on_stderr=on_stderr, passthrough=passthrough)
shutil.copy2(instance_dir / "server.cfg", runtime_dir / "merged" / "left4dead2/cfg/server.cfg")
run_command([
"systemctl", "--user", "start", f"l4d2@{name}.service"
], on_stdout=on_stdout, on_stderr=on_stderr, passthrough=passthrough)
def stop_instance(name: str, *, root: Path = Path("/opt/l4d2"), on_stdout=None, on_stderr=None, passthrough=False) -> None:
run_command([
"systemctl", "--user", "stop", f"l4d2@{name}.service"
], on_stdout=on_stdout, on_stderr=on_stderr, passthrough=passthrough)
run_command([
"fusermount3", "-u", str(root / "runtime" / name / "merged")
], on_stdout=on_stdout, on_stderr=on_stderr, passthrough=passthrough)
def delete_instance(name: str, *, root: Path = Path("/opt/l4d2"), on_stdout=None, on_stderr=None, passthrough=False) -> None:
instance_dir = root / "instances" / name
runtime_dir = root / "runtime" / name
if not instance_dir.exists() and not runtime_dir.exists():
return
stop_instance(name, root=root, on_stdout=on_stdout, on_stderr=on_stderr, passthrough=passthrough)
if instance_dir.exists():
shutil.rmtree(instance_dir)
if runtime_dir.exists():
shutil.rmtree(runtime_dir)
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-host-lib/tests/test_lifecycle.py -q`
Expected: PASS.
- [ ] **Step 5: Commit lifecycle implementation**
```bash
git add components/l4d2-host-lib/src/l4d2host/{fs/base.py,fs/fuse_overlayfs.py,instances.py} components/l4d2-host-lib/tests/test_lifecycle.py
git commit -m "feat(l4d2): implement start stop delete lifecycle with callback support"
```
### Task 7: Add status and log read APIs
**Files:**
- Create: `components/l4d2-host-lib/src/l4d2host/status.py`
- Create: `components/l4d2-host-lib/src/l4d2host/logs.py`
- Test: `components/l4d2-host-lib/tests/test_status.py`
- Test: `components/l4d2-host-lib/tests/test_logs.py`
- [ ] **Step 1: Write failing read-API tests**
```python
from l4d2host.status import map_active_state
def test_status_mapping():
assert map_active_state("active") == "running"
assert map_active_state("inactive") == "stopped"
assert map_active_state("weird") == "unknown"
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-host-lib/tests/test_status.py components/l4d2-host-lib/tests/test_logs.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement status/log readers**
```python
from dataclasses import dataclass
import subprocess
from l4d2host.process import run_command
@dataclass
class InstanceStatus:
state: str
raw_active_state: str
raw_sub_state: str
def get_instance_status(name: str) -> InstanceStatus:
try:
result = run_command([
"systemctl",
"--user",
"show",
f"l4d2@{name}.service",
"--property=ActiveState",
"--property=SubState",
"--no-pager",
])
except subprocess.CalledProcessError:
return InstanceStatus(state="unknown", raw_active_state="unknown", raw_sub_state="unknown")
pairs = {}
for line in result.stdout.splitlines():
if "=" in line:
k, v = line.split("=", 1)
pairs[k] = v
active = pairs.get("ActiveState", "unknown")
sub = pairs.get("SubState", "unknown")
if active == "active":
mapped = "running"
elif active in {"inactive", "failed"}:
mapped = "stopped"
else:
mapped = "unknown"
return InstanceStatus(state=mapped, raw_active_state=active, raw_sub_state=sub)
def stream_instance_logs(name: str, *, lines: int = 200, follow: bool = True):
cmd = [
"journalctl",
"--user",
"-u",
f"l4d2@{name}.service",
"-n",
str(lines),
"-o",
"cat",
]
if follow:
cmd.append("-f")
proc = subprocess.Popen(cmd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, bufsize=1)
try:
for raw in iter(proc.stdout.readline, ""):
yield raw.rstrip("\n")
finally:
if proc.poll() is None:
proc.terminate()
proc.wait(timeout=2)
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-host-lib/tests/test_status.py components/l4d2-host-lib/tests/test_logs.py -q`
Expected: PASS.
- [ ] **Step 5: Commit read APIs**
```bash
git add components/l4d2-host-lib/src/l4d2host/{status.py,logs.py} components/l4d2-host-lib/tests/test_{status,logs}.py
git commit -m "feat(l4d2): add status and journald log read APIs"
```
### Task 8: Wire CLI to implementations and finalize docs
**Files:**
- Modify: `components/l4d2-host-lib/src/l4d2host/cli.py`
- Create: `components/l4d2-host-lib/README.md`
- Modify: tests under `components/l4d2-host-lib/tests/`
- [ ] **Step 1: Write failing CLI exit-code test**
```python
from typer.testing import CliRunner
from l4d2host.cli import app
def test_cli_propagates_subprocess_return_code(monkeypatch):
def fail(*args, **kwargs):
import subprocess
raise subprocess.CalledProcessError(returncode=9, cmd=["x"])
monkeypatch.setattr("l4d2host.cli.start_instance", fail)
r = CliRunner().invoke(app, ["start", "alpha"])
assert r.exit_code == 9
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-host-lib/tests/test_cli.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement CLI handlers**
```python
import subprocess
import typer
def _exit_from_subprocess_error(exc: subprocess.CalledProcessError) -> None:
raise typer.Exit(code=exc.returncode)
@app.command()
def start(name: str) -> None:
try:
start_instance(name, passthrough=True)
except subprocess.CalledProcessError as exc:
_exit_from_subprocess_error(exc)
```
- [ ] **Step 4: Final test run**
Run: `pytest components/l4d2-host-lib/tests -q`
Expected: PASS.
- [ ] **Step 5: Commit finalization**
```bash
git add components/l4d2-host-lib
git commit -m "docs(l4d2): finalize v1 CLI contracts and web-facing read APIs"
```
---
## Self-Review
- [ ] Spec coverage: command surface fixed, hard-coded paths, config semantics, delete no-op, callback streaming, read APIs.
- [ ] Placeholder scan: no TODO/TBD placeholders.
- [ ] Consistency: argument names (`on_stdout`, `on_stderr`, `passthrough`) are consistent across tasks.
- [ ] Verification: each task contains exact test commands and expected outcomes.

View file

@ -0,0 +1,749 @@
# L4D2 Web App v1 Implementation Plan
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** Build a local Flask web app where users manage L4D2 servers with auth, overlay selection, async jobs, desired-vs-actual status, and live logs.
**Architecture:** The app runs as a single Flask process with Jinja templates and vendored local HTMX. It imports `l4d2host` directly (no subprocess boundary), persists state in SQLite, and executes lifecycle actions asynchronously through an in-process scheduler with lock rules: same server serialized, different servers parallel, install global-exclusive.
**Tech Stack:** Python 3.12+, Flask, SQLAlchemy, Alembic, pytest, vendored HTMX, custom CSS, vanilla JS (SSE).
---
## Scope and Constraints
- In scope:
- public signup/login
- admin role with global permissions
- server CRUD with ordered overlays
- async lifecycle actions (`install`, `initialize`, `start`, `stop`, `delete`)
- live job logs and live server logs
- desired-vs-actual state in server list
- Out of scope (later phases):
- workshop mod management
- user-created overlays and web file manager
- Frontend constraints:
- no CSS/JS framework dependencies
- HTMX vendored locally only (no CDN)
- custom CSS only; links use consistent accent `#0F766E`
- Runtime constraints:
- single-process deployment for v1
- periodic status refresh every 8 seconds
## Planned File Layout
- `components/l4d2-web-app/pyproject.toml`
- `components/l4d2-web-app/src/l4d2web/app.py`
- `components/l4d2-web-app/src/l4d2web/config.py`
- `components/l4d2-web-app/src/l4d2web/db.py`
- `components/l4d2-web-app/src/l4d2web/models.py`
- `components/l4d2-web-app/src/l4d2web/auth.py`
- `components/l4d2-web-app/src/l4d2web/cli.py`
- `components/l4d2-web-app/src/l4d2web/services/l4d2_facade.py`
- `components/l4d2-web-app/src/l4d2web/services/spec_yaml.py`
- `components/l4d2-web-app/src/l4d2web/services/job_worker.py`
- `components/l4d2-web-app/src/l4d2web/services/status.py`
- `components/l4d2-web-app/src/l4d2web/routes/auth_routes.py`
- `components/l4d2-web-app/src/l4d2web/routes/server_routes.py`
- `components/l4d2-web-app/src/l4d2web/routes/overlay_routes.py`
- `components/l4d2-web-app/src/l4d2web/routes/job_routes.py`
- `components/l4d2-web-app/src/l4d2web/routes/log_routes.py`
- `components/l4d2-web-app/src/l4d2web/templates/*.html`
- `components/l4d2-web-app/src/l4d2web/static/vendor/htmx.min.js`
- `components/l4d2-web-app/src/l4d2web/static/css/{tokens,layout,components,logs}.css`
- `components/l4d2-web-app/src/l4d2web/static/js/sse.js`
- `components/l4d2-web-app/alembic.ini`
- `components/l4d2-web-app/alembic/env.py`
- `components/l4d2-web-app/alembic/versions/0001_initial.py`
- `components/l4d2-web-app/tests/*.py`
- `components/l4d2-web-app/README.md`
### Task 1: Scaffold Flask app, config, and health endpoint
**Files:**
- Create: `components/l4d2-web-app/pyproject.toml`
- Create: `components/l4d2-web-app/src/l4d2web/app.py`
- Create: `components/l4d2-web-app/src/l4d2web/config.py`
- Test: `components/l4d2-web-app/tests/test_health.py`
- [ ] **Step 1: Write failing health test**
```python
from l4d2web.app import create_app
def test_health_endpoint():
app = create_app({"TESTING": True})
client = app.test_client()
r = client.get("/health")
assert r.status_code == 200
assert r.get_json() == {"status": "ok"}
```
- [ ] **Step 2: Run test and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_health.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement app factory and route**
```python
from flask import Flask, jsonify
def create_app(test_config=None):
app = Flask(__name__)
app.config.from_mapping(
SECRET_KEY="dev",
DATABASE_URL="sqlite:///l4d2web.db",
STATUS_REFRESH_SECONDS=8,
)
if test_config:
app.config.update(test_config)
@app.get("/health")
def health():
return jsonify({"status": "ok"})
return app
```
- [ ] **Step 4: Run test and verify pass**
Run: `pytest components/l4d2-web-app/tests/test_health.py -q`
Expected: PASS.
- [ ] **Step 5: Commit scaffold**
```bash
git add components/l4d2-web-app
git commit -m "feat(l4d2-web): scaffold flask app and health endpoint"
```
### Task 2: Add database models and migration baseline
**Files:**
- Create: `components/l4d2-web-app/src/l4d2web/db.py`
- Create: `components/l4d2-web-app/src/l4d2web/models.py`
- Create: `components/l4d2-web-app/alembic.ini`
- Create: `components/l4d2-web-app/alembic/env.py`
- Create: `components/l4d2-web-app/alembic/versions/0001_initial.py`
- Test: `components/l4d2-web-app/tests/test_models.py`
- [ ] **Step 1: Write failing model tests**
```python
from l4d2web.db import init_db, session_scope
from l4d2web.models import User
def test_create_user(tmp_path, monkeypatch):
monkeypatch.setenv("DATABASE_URL", f"sqlite:///{tmp_path/'app.db'}")
init_db()
with session_scope() as s:
user = User(username="alice", password_digest="digest", admin=False)
s.add(user)
s.flush()
assert user.id is not None
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_models.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement schema with Rails-style names**
```python
class User(Base):
__tablename__ = "users"
id = mapped_column(Integer, primary_key=True)
username = mapped_column(String(64), unique=True, nullable=False)
password_digest = mapped_column(String(255), nullable=False)
admin = mapped_column(Boolean, default=False, nullable=False)
class Server(Base):
__tablename__ = "servers"
id = mapped_column(Integer, primary_key=True)
user_id = mapped_column(ForeignKey("users.id"), nullable=False)
name = mapped_column(String(128), unique=True, nullable=False)
port = mapped_column(Integer, nullable=False)
desired_state = mapped_column(String(16), default="stopped", nullable=False)
actual_state = mapped_column(String(16), default="unknown", nullable=False)
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-web-app/tests/test_models.py -q`
Expected: PASS.
- [ ] **Step 5: Commit models and migration**
```bash
git add components/l4d2-web-app/src/l4d2web/{db.py,models.py} components/l4d2-web-app/alembic*
git commit -m "feat(l4d2-web): add sqlite schema and migration baseline"
```
### Task 3: Implement auth flows and admin bootstrap CLI
**Files:**
- Create: `components/l4d2-web-app/src/l4d2web/auth.py`
- Create: `components/l4d2-web-app/src/l4d2web/routes/auth_routes.py`
- Create: `components/l4d2-web-app/src/l4d2web/cli.py`
- Modify: `components/l4d2-web-app/src/l4d2web/app.py`
- Test: `components/l4d2-web-app/tests/test_auth.py`
- [ ] **Step 1: Write failing auth tests**
```python
def test_signup_creates_user(client):
r = client.post("/signup", data={"username": "alice", "password": "secret"})
assert r.status_code == 302
def test_login_sets_session(client, seed_user):
r = client.post("/login", data={"username": "alice", "password": "secret"})
assert r.status_code == 302
with client.session_transaction() as sess:
assert sess["user_id"] == seed_user.id
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_auth.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement auth and promote-admin command**
```python
from werkzeug.security import generate_password_hash, check_password_hash
def hash_password(raw: str) -> str:
return generate_password_hash(raw)
def verify_password(raw: str, digest: str) -> bool:
return check_password_hash(digest, raw)
```
```python
@click.command("promote-admin")
@click.argument("username")
def promote_admin(username: str):
with session_scope() as s:
user = s.scalar(select(User).where(User.username == username))
if not user:
raise click.ClickException("user not found")
user.admin = True
s.commit()
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-web-app/tests/test_auth.py -q`
Expected: PASS.
- [ ] **Step 5: Commit auth**
```bash
git add components/l4d2-web-app/src/l4d2web/{auth.py,cli.py,app.py} components/l4d2-web-app/src/l4d2web/routes/auth_routes.py components/l4d2-web-app/tests/test_auth.py
git commit -m "feat(l4d2-web): add signup/login and promote-admin bootstrap command"
```
### Task 4: Implement overlays and server spec CRUD
**Files:**
- Create: `components/l4d2-web-app/src/l4d2web/routes/overlay_routes.py`
- Create: `components/l4d2-web-app/src/l4d2web/routes/server_routes.py`
- Test: `components/l4d2-web-app/tests/test_servers_overlays.py`
- [ ] **Step 1: Write failing CRUD tests**
```python
def test_admin_can_create_overlay(admin_client):
r = admin_client.post("/admin/overlays", data={"name": "standard", "path": "/opt/l4d2/overlays/standard"})
assert r.status_code == 302
def test_server_create_persists_overlay_order(user_client, seed_overlays):
payload = {"name": "alpha", "port": 27015, "overlay_ids": [2, 1], "arguments": [], "config": []}
r = user_client.post("/servers", json=payload)
assert r.status_code == 201
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_servers_overlays.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement routes with ownership rules**
```python
@bp.post("/servers")
@require_login
def create_server():
payload = request.get_json()
with session_scope() as s:
server = Server(
user_id=current_user().id,
name=payload["name"],
port=int(payload["port"]),
arguments=json.dumps(payload.get("arguments", [])),
config=json.dumps(payload.get("config", [])),
)
s.add(server)
s.flush()
for position, overlay_id in enumerate(payload.get("overlay_ids", [])):
s.add(ServerOverlay(server_id=server.id, overlay_id=overlay_id, position=position))
s.commit()
return {"id": server.id}, 201
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-web-app/tests/test_servers_overlays.py -q`
Expected: PASS.
- [ ] **Step 5: Commit CRUD routes**
```bash
git add components/l4d2-web-app/src/l4d2web/routes/{overlay_routes.py,server_routes.py} components/l4d2-web-app/tests/test_servers_overlays.py
git commit -m "feat(l4d2-web): add overlay admin CRUD and server spec CRUD with ordering"
```
### Task 5: Integrate direct `l4d2host` facade and YAML generation
**Files:**
- Create: `components/l4d2-web-app/src/l4d2web/services/spec_yaml.py`
- Create: `components/l4d2-web-app/src/l4d2web/services/l4d2_facade.py`
- Test: `components/l4d2-web-app/tests/test_l4d2_facade.py`
- [ ] **Step 1: Write failing facade tests**
```python
def test_initialize_writes_yaml_and_calls_library(monkeypatch, server_record):
called = {}
def fake_initialize(name, spec_path, **kwargs):
called["name"] = name
called["spec_path"] = str(spec_path)
monkeypatch.setattr("l4d2web.services.l4d2_facade.initialize_instance", fake_initialize)
from l4d2web.services.l4d2_facade import initialize_server
initialize_server(server_record)
assert called["name"] == server_record.name
assert called["spec_path"].endswith(".yaml")
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_l4d2_facade.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement facade and YAML generator**
```python
def write_temp_spec(server, overlays: list[str]) -> Path:
payload = {
"port": server.port,
"overlays": overlays,
"arguments": json.loads(server.arguments),
"config": json.loads(server.config),
}
f = tempfile.NamedTemporaryFile("w", suffix=".yaml", delete=False)
yaml.safe_dump(payload, f, sort_keys=False)
f.close()
return Path(f.name)
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-web-app/tests/test_l4d2_facade.py -q`
Expected: PASS.
- [ ] **Step 5: Commit facade**
```bash
git add components/l4d2-web-app/src/l4d2web/services/{spec_yaml.py,l4d2_facade.py} components/l4d2-web-app/tests/test_l4d2_facade.py
git commit -m "feat(l4d2-web): add direct l4d2host facade with generated yaml specs"
```
### Task 6: Build scheduler, in-process worker pool, and recovery
**Files:**
- Create: `components/l4d2-web-app/src/l4d2web/services/job_worker.py`
- Modify: `components/l4d2-web-app/src/l4d2web/app.py`
- Test: `components/l4d2-web-app/tests/test_job_worker.py`
- [ ] **Step 1: Write failing worker tests**
```python
def test_same_server_jobs_serialized(worker_fixture):
result = worker_fixture.run_once()
assert result["same_server_parallel"] is False
def test_install_is_global_exclusive(worker_fixture):
result = worker_fixture.run_once()
assert result["install_parallel"] is False
def test_startup_recovers_stale_running_jobs(app):
from l4d2web.services.job_worker import recover_stale_jobs
recovered = recover_stale_jobs()
assert recovered >= 0
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_job_worker.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement scheduler and lock rules**
```python
class SchedulerState:
def __init__(self):
self.install_running = False
self.running_servers: set[int] = set()
def can_start(job, state: SchedulerState) -> bool:
if job.operation == "install":
return (not state.install_running) and (not state.running_servers)
if state.install_running:
return False
return job.server_id not in state.running_servers
```
- [ ] **Step 4: Add single-process guard and stale job recovery**
Run: `pytest components/l4d2-web-app/tests/test_job_worker.py -q`
Expected: PASS.
- [ ] **Step 5: Commit worker engine**
```bash
git add components/l4d2-web-app/src/l4d2web/services/job_worker.py components/l4d2-web-app/src/l4d2web/app.py components/l4d2-web-app/tests/test_job_worker.py
git commit -m "feat(l4d2-web): implement async scheduler, lock rules, and startup recovery"
```
### Task 7: Persist command logs and add SSE job stream
**Files:**
- Create: `components/l4d2-web-app/src/l4d2web/routes/job_routes.py`
- Modify: `components/l4d2-web-app/src/l4d2web/services/job_worker.py`
- Test: `components/l4d2-web-app/tests/test_job_logs.py`
- [ ] **Step 1: Write failing log tests**
```python
def test_job_logs_seq_monotonic(db_session, finished_job):
rows = db_session.execute(text("select seq from job_logs where job_id=:id order by seq"), {"id": finished_job.id}).all()
assert rows == sorted(rows)
def test_sse_resume_from_last_seq(client, seeded_job_logs):
r = client.get(f"/jobs/{seeded_job_logs.job_id}/stream?last_seq=3")
assert r.status_code == 200
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_job_logs.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement log persistence + SSE**
```python
def append_job_log(session, job_id: int, stream: str, line: str) -> None:
last_seq = session.scalar(select(func.max(JobLog.seq)).where(JobLog.job_id == job_id)) or 0
clipped = line[:4096]
session.add(JobLog(job_id=job_id, seq=last_seq + 1, stream=stream, line=clipped))
session.flush()
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-web-app/tests/test_job_logs.py -q`
Expected: PASS.
- [ ] **Step 5: Commit job logs**
```bash
git add components/l4d2-web-app/src/l4d2web/{routes/job_routes.py,services/job_worker.py} components/l4d2-web-app/tests/test_job_logs.py
git commit -m "feat(l4d2-web): persist command logs and stream job output over sse"
```
### Task 8: Add SSE server runtime logs
**Files:**
- Create: `components/l4d2-web-app/src/l4d2web/routes/log_routes.py`
- Test: `components/l4d2-web-app/tests/test_server_logs.py`
- [ ] **Step 1: Write failing server-log tests**
```python
def test_owner_can_stream_server_logs(owner_client, owned_server):
r = owner_client.get(f"/servers/{owned_server.id}/logs/stream")
assert r.status_code == 200
def test_non_owner_forbidden(user_client, foreign_server):
r = user_client.get(f"/servers/{foreign_server.id}/logs/stream")
assert r.status_code == 403
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_server_logs.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement runtime log stream route**
```python
@bp.get("/servers/<int:server_id>/logs/stream")
@require_login
def stream_server_logs(server_id: int):
server = load_authorized_server(server_id)
def gen():
for line in facade.stream_server_logs(server.name, lines=200, follow=True):
yield f"data: {line}\n\n"
return Response(gen(), mimetype="text/event-stream")
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-web-app/tests/test_server_logs.py -q`
Expected: PASS.
- [ ] **Step 5: Commit server log streaming**
```bash
git add components/l4d2-web-app/src/l4d2web/routes/log_routes.py components/l4d2-web-app/tests/test_server_logs.py
git commit -m "feat(l4d2-web): add live server log streaming with ownership checks"
```
### Task 9: Implement desired-vs-actual state model and refresh
**Files:**
- Create: `components/l4d2-web-app/src/l4d2web/services/status.py`
- Modify: `components/l4d2-web-app/src/l4d2web/services/job_worker.py`
- Modify: `components/l4d2-web-app/src/l4d2web/routes/server_routes.py`
- Test: `components/l4d2-web-app/tests/test_status.py`
- [ ] **Step 1: Write failing status tests**
```python
def test_display_state_priority(status_service):
assert status_service.display_state(active_job_operation="start", actual_state="stopped") == "starting"
def test_drift_badge(status_service):
assert status_service.is_drift(desired_state="running", actual_state="stopped", has_active_job=False) is True
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_status.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement state logic and refresh hooks**
```python
def compute_display_state(active_operation, actual_state):
priority = {
"delete": "deleting",
"start": "starting",
"stop": "stopping",
"initialize": "initializing",
}
if active_operation in priority:
return priority[active_operation]
return actual_state
```
- [ ] **Step 4: Implement 8-second actual-state refresh loop**
Run: `pytest components/l4d2-web-app/tests/test_status.py -q`
Expected: PASS.
- [ ] **Step 5: Commit state model**
```bash
git add components/l4d2-web-app/src/l4d2web/{services/status.py,services/job_worker.py,routes/server_routes.py} components/l4d2-web-app/tests/test_status.py
git commit -m "feat(l4d2-web): add desired vs actual status model with 8s refresh"
```
### Task 10: Apply security and reliability hardening
**Files:**
- Modify: `components/l4d2-web-app/src/l4d2web/app.py`
- Modify: `components/l4d2-web-app/src/l4d2web/routes/auth_routes.py`
- Modify: `components/l4d2-web-app/src/l4d2web/routes/overlay_routes.py`
- Modify: `components/l4d2-web-app/src/l4d2web/db.py`
- Test: `components/l4d2-web-app/tests/test_security.py`
- [ ] **Step 1: Write failing hardening tests**
```python
def test_csrf_required(client, seed_user):
r = client.post("/servers", data={"name": "x"})
assert r.status_code == 400
def test_overlay_path_must_be_under_opt_l4d2_overlays(admin_client):
r = admin_client.post("/admin/overlays", data={"name": "bad", "path": "/tmp/bad"})
assert r.status_code == 400
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_security.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement hardening**
```python
app.config.update(
SESSION_COOKIE_HTTPONLY=True,
SESSION_COOKIE_SAMESITE="Lax",
)
```
```python
engine = create_engine(db_url, connect_args={"check_same_thread": False})
with engine.connect() as conn:
conn.exec_driver_sql("PRAGMA journal_mode=WAL;")
conn.exec_driver_sql("PRAGMA busy_timeout=5000;")
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-web-app/tests/test_security.py -q`
Expected: PASS.
- [ ] **Step 5: Commit hardening**
```bash
git add components/l4d2-web-app/src/l4d2web/{app.py,db.py} components/l4d2-web-app/src/l4d2web/routes/{auth_routes.py,overlay_routes.py} components/l4d2-web-app/tests/test_security.py
git commit -m "feat(l4d2-web): add csrf, rate limits, sqlite wal, and path safety checks"
```
### Task 11: Build minimal UI with vendored HTMX and custom CSS
**Files:**
- Create: `components/l4d2-web-app/src/l4d2web/templates/base.html`
- Create: `components/l4d2-web-app/src/l4d2web/templates/dashboard.html`
- Create: `components/l4d2-web-app/src/l4d2web/templates/server_detail.html`
- Create: `components/l4d2-web-app/src/l4d2web/templates/admin_overlays.html`
- Create: `components/l4d2-web-app/src/l4d2web/static/vendor/htmx.min.js`
- Create: `components/l4d2-web-app/src/l4d2web/static/css/tokens.css`
- Create: `components/l4d2-web-app/src/l4d2web/static/css/layout.css`
- Create: `components/l4d2-web-app/src/l4d2web/static/css/components.css`
- Create: `components/l4d2-web-app/src/l4d2web/static/css/logs.css`
- Create: `components/l4d2-web-app/src/l4d2web/static/js/sse.js`
- Test: `components/l4d2-web-app/tests/test_pages.py`
- [ ] **Step 1: Write failing page tests**
```python
def test_dashboard_renders_server_name(auth_client_with_server):
r = auth_client_with_server.get("/dashboard")
assert r.status_code == 200
assert "alpha" in r.get_data(as_text=True)
def test_non_admin_cannot_open_overlay_admin(auth_client):
r = auth_client.get("/admin/overlays")
assert r.status_code == 403
```
- [ ] **Step 2: Run tests and verify failure**
Run: `pytest components/l4d2-web-app/tests/test_pages.py -q`
Expected: FAIL.
- [ ] **Step 3: Implement templates and style system**
```css
/* tokens.css */
:root {
--color-link: #0F766E;
--color-text: #132028;
--color-bg: #f7fbfa;
--space-2: 0.5rem;
--space-4: 1rem;
--radius: 10px;
}
a { color: var(--color-link); }
```
- [ ] **Step 4: Run tests and verify pass**
Run: `pytest components/l4d2-web-app/tests/test_pages.py -q`
Expected: PASS.
- [ ] **Step 5: Commit UI**
```bash
git add components/l4d2-web-app/src/l4d2web/{templates,static} components/l4d2-web-app/tests/test_pages.py
git commit -m "feat(l4d2-web): add minimal htmx ui with vendored assets and custom css"
```
### Task 12: Final integration, docs, and full verification
**Files:**
- Create: `components/l4d2-web-app/README.md`
- Modify: `components/l4d2-web-app/src/l4d2web/app.py`
- Test: all tests under `components/l4d2-web-app/tests`
- [ ] **Step 1: Write failing integration smoke test**
```python
def test_core_routes_registered(app):
routes = {r.rule for r in app.url_map.iter_rules()}
assert "/dashboard" in routes
assert "/jobs/<int:job_id>/stream" in routes
```
- [ ] **Step 2: Run test and verify failure (if wiring incomplete)**
Run: `pytest components/l4d2-web-app/tests -q`
Expected: FAIL or partial PASS before final wiring.
- [ ] **Step 3: Finalize app wiring and README**
README sections required:
- deployment model (single process)
- auth/admin bootstrap (`l4d2web promote-admin <username>`)
- scheduler lock semantics
- desired vs actual model and 8s refresh
- log architecture (`job_logs` forever, server logs from journald)
- frontend dependency policy (vendored HTMX only, custom CSS)
- [ ] **Step 4: Run full test suite**
Run: `pytest components/l4d2-web-app/tests -q`
Expected: PASS.
- [ ] **Step 5: Commit finalization**
```bash
git add components/l4d2-web-app
git commit -m "docs(l4d2-web): finalize v1 architecture contracts and verification"
```
---
## Self-Review
- [ ] Spec coverage complete for approved constraints (auth, admin bootstrap, overlays, async jobs, status model, logging, minimal UI).
- [ ] No placeholders (`TODO`, `TBD`, vague directives).
- [ ] Naming consistency (`user_id`, `server_id`, `overlay_id`, `job_id`, `password_digest`, `admin`).
- [ ] Every task includes concrete test commands and expected outcomes.