left4me/l4d2web
mwiegand 307df9c23a
feat(files): migrate dialogs (new-folder, delete, conflict) to dialogs.js
Step 3/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.

dialogs.js owns the three inline <dialog> modals that surround the
file manager:
  * #files-new-folder-modal — "+ folder" / mkdir prompt
  * #files-delete-modal — delete-confirm for files and folders
  * #files-conflict-modal — overwrite / keep-both / cancel choice

Pattern change: the legacy file used clone-and-rebind
(replaceWith(cloneNode(true)) + fresh addEventListener) to drop stale
state-bearing click listeners between dialog opens. dialogs.js replaces
that with a single delegated listener per dialog, reading per-dialog
state from module-scope nullable variables (conflictState,
deleteState, newFolderState). The state is set when the dialog opens
and cleared on the 'close' event so dismissals don't leave stale
references. Listener attaches once per page load.

Dialog opens go through window.modals.openInline()/closeInline()
instead of dialog.showModal()/close() directly, completing the inline-
modal convention from commit c51089d.

askConflict now resolves to "cancel" on any dismissal (Esc, backdrop,
programmatic close) thanks to the 'close' event handler — the legacy
version left the promise pending forever in those paths. Verified
live: closeInline() on an open conflict dialog resolves the pending
askConflict promise to "cancel".

Action-registry dispatch: dialogs.js registers "new-folder" and
"delete" handlers into __filesOverlay. Combined with editor.js's
registration of "new-file" and "edit" (Step 2), only "zip" remains in
the legacy click switch (pure URL navigation, no module dependency).

Cross-module exposure: askConflict moves from files-overlay.js to
dialogs.js; both set __filesOverlay.askConflict, but dialogs.js wins
by document order (it loads before legacy via the <script defer>
ordering in overlay_detail.html). The legacy upload + drag-drop call
sites switch from local askConflict() to window.__filesOverlay
.askConflict() — same shape, different lookup.

The orphaned newFolderDialog / conflictDialog / deleteDialog
declarations at the top of legacy are deleted; legacy no longer holds
references to those elements.

Numbers:
  files-overlay.js: 669 → 589 lines (-80)
  files-overlay/dialogs.js: 212 lines (new)
  Net: +132 lines. Growth is from the delegation/state-management
  scaffolding and module-header comments. The delete went lighter
  than the plan's ~150-line estimate because the new code is more
  carefully structured (less duplication across the 3 dialogs).

Verified live on /overlays/2 in Chromium:
  * 4 script tags load in order (core → editor → dialogs → legacy)
  * Registry has 10 keys; askConflict + withCollisionSuffix still set
  * "+ new folder" on overlay root → new-folder dialog opens with
    empty name input and "/" target label, closes cleanly
  * "✕" on a file row → delete-confirm dialog opens with the file's
    name displayed, closes cleanly
  * askConflict('a/b.txt') → conflict dialog opens with path shown
    - close via window.modals.closeInline() → resolves "cancel"
    - click [data-files-conflict-action="overwrite"] → resolves "overwrite"
    - click [data-files-conflict-action="keep-both"] → resolves "keep-both"
  * No console errors throughout
  * pytest still 573 passed, 1 skipped, 3 deselected

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 15:51:47 +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 dialogs (new-folder, delete, conflict) to dialogs.js 2026-05-17 15:51:47 +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