From b1e34257ed65d20acae72971f760bab716f50765 Mon Sep 17 00:00:00 2001 From: Julius Simonelli Date: Sat, 8 Aug 2026 23:00:48 +0200 Subject: [PATCH 01/14] spec: dedicated card-cleanup page (relocation follow-up) Co-Authored-By: Claude Fable 5 --- .../2026-08-08-card-cleanup-page-design.md | 76 +++++++++++++++++++ 1 file changed, 76 insertions(+) create mode 100644 docs/superpowers/specs/2026-08-08-card-cleanup-page-design.md diff --git a/docs/superpowers/specs/2026-08-08-card-cleanup-page-design.md b/docs/superpowers/specs/2026-08-08-card-cleanup-page-design.md new file mode 100644 index 000000000..01c48184e --- /dev/null +++ b/docs/superpowers/specs/2026-08-08-card-cleanup-page-design.md @@ -0,0 +1,76 @@ +# Card cleanup gets its own page — design + +**Date:** 2026-08-08 +**Status:** Approved in discussion with maintainer (relocation follow-up to +PR #1436); lightweight spec — the feature's behavior is unchanged and +governed by `2026-08-07-card-cleanup-design.md`. +**Scope:** Move the "Free up card space" UI from a collapsed section on the +import page to a dedicated page with a navbar entry; add an inline +integrity-audit affordance; keep the post-import entry point as a link. +No backend behavior changes to scan/delete. + +## Problem + +The tool lives inside the import page although its task is the opposite of +importing, so a user arriving cold (days after an import) doesn't find it. +Worse, its enabling dependency — the integrity audit that stamps +`hash_status='ok'` — lives on the audit page, so the first-run experience +on a pre-existing archive dead-ends: everything lands in "kept — run the +integrity audit" with no way to do that from where you're standing +(observed in production 2026-08-08: 5,523/5,523 files kept for exactly +this reason). + +## Design + +1. **New page** `vireo/templates/card_cleanup.html`, served at + `/card-cleanup` (route follows the existing page-route pattern in + app.py). Standalone page in the codebase's one-file-per-page style + (includes `_navbar.html`, inline CSS/JS). Contains the entire + scan → preview → delete flow moved from import.html, preserving the + existing ids/function names (`card-cleanup-*` / `cardCleanup*`) and + ALL user-facing copy verbatim — including the byte-exact confirmation + dialog from the parent spec, the incomplete-preview banner, and the + honest summary states. The section is no longer collapsed: on its own + page it renders expanded. +2. **Support code the section leaned on in import.html** (folder browser, + `formatBytes`, progress/`field-error`/modal CSS): the new page brings + its OWN minimal copies scoped to its needs — a single-select folder + browser (the card-cleanup `browserMode` variant), `formatBytes`, and + the small CSS set. Deliberate duplication over a shared-include + refactor of import.html: the parent PR's reviewers accepted mirrored + code at this scale, and un-inlining import.html's browser is exactly + the kind of unrelated refactor the parent spec declined. +3. **Inline audit affordance.** When the rendered preview's kept bucket + contains the `KEEP_NOT_VERIFIED` reason ("not verified by a checksummed + import — run the integrity audit"), show a callout above the buckets: + how many kept files carry that reason, one sentence explaining that + the archive copies must be checksum-verified before deletion is + allowed, a **Verify archive hashes** button that starts the existing + `verify-hashes` job (`POST /api/jobs/verify-hashes`) with the page's + standard progress rendering, and — on completion — a **Re-scan card** + affordance. Include the SMB cost warning: verification re-reads + archive files; on a VPN'd mount this is slow, best run close to the + NAS. No new backend endpoints. +4. **Navbar**: add "Card cleanup" to `_navbar.html` in the tools/pages + list (match existing nav idiom and ordering conventions). +5. **Import page**: remove the moved section and its JS; keep the + card-safety-pill entry point, now a link to + `/card-cleanup?source=` (URL-encoded). The new page pre-fills + its source input from the `source` query parameter. Multi-source + imports pass the first source; the new page keeps the existing + "remaining sources" hint behavior. +6. **No API changes.** Scan/delete/manifest endpoints, job types, and + manifest format are untouched. + +## Testing + +- Page route test: `GET /card-cleanup` → 200, contains the section + markup (mirrors existing page-route tests). +- Existing card-cleanup API tests unchanged and green. +- Import page test (if any asserts the section's presence) updated; a + test asserts import.html no longer contains the section container and + DOES contain the pill link. +- JS of both changed templates re-checked with node --check; no + duplicate ids on either served page. +- Manual visual QA remains pending for the human (both themes), as with + the parent PR. From 9c4ac52798d255f55f772865607310d5aa38f558 Mon Sep 17 00:00:00 2001 From: Julius Simonelli Date: Sat, 8 Aug 2026 23:15:03 +0200 Subject: [PATCH 02/14] feat: move Free up card space to a dedicated /card-cleanup page MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The card-cleanup tool lived in a collapsed section on the import page, which is where nobody looks days after an import — and its enabling dependency (the integrity audit that stamps hash_status='ok') lived on yet another page, so a first run on a pre-existing archive dead-ended with everything kept for "run the integrity audit". - New page vireo/templates/card_cleanup.html at /card-cleanup, rendered expanded, with the whole scan → preview → delete flow moved over: same ids, same function names, same user-facing copy (confirmation dialog, incomplete-preview banner, summary states) byte for byte. It brings its own minimal single-select folder browser (same /api/browse and /api/volumes endpoints) and formatBytes rather than un-inlining the import page's. - Inline integrity-audit affordance: when kept entries carry the KEEP_NOT_VERIFIED reason, a callout above the buckets says how many files that is, explains why they can't be deleted, warns that verification re-reads the whole archive (slow over a VPN'd mount), and offers "Verify archive hashes" (the existing POST /api/jobs/verify-hashes job) with the page's normal progress rendering plus a "Re-scan card" follow-up on completion. - Import page keeps the card-safety pill's "Free up card space…" button; it now navigates to /card-cleanup?source= (with the remaining folders as `others=` so the multi-source hint survives). The moved markup, JS, CSS and the folder browser's third mode are gone; the browser is back to its pre-feature two-mode shape. - Navbar/registry entry "Card cleanup" (app.ALL_PAGES, db.ALL_NAV_IDS, _navbar.html's mirrored list), page route in web/pages.py, route contract snapshot regenerated. No backend or API changes to the card-cleanup endpoints. Tests: vireo/tests/test_card_cleanup_api.py gains a page-route test and an import-page test (section gone, pill link present). test_card_cleanup_api/test_card_cleanup/test_path_guard/ test_route_contract/test_tabs_api/tests/test_workspaces: pass. test_app.py: 423 passed, 1 pre-existing local failure (test_api_exiftool_status_reports_missing). node --check on both templates' inline JS: clean; no duplicate ids on either rendered page. Co-Authored-By: Claude Fable 5 --- vireo/app.py | 2 + vireo/db.py | 2 +- vireo/templates/_navbar.html | 2 + vireo/templates/card_cleanup.html | 1040 ++++++++++++++++++++++++++ vireo/templates/import.html | 678 +---------------- vireo/tests/contracts/routes.txt | 1 + vireo/tests/test_card_cleanup_api.py | 24 + vireo/web/pages.py | 1 + 8 files changed, 1094 insertions(+), 656 deletions(-) create mode 100644 vireo/templates/card_cleanup.html diff --git a/vireo/app.py b/vireo/app.py index 9dcd9df36..e93048101 100644 --- a/vireo/app.py +++ b/vireo/app.py @@ -158,6 +158,8 @@ {"id": "dashboard", "label": "Dashboard", "href": "/dashboard"}, {"id": "storage", "label": "Storage", "href": "/storage"}, {"id": "audit", "label": "Audit", "href": "/audit"}, + {"id": "card_cleanup", "label": "Card cleanup", "href": "/card-cleanup", + "keywords": "card cleanup free space delete verified memory card format sd"}, {"id": "move", "label": "Move", "href": "/move"}, {"id": "id_conflicts", "label": "ID Conflicts", "href": "/id-conflicts", "keywords": "compare conflict prediction model disagreement species keyword classify review"}, diff --git a/vireo/db.py b/vireo/db.py index 388b39e71..0ea0cdd8b 100644 --- a/vireo/db.py +++ b/vireo/db.py @@ -376,7 +376,7 @@ def _chunks(values, size=_SQLITE_PARAM_CHUNK_SIZE): "import", "pipeline", "jobs", "pipeline_review", "pipeline_rapid_review", "review", "cull", "misses", "highlights", "life_list", "browse", "edit", "map", "location_review", "variants", - "dashboard", "storage", "audit", "move", "id_conflicts", + "dashboard", "storage", "audit", "card_cleanup", "move", "id_conflicts", "settings", "workspace", "lightroom", "shortcuts", "keywords", "duplicates", "logs", }) diff --git a/vireo/templates/_navbar.html b/vireo/templates/_navbar.html index bdb9945cb..af26664ef 100644 --- a/vireo/templates/_navbar.html +++ b/vireo/templates/_navbar.html @@ -2382,6 +2382,8 @@

Keyboard Shortcuts + + + + +
+ +
+
+ + +
+ + + + + + + + + +
+ +
+ + +
+ +
+ + + + diff --git a/vireo/templates/import.html b/vireo/templates/import.html index 43d04c238..52089c2d6 100644 --- a/vireo/templates/import.html +++ b/vireo/templates/import.html @@ -224,26 +224,6 @@ .advanced-import { margin-top: 10px; font-size: 12px; color: var(--text-secondary); } .advanced-import summary { cursor: pointer; } .metadata-repair { margin-top: 10px; padding-top: 10px; border-top: 1px solid var(--border-primary); } -/* Free up card space. The section is a
, so its - summary has to carry the same weight as the other cards'

. */ -#card-cleanup-section > summary { - cursor: pointer; font-size: 14px; font-weight: 600; color: var(--text-primary); -} -#card-cleanup-section[open] > summary { margin-bottom: 10px; } -.card-cleanup-bucket { margin-top: 6px; } -.card-cleanup-bucket > summary { cursor: pointer; font-size: 12px; color: var(--text-primary); } -/* Same callout shape as .managed-archive-callout, in warning colours: an - incomplete preview must not read as a quiet footnote. */ -.card-cleanup-warning { - font-size: 12px; color: var(--text-primary); margin: 8px 0; - padding: 8px 10px; border-radius: 6px; overflow-wrap: anywhere; - background: color-mix(in srgb, var(--warning, #bf8700) 12%, transparent); - border: 1px solid color-mix(in srgb, var(--warning, #bf8700) 45%, transparent); -} -/* Taller than .staging-details: these lists run to thousands of files. */ -#card-cleanup-section .staging-details { max-height: 220px; } -.card-cleanup-confirm-list { margin: 0; padding-left: 18px; font-size: 13px; color: var(--text-primary); } -.card-cleanup-confirm-list li { margin-bottom: 10px; overflow-wrap: anywhere; } @@ -499,10 +479,11 @@

Result

+ path to hand to the scanner. Opens the Card cleanup page with + this import's card folder pre-filled. --> + onclick="openCardCleanupPage()">Free up card space…
@@ -520,72 +501,6 @@

Result

- -
- Free up card space -
- Checks a memory card against the archive and deletes only the files - whose archive copy is verified. Everything else stays on the card. -
-
- - -
- -
- -
-
- - -
- - - - -
- @@ -612,33 +527,6 @@

Select Folder

- -
- -
-