left4me/scripts/dev-server.py
mwiegand 730ef09967
feat(log-streaming): enable srcds log streaming + temp UDP capture listener
Every managed server now auto-injects log on / mp_logdetail 3 / logaddress_add
into its generated server.cfg, streaming HL Log Standard events to a UDP
listener bundled with l4d2web. The listener is deliberately capture-only —
raw packets land in flat files per source address — so we can observe what
L4D2 actually emits on our servers before committing to a schema or event
vocabulary. Match/round/event model is a Phase 2 plan informed by that data.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-19 23:22:00 +02:00

166 lines
6.4 KiB
Python
Executable file

#!/usr/bin/env python3
"""Local dev server for inspecting the l4d2web UI without a real deploy.
Sets the same env vars the production systemd unit gets from
/etc/left4me/*.env, runs alembic upgrade head, and starts Flask. First
run also seeds an admin user + a tiny demo content set (one blueprint,
one script overlay, one files overlay) so the editor is visible at all
three call sites immediately.
Does NOT mock l4d2ctl, so server-deploy actions (initialize/start/stop)
still fail — they need real systemd + steamcmd. Blueprint + overlay
CRUD work fine.
Usage: scripts/dev-server.py [--port N] (default: 5051)
Reset: rm -rf .tmp/dev-server (next run reseeds)
"""
import json
import os
import pathlib
import sqlite3
import subprocess
import sys
from datetime import UTC, datetime
REPO_ROOT = pathlib.Path(__file__).resolve().parent.parent
DEV_ROOT = REPO_ROOT / ".tmp" / "dev-server"
DB_FILE = DEV_ROOT / "l4d2web.db"
def seed_demo_content():
"""Insert one blueprint, one script overlay, one files overlay, one server."""
overlay_root = DEV_ROOT / "overlays"
conn = sqlite3.connect(DB_FILE)
cur = conn.cursor()
admin_id = cur.execute(
"SELECT id FROM users WHERE admin = 1 LIMIT 1"
).fetchone()[0]
now = datetime.now(UTC).isoformat()
blueprint_id = None # captured below; needed for the server row
config_lines = [
"// L4D2 dev demo — try the srccfg highlighter + autocomplete",
"sv_cheats 0",
"mp_gamemode coop",
"z_difficulty Normal",
"// Type sv_ on a new line to see the cvar popup:",
"",
]
cur.execute(
"INSERT INTO blueprints "
"(user_id, name, arguments, config, created_at, updated_at) "
"VALUES (?, ?, ?, ?, ?, ?)",
(admin_id, "demo-srccfg", json.dumps([]),
json.dumps(config_lines), now, now),
)
blueprint_id = cur.lastrowid
script_text = (
"#!/usr/bin/env bash\n"
"# L4D2 dev demo — try the bash highlighter\n"
"set -euo pipefail\n"
"\n"
"for f in /overlay/*.cfg; do\n"
' echo "processing $f"\n'
"done\n"
)
cur.execute(
"INSERT INTO overlays (name, path, type, user_id, script, "
"last_build_status, created_at, updated_at) "
"VALUES (?, ?, 'script', NULL, ?, '', ?, ?)",
("demo-bash", "_pending", script_text, now, now),
)
script_id = cur.lastrowid
cur.execute("UPDATE overlays SET path = ? WHERE id = ?",
(str(script_id), script_id))
(overlay_root / str(script_id)).mkdir(parents=True, exist_ok=True)
cur.execute(
"INSERT INTO overlays (name, path, type, user_id, script, "
"last_build_status, created_at, updated_at) "
"VALUES (?, ?, 'files', NULL, '', '', ?, ?)",
("demo-files", "_pending", now, now),
)
files_id = cur.lastrowid
cur.execute("UPDATE overlays SET path = ? WHERE id = ?",
(str(files_id), files_id))
files_dir = overlay_root / str(files_id)
files_dir.mkdir(parents=True, exist_ok=True)
(files_dir / "test.cfg").write_text(
"// open this file in the editor modal — flip the language\n"
"// dropdown to bash to see the runtime language switch.\n"
"sv_cheats 0\n"
)
# Server: links the blueprint above to a port. desired_state stays
# "stopped" so the GUI shows it without trying to deploy via l4d2ctl.
cur.execute(
"INSERT INTO servers (user_id, blueprint_id, name, port, "
"desired_state, actual_state, last_error, created_at, updated_at) "
"VALUES (?, ?, 'demo-server', 27015, 'stopped', 'unknown', '', ?, ?)",
(admin_id, blueprint_id, now, now),
)
server_id = cur.lastrowid
conn.commit()
conn.close()
print(f" blueprint id={blueprint_id} 'demo-srccfg' -> /blueprints/{blueprint_id}")
print(f" overlay id={script_id} 'demo-bash' (script) -> /overlays/{script_id}")
print(f" overlay id={files_id} 'demo-files' (files) -> /overlays/{files_id}")
print(f" server id={server_id} 'demo-server' (:27015) -> /servers/{server_id}")
def main():
port = 5051
if "--port" in sys.argv:
port = int(sys.argv[sys.argv.index("--port") + 1])
DEV_ROOT.mkdir(parents=True, exist_ok=True)
(DEV_ROOT / "overlays").mkdir(exist_ok=True)
(DEV_ROOT / "instances").mkdir(exist_ok=True)
# Production-equivalent env (systemd gets these from /etc/left4me/*.env).
os.environ.update({
"SECRET_KEY": "local-dev-only-not-a-secret",
"DATABASE_URL": f"sqlite:///{DB_FILE}",
"LEFT4ME_ROOT": str(DEV_ROOT),
"SESSION_COOKIE_SECURE": "0", # so http://127.0.0.1 cookies stick
"JOB_WORKER_ENABLED": "false", # don't fire (sudo-requiring) l4d2ctl
"LOG_CAPTURE_DIR": str(DEV_ROOT / "captures"),
})
print(f"==> Migrating {DB_FILE} to head", flush=True)
subprocess.run(["uv", "run", "alembic", "upgrade", "head"],
cwd=REPO_ROOT / "l4d2web", check=True, stdout=subprocess.DEVNULL)
user_count = sqlite3.connect(DB_FILE).execute(
"SELECT COUNT(*) FROM users").fetchone()[0]
admin_user = os.environ.get("LEFT4ME_DEV_ADMIN_USERNAME", "dev")
admin_pw = os.environ.get("LEFT4ME_DEV_ADMIN_PASSWORD", "devdevdev")
if user_count == 0:
print(f"==> Seeding admin user '{admin_user}'", flush=True)
subprocess.run(
["uv", "run", "flask", "--app", "l4d2web.app:create_app()",
"create-user", admin_user, "--admin"],
cwd=REPO_ROOT, check=True,
env={**os.environ, "LEFT4ME_ADMIN_PASSWORD": admin_pw},
)
print("==> Seeding demo content", flush=True)
seed_demo_content()
print(f"\n admin: {admin_user} / {admin_pw}", flush=True)
print(f" db: {DB_FILE}", flush=True)
print(f" root: {DEV_ROOT}", flush=True)
print(f"\n Ctrl-C to stop. Reset all state: rm -rf .tmp/dev-server\n", flush=True)
print(f"==> Starting Flask on http://127.0.0.1:{port}", flush=True)
# --debug enables: Python code reload on save, template auto-reload,
# verbose error pages, the Werkzeug interactive debugger. Safe here
# because we bind to 127.0.0.1 only.
os.execvp("uv", ["uv", "run", "flask",
"--app", "l4d2web.app:create_app()",
"--debug", "run", "--port", str(port)])
if __name__ == "__main__":
main()