- pendingCommand captured in htmx:beforeRequest (not requestConfig).
- ensureLoaded shares a single inflight Promise across concurrent calls.
- Document why synthetic null-id entries are safe in the cache.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- _console_line.html: command + reply, error variant, "(no reply)" placeholder.
- server_detail.html: console section between Live State and Files, replays
last 50 history rows server-side; HTMX form appends new lines via hx-swap.
- console-history.js: ArrowUp/Down recall against /console/history JSON;
scroll-to-bottom on load and after each new line.
- CSS: fixed-height scrolling transcript, terminal-ish styling, spinner via
HTMX in-flight class.
- test_console_routes.py: update 4 assertions from legacy [ERROR] literal
to console-error CSS class (matches new semantic markup).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Tighter, more terminal-flavored. Mono font on the label echoes how
paths are rendered elsewhere in the tree. New-folder dialog title
also shows "/" when targeting the root.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three bugs surfaced in browser testing, plus one UX request:
1. The Uploads panel and the binary-mode editor sub-panels stayed
visible after `el.hidden = true` because their `display: flex/grid`
rules in components.css have the same specificity as the UA's
`[hidden]{display:none}` and come later in cascade. Add a targeted
`[hidden]!important` rule for the affected classes.
2. Clicking a folder toggle inside a `files` overlay did nothing.
`file-tree.js` looked for `.file-tree-children` via
`button.nextElementSibling`, but the files-overlay row template
inserts a per-row action span between the toggle and the children
div. Switch to `closest('.file-tree-row').querySelector(':scope >
.file-tree-children')` so both row variants resolve correctly.
3. Pressing Enter on the new-folder dialog did nothing — the keydown
handler was attached with `{once:true}` inside `openNewFolder`,
so the first letter the user typed consumed the listener and Enter
never fired. Move the listener to module init so it survives
subsequent keystrokes and dialog reopenings.
UX: render the overlay root as a row inside the tree (label
"(overlay root)") rather than as a separate toolbar. The root row
carries the same `+ new file · + new folder · ⬇ zip` hover-action
column as every other folder row, so drop-on-row, hover-reveal, and
data-target-path semantics are uniform across the tree.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
webkitGetAsEntry() only returns an Entry for real OS-originated drag-drops;
synthetic DragEvents (and some browsers without folder-drop support) get
null back. Per-item fallback to getAsFile() keeps single-file drops working
in those cases without sacrificing the whole-folder upload path on real
OS drops.
Caught while end-to-end testing on the deploy box: a programmatically-
dispatched drop fired the listener and reached preventDefault(), but no
upload row appeared because the file collection loop never enqueued.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds Overlay.type='files' whose source-of-truth IS the overlay directory
itself. Users can:
* upload arbitrary files / whole folders by dragging from the OS onto a
folder row in the file tree (one POST per file, queue with
concurrency 3, per-file progress in a floating Uploads panel)
* move via drag-and-drop inside the tree (same gesture, source
distinguishes; refuses cycles)
* create / edit / rename / replace through a single editor modal
(text flavor for editable files, binary flavor with replace-upload
for everything else; filename input is the rename surface)
* mkdir empty folders (slashes allowed for nested intermediates)
* stream a folder as a zip download
* delete files and empty folders
Backend is type-agnostic past the new files_routes endpoints, so the
existing mount / spec / overlayfs / expose_server_cfg pipeline is reused
unchanged. is_editable gates the row's edit affordance and the /save
content rules. Three new safe-resolve helpers (write/delete/move) cover
the new operations with the same anchor-and-resolve pattern as listing
and download. FilesBuilder is a no-op so the build subsystem can
dispatch uniformly.
Spec: docs/superpowers/specs/2026-05-09-files-overlay-design.md
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The previous wiring attached click listeners on DOMContentLoaded, so
any [data-modal-open] / [data-modal-close] / dialog.modal element
that came in via a later HTMX partial swap silently lost its
behaviour. The server-detail Actions partial reloads its reset/delete
triggers on every state change, so reset was unclickable after the
first state change post-load.
Switch to a single delegated click handler on document. Same logic,
but matches via Element.closest() so it works regardless of when an
element was added to the DOM. No re-bind needed after HTMX swaps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Vendors HTMX 2.0.4 (the prior file was a 1-line stub) and uses it to poll
two new partials on a 2s tick while a job is in flight:
- /servers/<id>/actions → state badge, filtered action buttons,
last-job sentence, live job log (SSE) while a Start/Stop/Reset job
is running. When the job is terminal the partial re-renders without
hx-trigger and polling stops.
- /overlays/<id>/build-status → build state badge, last-build
sentence, live job log while a build_overlay job is running. Same
terminal-state stop behavior.
Server detail restructure:
- Editable name moves out of the page body into a Rename modal
triggered from a link next to Delete in the page footer.
- Compact dl with Port (linked as steam://run/550//+connect <host>:<port>)
and Blueprint.
- Actions row: state badge + state-filtered buttons (start/stop, reset)
+ last-job sentence. Drift warning when desired ≠ actual.
- Recent Jobs table removed.
Overlay detail restructure:
- Single panel, dl Type/Scope, no separate Last build row, no Builds
section.
- Script form gets two compound submits: "Save and build" and
"Save, reset and rebuild". Standalone Rebuild/Wipe gone.
- Build status state badge + last-build sentence under the editor;
action buttons hide while a build is in flight.
- Rename modal in the page footer next to Delete.
sse.js binds on htmx:load (covers initial document and post-swap inserts)
and closes EventSources on htmx:beforeCleanupElement to avoid leaking
streams across swaps.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Each linked overlay gets a checkbox on the blueprint detail page that opts
its server.cfg in as exec server_overlay_<id>. The web app builds the
spec with {path, alias} per overlay and prepends exec server_overlay_<id>
lines to the blueprint config in lowest-overlay-first order. The host
stages those copies in the overlayfs upper layer before mounting (avoids
copy-up writes against a sandbox-uid file). A live preview block above the
Config textarea shows what gets auto-executed.
Schema:
- alembic 0007: BlueprintOverlay.expose_server_cfg BOOLEAN
Spec contract:
- l4d2host OverlayRef(path, alias?). load_spec accepts both bare-string
and {path, alias} entries.
Side effects folded in (same file in l4d2_facade):
- start_server auto-initializes; the manual Initialize step is no longer
needed before Start.
- initialize_server no longer runs blueprint builders — builds happen on
overlay save, not on every server Start.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Replace the per-row checkbox + numeric Order table on the blueprint
detail page with a drag-to-reorder list of selected overlays plus a
native <select> for adding more. Removing uses an × button per row;
the option sorted-inserts back into the dropdown alphabetically.
Native HTML5 drag-and-drop, no library, no JS-disabled fallback.
Server contract is unchanged: each list row owns one hidden
<input name="overlay_ids">, DOM order = submission order, and the
existing fallback_position branch in ordered_overlay_ids_from_form
absorbs the now-omitted overlay_position_<id> fields.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The vendored static/vendor/htmx.min.js turned out to be a 33-byte
placeholder, so the hx-get/hx-target/hx-trigger attributes on the
overlay file tree's folder buttons were inert: clicks rotated the
chevron (own JS) but never fetched. Switch the lazy-load to a
~30-line plain-JS handler in static/js/file-tree.js that fetches
button.dataset.filesUrl on first expand and dedupes via dataset.loaded.
Update the spec/plan to match. Route + partial contracts unchanged.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a server-rendered collapsible file tree section to the overlay
detail page so users can verify what their script/workshop overlays
produced and pull individual artifacts (VPKs, configs) without SSH.
HTMX-driven lazy folder expansion with click-to-download via send_file;
symlinks land anywhere under LEFT4ME_ROOT (so workshop addons stream
from the shared cache) but escapes are refused. Same access rule as the
rest of the page (admin or owner). 39 new tests; full web suite green.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- Native <dialog> modal infra (CSS + ~30 LOC JS, no framework) used for
create forms and delete confirmations.
- Index pages become listing-only: + Create button opens a modal; the
broken blueprint Actions column and inline overlay edit cells are gone.
- Server detail gains a blueprint reassignment form; existing Delete
button now opens a confirmation modal before tearing down the runtime.
- Blueprint detail gains a Delete button + confirmation modal (was
unreachable from the UI before).
- New overlay detail page at /overlays/<id> with edit form, "Used by"
blueprints list, and delete (admin only).
- Server create: port field is now optional; backend auto-assigns the
next free port from LEFT4ME_PORT_RANGE_START/_END (default
27015-27115). 409 on range exhaustion.
- New routes: POST /blueprints/<id>/delete (form sentinel matching
overlays pattern), POST /servers/<id> (form-friendly blueprint
reassign), GET /overlays/<id>.
- Server delete operation now redirects to /servers; overlay update
redirects to /overlays/<id>.
Server rename remains unsupported pending an id-vs-name design pass for
l4d2host (the runtime directory is name-keyed; renaming would orphan
files).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>