Commit graph

270 commits

Author SHA1 Message Date
mwiegand
eabb9764b9
style(server-detail): grow inspection-strip tabs; pin console input above scrollable transcript
Tab padding: --space-xs → --space-s (vertical, 2×) and --space-m → calc(--space-m * 1.5) (horizontal, +50%), plus font-size: 1.05em. strip-expand bumped from --space-xs/--space-s to --space-s/--space-m proportionally. Console panes (inline tab-pane and modal body) are now flex columns so the transcript scrolls and the input form stays pinned at the bottom; max-height: none overrides the global 400px cap in both scopes.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:43:54 +02:00
mwiegand
70b80d4ceb
fix(server-detail): tall modal heights, true recent count, re-fetch on reopen, drop dead macro + arg
- Fix 1: add .modal .log-stream.tall / .console-transcript.tall → max-height 60vh so
  log and console modals render taller than the compact inline tab
- Fix 2: replace len(recent_rows) with a select(func.count(func.distinct(...))) so
  recent_players_total_count reflects all matching players, not the .limit(50) cap;
  add test_live_state_total_count_reflects_truth_above_limit (60 sessions → "60 Recent")
- Fix 3: dispatch custom modal:opened event after showModal() in both openInline and
  fetchAndShowRouted; switch recent-players-modal hx-trigger from "revealed" to
  "modal:opened from:closest dialog" so HTMX re-fetches on every open, not just first.
  Manual smoke-test not performed — relies on JS event dispatch + test suite; no JS
  test framework in repo.
- Fix 4: remove dead config_field macro (value-form, never called; config_field_block
  is the one actually used)
- Fix 5: drop unused editable parameter from config_field_block macro definition and
  the editable=True call on the Hostname field

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:40:20 +02:00
mwiegand
2d28d9f800
test(e2e): tab switching + expand-to-modal on server detail
Append two new e2e tests that cover the inspection strip:
- test_tabs_switch_between_log_console_files: verifies data-active-tab
  attribute mirrors tab clicks and the correct tabpanel is shown/hidden
- test_expand_opens_matching_modal: verifies the ⛶ button opens the
  <dialog> matching the active tab name

Also fix the pre-existing test_hover_download_initiates_file_download
for the new tab-based layout: click the Files tab first (rows were
inside a hidden tabpanel), and scope the row locator to the tab pane
to avoid a strict-mode violation now that the modal also contains a
duplicate file tree.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:32:19 +02:00
mwiegand
6de5f90626
feat(live-state): ?view=recent-modal branch + single-column modal list
Adds the _recent_players_modal_body.html partial for the full recent-players
list (no 10-item cap), the route branch in live_state_fragment that renders it
when ?view=recent-modal is requested, and the .recent-modal-list CSS rule that
forces single-column layout inside the modal.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:25:36 +02:00
mwiegand
96bbd0c136
fix(server-detail): restore auto-escape via macro-call blocks + extract console_form macro
Replace four raw-string | safe config_field calls with {% call config_field_block %}
blocks so Jinja auto-escaping is preserved for server.hostname, server.name,
blueprint.name, server.rcon_password and g.user.username. Extract a console_form
macro to eliminate the duplicated inline/modal form and restore the missing
placeholder on the modal input. Add XSS regression test that confirms the fix
is load-bearing (test fails when templates are reverted to pre-fix state).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:23:40 +02:00
mwiegand
11142c1d08
feat(server-detail): state cluster + inspection strip + five modals
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:18:38 +02:00
mwiegand
808a59b2db
feat(base): include tabs.js
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:15:34 +02:00
mwiegand
eb0c1a52db
feat(js): tabs.js — tab activation + expand-to-active-modal
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:14:34 +02:00
mwiegand
be3a00a8f5
feat(css): state-cluster, inspection-strip, compact player grids
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:12:31 +02:00
mwiegand
e2b6f39828
feat(server-actions): remove inline job-log; link → job-log-modal trigger
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:10:02 +02:00
mwiegand
6656588b8f
refactor(live-state): hoist display_name into {% set %} to DRY card loops
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:08:46 +02:00
mwiegand
20fb564246
feat(live-state): compact 4-col current + 5-col recent chips + N Recent trigger
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:07:15 +02:00
mwiegand
9554661e5a
fix(live-state): cap recent_rows query at 50 to bound row count
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:05:13 +02:00
mwiegand
309354942a
feat(live-state): expose sliced recents + total count to template
Drop .limit(20) from the recent_rows query so the full history window is
available for the future recent-players modal; derive recent_players_overview
(first 10) and recent_players_total_count from the unbounded result and pass
both into _live_state.html alongside the existing recent_players key.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 21:02:57 +02:00
mwiegand
7963b69cb3
feat(templates): add _macros.html with config_field macro 2026-05-17 21:00:01 +02:00
mwiegand
b45adcd819
feat(console): add color legend under console input
Three labelled swatches mirror the dropdown's name colors so users
can decode the cvar/command/sourcemod color scheme without guesswork.
Plain-text caveat next to the sourcemod swatch notes that those
commands are plugin-dependent.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:56:54 +02:00
mwiegand
44e82e3c42
feat(console): color-code sm_* (SourceMod) suggestions distinctly
sm_* commands depend on the SourceMod plugin being loaded on the
target server, which is not always the case. Render their names in
the third syntax-palette color (purple via --cm-number) so the user
can tell at a glance that these may not exist on the server they
are targeting. Vanilla cvars and commands keep their existing
pink/green colors. Theme-aware via the existing token swap.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:44:27 +02:00
mwiegand
d21cd72f8d
test(files): cover server-detail hover-download
New test module (test_server_detail.py) — the server-detail page is
NOT a files-overlay (files_overlay=False in the template), it just
reuses _overlay_file_tree.html in read-only mode. Tests live
separately to make the semantic split visible.

The test navigates to /servers/<id>, hovers the server.cfg row to
defeat the CSS :hover gate on .files-row-actions
(opacity:0/pointer-events:none → 1/auto), clicks the ⬇ download
link, and asserts both the suggested filename and the byte content
of the downloaded file.

The :hover gate is load-bearing: without locator.hover() first,
pointer-events:none blocks the click. A regression that ships
actions always-visible would change the user-flow ergonomics and
needs to update this test deliberately.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
(Tier 3).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:22:17 +02:00
mwiegand
b43bb9e0fa
test(files): add server-detail e2e fixture
Adds server_with_files fixture: seeds alice + a Blueprint + a Server
row pinned to port 27015, then pre-creates
LEFT4ME_ROOT/runtime/<server_id>/merged/ with a single seed file.
The /servers/<id> page lists files from that merged directory (the
kernel-overlayfs view of a running server), which never exists on
dev/test boxes — without the pre-mkdir + seed, the page renders
the empty-state branch instead of file rows.

Server.port is a global UNIQUE constraint on the model, but every
e2e test gets its own SQLite DB, so a fixed L4D2-default port is
fine.

Sets the stage for the Tier 3 hover-download test.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:20:20 +02:00
mwiegand
e89dd25cdd
test(files): cover internal drag row to folder move
Drags server.cfg from the overlay root onto the cfg/ folder row,
asserts both that the file actually moved on disk AND that the tree
reflects the move after the debounced HTMX refresh of both parents.

Exercises the internal-drag path in uploads.js:332-366: the
custom-MIME setData/getData contract, the POST /files/move call,
and the dual scheduleRefresh(parentOf(src)) + scheduleRefresh(dst)
on success. Playwright's locator.drag_to() synthesizes setData
correctly — it relies on dataTransfer.getData(), NOT
webkitGetAsEntry which Playwright cannot fake.

Pitfalls handled inline: scoped the source/target locators to
li.file-tree-row-{file,dir} because action buttons inside the row
duplicate data-target-path + data-row-kind and would trip strict
mode. Used to_have_count instead of to_be_visible because cfg's
children div is collapsed (hidden) by default — the moved row is
in the DOM but not visually rendered until cfg is expanded.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
(Tier 2 case C).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:18:57 +02:00
mwiegand
a6be29c6d2
test(files): cover Escape closes editor with no stale state
Opens server.cfg, drives the CM6 controller to a dirty buffer,
presses Escape to close without saving, then reopens the same file
and asserts the editor shows the original disk content — not the
discarded buffer.

Pins two invariants: native <dialog>'s cancel→close path stays
intact (no JS shortcut around Escape), and the reopened editor
fetches a FRESH fragment via htmx.ajax with CM6 re-mounting on
the new textarea. A regression that cached buffer state to "feel
snappy" would fail this test loudly.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
(Tier 2 case B).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:17:21 +02:00
mwiegand
b222fdc918
test(files): cover share-URL deep link reopens editor
Navigates directly to /overlays/<id>?modal=<urlencoded edit path>
and asserts the routed editor auto-opens on the right file.

This is the central guarantee of the URL-addressable modals spec —
copy the URL, share it, and the recipient lands on the same modal
state. A regression in modals.js's DOMContentLoaded bootstrap (the
URLSearchParams.get("modal") → fetchAndShowRouted chain) would
silently dead-link every shared URL; this test fails loudly instead.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md
(Tier 2, highest-value case).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:16:51 +02:00
mwiegand
2fcf9c3778
docs(console): note single-form assumption near activeBinding
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 19:10:01 +02:00
mwiegand
97a4e51f8a
refactor(console): module-scope listeners + form-level event delegation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 19:05:27 +02:00
mwiegand
25016b0ff6
refactor(css): consolidate monospace stack into --font-mono token
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 19:00:11 +02:00
mwiegand
81ba4ac83a
test(files): cover filename-rename on save
Opens server.cfg, changes the filename input to server-renamed.cfg,
clicks Save, and asserts: the routed modal closes, the old name is
gone from disk, the new name carries the original content, and the
tree row swaps over. Pins the rename-on-save branch in
routedSaveClicked (the non-is_new path that diffs originalLeaf vs
editedFilename and emits `new_path`).

Deliberately omits __filesEditor.setContent — rename should preserve
the textarea-seeded content. A regression that wrote an empty body
fails on the content-equality assertion.

Tier 1 complete: 7 tests + fixture extension. Full suite runs in
<10s on warm Chromium.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:46:40 +02:00
mwiegand
3ea57b2bdb
test(files): cover new-folder + delete cycle
Creates a folder via the inline new-folder dialog's Enter-keydown
submit path, then deletes it via the inline delete-confirm dialog.
Each step asserts disk + tree state. The Enter path is the
direct-bound listener (not delegated), so it's the most likely to
break under future refactors — pinning it here surfaces such
regressions immediately.

Row-action buttons (`✕`) live inside `<li draggable="true">` — the
draggable ancestor confuses Playwright's hit-test even though real
browsers click through fine. The click uses force=True to skip the
hit-test (documented inline).

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:44:58 +02:00
mwiegand
6b0fbb75bf
test(files): cover binary replace via browse
Opens icon.png, attaches new bytes via Playwright's file chooser
(intercepting the click → hidden-input.click() → OS picker chain),
clicks Replace, and asserts the new bytes land on disk under the
unchanged filename. Covers the multipart /files/replace endpoint and
the click → setRoutedReplacement → save-enabled UI wiring.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:42:13 +02:00
mwiegand
c1ea5eb11e
test(files): cover binary editor UI
Opens icon.png and asserts the routed editor renders the binary
branch of overlay_file_editor.html: replace-zone present, save
button labelled "Replace" and disabled on open, download link
pointing back at /files/download.

The same /files/edit route serves both text and binary modes — the
server picks the template branch from is_editable() + a magic-byte
check. Without this test, a regression that flipped a binary file
into the text branch would mean rendering raw PNG bytes inside an
editable textarea (and a misleadingly working save button).

Also asserts no textarea[data-rel-path] is in the DOM, so a future
regression that left both branches enabled fails loudly.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:41:37 +02:00
mwiegand
aad8356613
test(files): cover 409 askConflict keep-both path
Tries to create `cfg`, which collides with the seeded directory of the
same name. /files/save returns 409 "destination is not a file";
routedSaveClicked routes that through fo.askConflict, which opens the
inline #files-conflict-modal on top of the still-open routed editor.
Clicking keep-both triggers a second POST with the suffixed path
(`cfg (1)`), the routed modal closes, and the new row materialises in
the tree.

This is the F4 path from 8dc14f0 ("wire askConflict into the routed
new-file 409 path"). Before that commit, the routed code branch fell
through to a generic alert(). With this test in place, a missing
call site fails loudly instead of silently.

Pins three invariants:
  * The conflict dialog is INLINE, not routed — it appears without a
    URL change (the decision tree in AGENTS.md "Modals: inline vs
    routed" hinges on this).
  * .files-conflict-path echoes the original colliding path, not the
    computed suffix — the suffix is internal, the user sees the
    collision.
  * withCollisionSuffix("cfg") → "cfg (1)" (no dot after the last
    slash → trailing-suffix branch in uploads.js).

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:40:56 +02:00
mwiegand
d92f71f691
test(files): cover routed new-file flow
Clicks `+ new file` at the overlay root, fills the routed editor's
filename + CM6 content, and clicks Create. Asserts the modal closes,
the file lands on disk, AND the new row appears in the live tree
after the debounced HTMX refresh — the last assertion catches the
class of bug where /files/save persists but
scheduleRefresh(parentOf(fullPath)) never lands a fresh listing.

The new-file modal reuses overlay_file_editor.html with is_new=True;
this test exercises the branch in routedSaveClicked that composes
fullPath from filename + data-at-folder, distinct from the
rename-on-save path the edit-mode test covers.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:39:51 +02:00
mwiegand
3cafdba2cc
test(files): cover text-file edit round trip
Opens server.cfg through the file-row name button, drives the CM6
controller via window.__filesEditor.setContent, clicks save, and
asserts both that the routed modal closes and that the new bytes
landed on disk under overlay_root. Guards against four classes of
regression at once: the /files/edit fragment delivering the wrong
data-rel-path, editor.js's htmx:afterSwap re-init failing to wire
__filesEditor, routedSaveClicked stopping short of closeRouted(),
and the /files/save endpoint failing to persist.

Adds a `_wait_for_routed_editor` helper centered on `.cm-content`
inside `#files-editor-fragment` — the textarea itself is
display:none after CM6 mounts, so to_be_visible on the textarea
would always fail; the cm-content surface is the real "editor is
ready" signal.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:39:03 +02:00
mwiegand
19357124f4
docs(editor): document vocab argument shape on rankVocab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 18:38:01 +02:00
mwiegand
2060af44f2
fix(console): guard against missing window.__rankVocab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 18:37:42 +02:00
mwiegand
911bbf9103
test(files): add files-overlay e2e fixture
Extracts the shared boot/serve plumbing out of live_server into
_boot_app + _serve helpers, then adds files_overlay_server: a
function-scoped fixture that monkey-patches LEFT4ME_ROOT to tmp_path
BEFORE create_app(), seeds a files-type Overlay owned by alice, and
populates the overlay root with one editable text file, one binary
file, and one nested folder. Sets up the surface area the Tier-1
files-overlay e2e tests need without duplicating the live_server
boilerplate.

Also exposes a top-level `login(page, base_url, ...)` helper so
future test modules can share it instead of re-pasting the form-POST
flow.

Per docs/superpowers/plans/2026-05-17-files-overlay-e2e-handoff.md.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 18:35:43 +02:00
mwiegand
2d5a72b317
fix(editor): rebuild script and docs cover both bundles
build-editor.sh was calling npx esbuild directly for editor-entry.js
only, leaving vocab-rank.bundle.js stale when devs used the documented
rebuild path. Switch to npm run build (the single source of truth in
package.json) so both bundles are always rebuilt together. Add
vocab-rank.bundle.js to the sha256 manifest and update the vendor README
to describe both build artifacts.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 18:21:47 +02:00
mwiegand
2173685de6
feat(console): wire up autocomplete bundle + stylesheet in base.html
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 17:56:11 +02:00
mwiegand
7aa9b0b49c
fix(console): use existing CSS tokens for autocomplete dropdown
Replace phantom token refs (--border-strong, --fg-muted, --font-mono)
with the project's real tokens (--color-border, --color-muted) or a
plain font stack where no project-wide token exists.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 17:54:44 +02:00
mwiegand
5a85153c4f
feat(console): add autocomplete dropdown stylesheet 2026-05-17 17:52:02 +02:00
mwiegand
cdb6a87960
fix(console): apply review fixes for first-keystroke race and exact-match Tab
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 17:50:31 +02:00
mwiegand
40961eacdd
feat(console): add vanilla autocomplete dropdown module 2026-05-17 17:45:31 +02:00
mwiegand
d8dd2d23d2
feat(editor): build standalone vocab-rank bundle for console
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 17:41:24 +02:00
mwiegand
ca6a7aa74c
refactor(editor): use shared rankVocab in autocomplete
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 17:37:54 +02:00
mwiegand
9ff93164d7
feat(editor): extract pure rankVocab module + tests
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-05-17 17:33:26 +02:00
mwiegand
8dc14f0cca
feat(files): wire askConflict into the routed new-file 409 path
The is_new branch of routedSaveClicked in editor.js used to alert on
409 and force the user to manually pick a different filename. Restore
the overwrite / keep-both / cancel prompt the legacy openEditorTextNew
flow had (via askConflict, lost when Step 9 deleted legacySaveClicked).

Flow on 409:
  * "overwrite" → re-POST /save with the same path. /save overwrites
    in place when the destination is a regular file.
  * "keep-both" → compose a suffixed path via fo.withCollisionSuffix
    (now multi-extension-aware after F3) and POST that.
  * "cancel" → leave the routed modal open with the user's typed
    content intact so they can edit the filename and retry.

Defensively gates the askConflict + withCollisionSuffix calls on
typeof === "function" so older bookmarks (or a dev environment with
one of the modules missing) fall back to an alert rather than a
TypeError. The 409 alert branch is preserved for that path.

Note on when /save actually 409s: regular-file collisions overwrite
silently (200). 409 fires only when the new path collides with a
directory (or a symlink, or a non-file fs entry) — same contract as
the legacy flow had.

Verified live on /overlays/2 in Chromium with a real round-trip:
  1. Click "+ new folder" → create tmp_409_probe
  2. Click "+ new file" → type "tmp_409_probe" → click Create
  3. /save returns 409 (destination is not a file) → askConflict
     opens with the colliding path displayed
  4. Routed modal stays open behind the conflict dialog (typed
     content preserved)
  5. Cancel on conflict → conflict closes, routed modal still open
  6. Cleanup: delete the tmp_409_probe folder via the action API
  * No console errors throughout
  * Demo overlay state unchanged after cleanup

pytest stays at 577 passed, 1 skipped, 3 deselected (no Python
changes in F4).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:12:14 +02:00
mwiegand
8ccb2339ca
fix(files): handle double-extensions in withCollisionSuffix
The legacy implementation used lastIndexOf('.') which splits inside
compressed-tarball extensions: foo.tar.gz collided to foo.tar (1).gz,
which then isn't valid as a .gz to unpack and is also ugly.

Recognize the common double-extensions (.tar.gz, .tar.bz2, .tar.xz,
.tar.zst, .tar.lz, .tar.lzma) and treat the entire suffix as a single
logical extension when collision-renaming. So:

  foo.txt          → foo (1).txt              (unchanged)
  foo.tar.gz       → foo (1).tar.gz           (fixed)
  archive.tar.bz2  → archive (1).tar.bz2      (fixed)
  ARCHIVE.TAR.XZ   → ARCHIVE (1).TAR.XZ       (case-insensitive detect)
  subdir/foo.tar.gz → subdir/foo (1).tar.gz   (works with nested paths)
  backup.tar       → backup (1).tar           (single .tar, unchanged)
  config.local.json → config.local (1).json   (multi-dot non-tar, unchanged)
  README           → README (1)               (no extension, unchanged)
  .hidden          → ' (1).hidden'            (legacy quirk preserved)

Detection is lowercase against the basename only, so /path/with.dots/
in folder names doesn't trip the match.

The .hidden edge case (leading-dot basename, no extension) keeps its
legacy '" (1).hidden"' result — fixing it requires changing the dot >
slash predicate which would also shift other behaviors. Marked
separately if anyone wants to revisit.

The helper is exposed on window.__filesOverlay.withCollisionSuffix
and is called from two paths in uploads.js (the upload-conflict
'keep both' branch and the move-conflict 'keep both' branch via
askConflict). Both paths now produce a sensible filename when the
colliding path uses a double-extension.

No pytest tests added: the helper is JS-only and the project has no
JS test framework (per the plan's Out-of-Scope clause). Chromium
manual verification via window.__filesOverlay.withCollisionSuffix
called on 10 inputs confirms each case above.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:06:44 +02:00
mwiegand
1de61e8e4d
refactor(files): drop the always-True download_supported flag
download_supported was set to True at every call site (3 templates, 2
route render calls) and never to False. The {% set show_download =
download_supported and not entry.broken %} branch in
_overlay_file_node.html was therefore equivalent to {% set
show_download = not entry.broken %}, and {% set has_actions =
(files_overlay or show_download) and not entry.broken %} simplifies
further: when not broken, both clauses make has_actions True
regardless of files_overlay; when broken, both clauses make it False.
So has_actions = not entry.broken.

Collapsed:
  * Removed download_supported = True from overlay_detail.html (×2),
    server_detail.html, and the two render_template calls in
    files_routes.py
  * Removed the show_download intermediate and the inner {% if
    show_download %} guard in _overlay_file_node.html (the surrounding
    {% if has_actions %} already guarantees not entry.broken)
  * has_actions now directly equals not entry.broken

If a future requirement actually wants per-overlay download-disable,
re-introduce a flag at that point with a real callsite that sets it
False (and a test that exercises that path).

pytest: 577 passed, 1 skipped, 3 deselected — unchanged. None of the
existing tests gated on download_supported.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 17:05:17 +02:00
mwiegand
5f82950d7c
feat(files): delete /files/content endpoint + extract _apply_optional_rename
Step 12/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
End of Phase C — end of the rewrite plan.

Two cleanups in one commit:

1. Delete GET /overlays/<id>/files/content.

The legacy openEditorForFile in files-overlay.js was its only caller,
and Step 9 deleted that code path. grep confirms no remaining live
callers (the matches in .claude/worktrees/* are other in-flight
branches; matches in docs/ are plan/spec text describing the route's
history). Removed:

  * The @bp.get route function (was already a thin wrapper around
    _load_file_for_editing from Step 11)
  * The endpoint's mention in the module docstring
  * test_content_returns_text
  * test_content_returns_415_for_binary
  * test_content_404_for_non_files_overlay
  * The /files/content entry in the batched "non-files-overlay 404s
    everywhere" test

The _load_file_for_editing helper from Step 11 becomes single-caller
(only the edit route uses it now). Kept because the function name
gives the prelude a useful named concept and inlining would add ~17
lines of low-density logic into overlay_file_edit_page.

2. Extract _apply_optional_rename.

overlay_file_save and overlay_file_replace had near-identical rename
branches: safe_resolve_for_move → 422-on-traversal, 409-if-dst-exists,
mkdir-parents, os.rename → echo_path = new_path. Extracted into
_apply_optional_rename(overlay, path, new_path) → (write_target,
echo_path) | Response.

The helper handles both cases:
  * Rename: atomic rename, returns (dst, new_path)
  * No rename: safe_resolve_for_write, mkdir parents, returns
    (write_target, path)

Save's "destination is not a file" 409 (creation branch) stays inline
in overlay_file_save — it's save-specific behavior that doesn't apply
to /replace (which assumes a file exists or creates one).

Subtle behavior change in /save: the prior code called
safe_resolve_for_write(new_path or path) upfront and then potentially
overrode write_target via safe_resolve_for_move. The new code only
calls one validator per branch. Confirmed equivalent: per
overlay_files.py:36-58 (safe_resolve_for_write) vs. lines 76-106
(safe_resolve_for_move), the dst-side checks are identical (root
escape, symlink refuse, parent-is-dir) and safe_resolve_for_move adds
strictly more (src must exist, cycle check for directory moves). pytest
covers the save/replace paths and stays green.

pytest: 580 → 577 passed, 1 skipped, 3 deselected. The -3 is the 3
deleted /files/content tests.

files_routes.py: ended at 735 lines. The plan estimated ~450 — the
delta is the new /files/new route (~43 lines, Step 5), the binary
template branch (Step 7), the _load_file_for_editing helper (Step 11),
and module-header expansions. The structural goal (no dead routes,
shared helpers across paths) is met.

End-of-plan summary:
  * 1091-line files-overlay.js → 4 focused modules totaling 1191 lines
    (core.js 247, editor.js 309, dialogs.js 212, uploads.js 423)
  * Editor flows (text edit, binary replace, create new) all run
    through URL-addressable modals (?modal= deep-linkable)
  * Legacy <dialog id="files-editor-modal"> deleted
  * /files/content deleted (dead endpoint)
  * Shared helpers _load_file_for_editing + _apply_optional_rename
  * pytest stayed green at every step
  * Chromium-verified every step

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:29:55 +02:00
mwiegand
3facc323b6
refactor(files): extract _load_file_for_editing helper
Step 11/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.

overlay_file_content and overlay_file_edit_page used to repeat the
same prelude: load the files-overlay, resolve sub_path safely, confirm
it's a regular file, attempt a UTF-8 read with the is_editable sniff
+ UnicodeDecodeError-on-tail fallback. The bodies have diverged in
two places (the binary branch differs — content endpoint returns 415,
edit page renders the binary template) but the prelude is identical.

Extract _load_file_for_editing(overlay_id, sub_path, user). Returns
either a Flask Response (400/404/403/500) or a 5-tuple
(overlay, target, content_or_None, is_binary, byte_count). Both
routes call it and translate the tuple into their output shape:

  * overlay_file_content: 415 when is_binary, else jsonify text
  * overlay_file_edit_page: render binary template when is_binary,
    else render text template

The previous _render_binary_editor helper folds back into the route
since it was only called from there and benefits from inlining now
that the prelude is shared.

is_binary in the tuple is True both when is_editable() said no up
front (8-KiB sniff) AND when the tail-of-file read raised
UnicodeDecodeError — collapsing what was a duplicated try/except
branch in each route.

No behavior change. pytest still 580 passed, 1 skipped, 3 deselected.

Step 12 (next) audits /files/content callers and likely deletes the
endpoint since Phase B removed its only caller (legacy
openEditorForFile via files-overlay.js); the shared helper would
then collapse to single-caller status but remains useful for
grouping the prelude into a named concept.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-17 16:24:32 +02:00
mwiegand
ddf03c6fb8
feat(files): delete files-overlay.js stub + its script tag
Step 10/12 of docs/superpowers/plans/2026-05-17-files-overlay-rewrite.md.
End of Phase B.

The 19-line tombstone file at static/js/files-overlay.js had no code
since Step 4; it was kept around as a stable resolution target for the
<script> tag while the modules de-duplicated. After Step 9 made the
modules complete and stand-alone, the stub is just a wasted HTTP
request and a misleading filename in the manifest. Deleted, plus the
matching <script defer> tag in overlay_detail.html.

Final loader shape — exactly 4 script tags, all defer, all in
files-overlay/:
  1. core.js   — helpers, manager guard, action registry
  2. editor.js — URL-addressable editor (text + binary + new-file)
  3. dialogs.js — new-folder + delete-confirm + conflict
  4. uploads.js — upload queue + drag-drop + zip action

Verified live on /overlays/2 in Chromium:
  * Exactly 4 files-overlay script tags load (no more files-overlay.js)
  * window.__filesOverlay registry has its 10 keys; askConflict +
    withCollisionSuffix + handleAction + registerHandler all functions
  * File tree renders (3 file rows + 1 folder row, as before)
  * No legacy #files-editor-modal in DOM
  * No console errors
  * pytest still 580 passed, 1 skipped, 3 deselected

Phase B end-state vs. Phase A end-state:
  * editor.js: 550 → 309 lines (Step 9 gutted legacy)
  * files-overlay.js: 19 → 0 (deleted in this step)
  * 5 new pytest tests for the /files/new + binary template
  * Legacy <dialog id="files-editor-modal"> gone from
    overlay_detail.html
  * Editor flows (text edit, binary replace, create new) all run
    through the URL-addressable modal (?modal= deep-linkable)

Phase C (steps 11–12) is server-only: extract a shared path-resolution
helper between overlay_file_content + overlay_file_edit_page, then
delete /files/content if grep confirms no remaining callers.

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