Addresses Critical #1 + Important #2/#3/#4 from the Task 9 code review. CRITICAL — Tab/Enter were stolen by CodeJar before the popup handler saw them. CodeJar registers its keydown listener during construction (line ~159), so it ran first in bubble order: Tab handler preventDefaulted and inserted 2 spaces, Enter handler preventDefaulted + stopPropagation'd (with leading indent), so the popup-accept either ran on corrupted state or never fired at all. Fix: register the popup listener with {capture: true} and call stopPropagation on the keys we own — that way capture phase fires before CodeJar's bubble listener and the key is fully consumed by the popup while it's visible. Normal typing (popup hidden) early-returns without stopPropagation, so CodeJar's tab-indent + enter-preserve-indent still work when there's no autocomplete to accept. IMPORTANT — destroy() leaked the popup <ul> into document.body. Each mount/destroy cycle (e.g. modal close/reopen) left an orphan popup. Fix: pop.remove() in destroy(). IMPORTANT — async refreshPopup could race in stale renders if the first keystroke fired the vocab fetch and the second keystroke captured a different ctx before the fetch resolved. Fix: warm the cache with a fire-and-forget loadVocab(language) at mount, so the first user keystroke hits cache. Eliminates the only realistic window for the race. IMPORTANT — acceptCompletion's Range.setStart could throw IndexSizeError on pathological state (caret inside a tokenized span where the fragment isn't fully upstream). Fix: try/catch the entire DOM mutation block, log + dismiss on failure. Plus an inline comment documenting the single-text-node invariant the current grammars hold. Plan source updated for the capture-phase fix (most important for future regeneration); the other fixes are smaller and only mirrored into the actual code. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com> |
||
|---|---|---|
| .. | ||
| alembic | ||
| l4d2web | ||
| tests | ||
| alembic.ini | ||
| pyproject.toml | ||
| README.md | ||
l4d2-web-app
Flask web app for managing L4D2 servers through user-private blueprints.
Key v1 behaviors
- Local username/password login; no public signup
- Admin-managed overlay catalog
- Private blueprints per user
- Server creation from blueprints (live-linked; no per-server blueprint overrides)
- Async job model with persisted command logs in
job_logs - Desired vs actual state model
- Live logs for jobs and servers via SSE endpoints
- Host operations go through
l4d2ctlvia a local host command runner, not directl4d2hostimports
Frontend constraints
- Server-rendered templates (Jinja)
- Vendored HTMX (
static/vendor/htmx.min.js) - Custom CSS only
- Tokenized, consistent link and accent colors
Development
From the workspace root (../):
uv sync # creates .venv, installs l4d2host + l4d2web editable, plus dev deps
uv run pytest l4d2web/tests -q
Configuration
The web app reads these settings from the environment:
DATABASE_URL: SQLAlchemy database URL, for examplesqlite:////var/lib/left4me/left4me.db.SECRET_KEY: Flask secret key used for sessions and CSRF-sensitive state.JOB_WORKER_THREADS: number of background job worker threads.
In the systemd deployment, environment is loaded from /etc/left4me/host.env and /etc/left4me/web.env.
Admin Bootstrap
Create the first admin account with the Flask CLI. Provide the password through LEFT4ME_ADMIN_PASSWORD:
LEFT4ME_ADMIN_PASSWORD='change-me' flask create-user <username> --admin