left4me/l4d2web
mwiegand cb391ad456
feat(files): migrate uploads + drag-drop to uploads.js; legacy file is a stub
Step 4/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.

End of Phase A. files-overlay.js is now a 19-line tombstone comment;
all behavior lives in the 4 modules under files-overlay/. Step 10
deletes the stub and its <script> tag.

uploads.js owns:
  * The upload queue (concurrency 3, XHR-based progress, queued/active/
    done/cancelled/error/conflict states) and the progress panel
  * Drag-drop on treeRoot (5 events: dragstart, dragend, dragover,
    dragleave, drop). Internal drags (row → folder, via the custom
    application/x-files-overlay MIME) and external drags (OS files +
    folders via webkitGetAsEntry) flow through the same drop handler.
  * walkEntry for recursive folder walks during external drops
  * withCollisionSuffix (moves here from files-overlay.js — its biggest
    caller is the upload-conflict "keep both" path; exposed on
    __filesOverlay for editor.js's save-409 path too)
  * "zip" action handler (registered into __filesOverlay) — pure URL
    navigation; placed here as the closest thematic home

Pattern change: upload-row cancel buttons converted from direct-bound
per row (inside buildUploadRow, which captured `item` in a closure) to
a single document-level delegated click listener. Each row carries
data-upload-id; an uploads Map<uploadId, item> looks up the item at
click time. The Map entry is removed in the uploadsClearBtn handler
when a done/error/cancelled row is cleared, so the Map doesn't grow
unbounded.

Drag-drop stays direct-bound to treeRoot per the plan escape hatch —
the 5 events share coordinated highlight state (is-drag-source,
is-drop-target classes that are toggled across events), and treeRoot
is persistent (never swapped). Delegation would obscure the state
coordination logic without any real benefit.

Phase A end-state:
  * core.js (247 lines): helpers, manager guard, registry dispatch
  * editor.js (550 lines): editor flows (legacy + URL-addressable)
  * dialogs.js (212 lines): new-folder, delete-confirm, conflict
  * uploads.js (423 lines): upload queue + drag-drop + zip + collision
  * files-overlay.js (19 lines): tombstone comment, deleted in Step 10

Total: ~1432 lines across 4 modules + 19-line stub. The plan estimated
~780 lines across 4 modules; actual is ~1.8× larger, the difference
being module-header comments and the delegation-with-state scaffolding
(e.g., editor.js's dual-editor listener split, dialogs.js's per-dialog
state plus close-event resolvers).

Verified live on /overlays/2 in Chromium:
  * 5 script tags load in document order (core → editor → dialogs →
    uploads → legacy stub)
  * Registry has 10 keys; askConflict and withCollisionSuffix are both
    callable; withCollisionSuffix('foo.tar.gz') === 'foo.tar (1).gz'
    (legacy behavior preserved — lastIndexOf('.') splits before the
    final extension)
  * Uploads panel + list elements present in DOM, panel hidden by
    default
  * "zip" action button exists; registered handler would set
    window.location.href to /overlays/2/files/download_zip?path=...
  * No console errors
  * pytest still 573 passed, 1 skipped, 3 deselected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:57:30 +02:00
..
alembic feat(l4d2-web): add command_history table for RCON console transcript 2026-05-14 21:26:56 +02:00
l4d2web feat(files): migrate uploads + drag-drop to uploads.js; legacy file is a stub 2026-05-17 15:57:30 +02:00
scripts fix(editor-v2): fix cm6 to rows-derived height, eliminate layout shift 2026-05-17 10:27:28 +02:00
tests feat(modals): GET /overlays/<id>/files/edit route 2026-05-17 11:43:18 +02:00
alembic.ini chore(l4d2): flatten component layout 2026-05-05 23:47:06 +02:00
pyproject.toml refactor(repo): uv workspace + hatchling + layout restructure 2026-05-15 22:04:29 +02:00
README.md refactor(repo): uv workspace + hatchling + layout restructure 2026-05-15 22:04:29 +02:00

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 l4d2ctl via a local host command runner, not direct l4d2host imports

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 example sqlite:////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