From 76bd6e8d4d254b9944d1c50b61ff0e85e19e4911 Mon Sep 17 00:00:00 2001 From: mwiegand Date: Fri, 8 May 2026 20:16:10 +0200 Subject: [PATCH] =?UTF-8?q?docs(specs):=20overlay=20file=20tree=20?= =?UTF-8?q?=E2=80=94=20design=20+=20implementation=20plan?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Captures the design rationale for the new overlay-detail Files section (verify build output, click-to-download for individual files via Flask send_file, HTMX-driven lazy folder expansion) and the paired implementation plan that produced it. Adds .superpowers/ to .gitignore so brainstorm session artifacts never sneak into a future commit. Co-Authored-By: Claude Opus 4.7 (1M context) --- .gitignore | 1 + .../plans/2026-05-08-overlay-file-tree.md | 161 ++++++++++++++++++ .../2026-05-08-overlay-file-tree-design.md | 117 +++++++++++++ 3 files changed, 279 insertions(+) create mode 100644 docs/superpowers/plans/2026-05-08-overlay-file-tree.md create mode 100644 docs/superpowers/specs/2026-05-08-overlay-file-tree-design.md diff --git a/.gitignore b/.gitignore index 6ef3e46..7ac5e76 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ __pycache__/ l4d2web.db* # CocoIndex Code (ccc) /.cocoindex_code/ +.superpowers/ diff --git a/docs/superpowers/plans/2026-05-08-overlay-file-tree.md b/docs/superpowers/plans/2026-05-08-overlay-file-tree.md new file mode 100644 index 0000000..f56561d --- /dev/null +++ b/docs/superpowers/plans/2026-05-08-overlay-file-tree.md @@ -0,0 +1,161 @@ +# Overlay File Tree Implementation Plan + +> **Approval status:** User-approved 2026-05-08; implemented + deployed in the same session. This plan is committed retrospectively to record the work. + +**Goal:** Build the overlay-detail "Files" section per `docs/superpowers/specs/2026-05-08-overlay-file-tree-design.md` — a server-rendered collapsible tree of `${LEFT4ME_ROOT}/overlays/{overlay.id}/` with HTMX lazy expansion and click-to-download for individual files. Read-only; same access rule as the rest of the overlay detail page. + +**Architecture:** A new `files_bp` blueprint exposes two GETs: `/overlays//files?path=` returns the listing as an HTML fragment (used both for first paint at the root level via `page_routes.overlay_detail` context, and for HTMX swaps when a folder expands), and `/overlays//files/download?path=` streams a single file. Pure helpers live in `l4d2web/services/overlay_files.py`: `safe_resolve_for_listing` (refuses symlink escape from overlay root), `safe_resolve_for_download` (allows symlink targets anywhere under `LEFT4ME_ROOT` — workshop addons stream from the shared cache; absolute symlinks to `/etc/passwd` are still blocked), and `list_directory` (one-level scan, dirs-first sort, 500-entry cap, symlink + broken-symlink markers, resolved size for files). Two Jinja partials (`_overlay_file_tree.html`, `_overlay_file_node.html`) plus a 12-line event-delegated `static/js/file-tree.js` for collapse/re-expand handle the UI; styles append to `static/css/components.css` against existing tokens. + +--- + +## Locked Decisions + +See the design doc for rationale. Implementation-relevant summary: + +- New blueprint `files_bp` registered in `l4d2web/app.py` next to `overlay_bp`. +- Path resolution chains through `l4d2host.paths.overlay_path()` (already validates the overlay ref + resolves under `LEFT4ME_ROOT/overlays/`) and `l4d2web.services.security.validate_overlay_ref` (rejects empty/`.`/`..`/absolute/whitespace/backslash for the sub-path component). +- Listing rule: target must be a descendant of `overlay_root` after `Path.resolve()`. Download rule: real path must be a descendant of `LEFT4ME_ROOT` after `os.path.realpath()`. +- Tree shape: single recursive partial. `_overlay_file_tree.html` renders `