Skip to content

Move Free up card space to a dedicated /card-cleanup page - #1443

Merged
github-actions[bot] merged 14 commits into
mainfrom
card-cleanup-page
Aug 9, 2026
Merged

Move Free up card space to a dedicated /card-cleanup page#1443
github-actions[bot] merged 14 commits into
mainfrom
card-cleanup-page

Conversation

@jss367

@jss367 jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner

What

Implements docs/superpowers/specs/2026-08-08-card-cleanup-page-design.md,
the relocation follow-up to #1436.

  • New standalone page /card-cleanup (navbar entry) holding the entire
    scan → preview → delete flow, moved from the import page with all
    user-facing copy byte-identical (confirmation dialog, incomplete-preview
    banner, summary states — verified by scripted comparison).
  • New inline audit affordance: when kept files carry the "not verified by
    a checksummed import" reason, the page shows the exact count, explains
    the checksum gate, offers Verify archive hashes (the existing
    verify-hashes job, with progress and an honest result summary), warns
    about the SMB/VPN cost, and offers Re-scan card on completion.
    This closes the first-run dead end observed in production (5,523/5,523
    files kept with that reason and no path forward from the page).
  • Import page trimmed back: the folder browser is byte-restored to its
    pre-feature form; the card-safety-pill button now deep-links to
    /card-cleanup?source=<path> (plus others= so the multi-source hint
    survives verbatim).
  • One job at a time, visibly: a single busy-state owner disables
    scan/delete/verify buttons during any run (the Delete hint says why),
    preventing a mid-audit delete from hijacking the job watch.
  • No backend changes to the card-cleanup endpoints; the page carries its
    own minimal single-select folder browser against the same /api/browse
    and /api/volumes endpoints (deliberate duplication per the spec).

Tests

Feature suites: 88 passed (incl. new page-route, import-page-trim, and
audit-callout coupling tests). test_app.py + tests/test_workspaces.py:
558 passed, 1 failed — the known pre-existing machine-local exiftool
test. Route-contract snapshot regenerated (+1 line). node --check clean
on both changed templates; no duplicate ids on either rendered page;
ruff clean. Manual visual QA (both themes) pending, as with #1436.

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added a dedicated Card Cleanup page accessible from the navigation.
    • Supports folder selection, recursive scanning, progress tracking, previews, integrity audits, cancellation, and deletion.
    • Added warnings for incomplete scans and archive integrity issues.
    • Completed imports now link directly to Card Cleanup with source folders prefilled.
  • Bug Fixes

    • Improved handling of failed, corrupt, modified, and unreadable archive verification results.
    • Prevented deletion when an audit requires rescanning.

jss367 and others added 4 commits August 8, 2026 23:00
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
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=<first card folder> (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 <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…upling

Review follow-ups to the /card-cleanup page move.

Important: the Delete button stayed live while a verify-hashes audit was
streaming. Clicking it hijacked the watch — cardCleanupWatchJob closed the
audit's EventSource and overwrote state.jobId, so cardCleanupFinishAudit
never ran: audit button stuck disabled, blank status, no "Re-scan card",
and the audit itself still running on the server with nothing on the page
tracking it. The same silent hole existed the other way round, where
"Scan card" and "Verify archive hashes" looked pressable during a run and
early-returned.

- New cardCleanupSetBusy(running, opts) owns the three start buttons for
  the duration of any run (scan, delete, audit) and restores the Delete
  button's prior enabled state and hint afterwards, since what Delete may
  do depends on the manifest. During an audit the delete hint says why it
  is unavailable ("Verification is running — deletion is available when it
  finishes.") rather than greying out with no explanation.
- Defensive `if (cardCleanupState.jobId) return;` in
  cardCleanupOpenConfirm, and cardCleanupConfirmDelete now closes the
  dialog and reports "Another job is still running — nothing was deleted."
  instead of stranding the running job.
- Pin the audit-callout coupling both ways: a comment on
  card_cleanup.KEEP_NOT_VERIFIED explaining that card_cleanup.html matches
  its tail, plus test_audit_callout_reason_stays_in_sync asserting the
  served page carries the literal and the reason still contains it.
- Drop the dead `.card h3` rule (the page's card has no h3).

Tests: vireo/tests/test_card_cleanup_api.py + test_card_cleanup.py — 77
passed. test_route_contract + test_build_static pass. node --check on the
page's inline JS clean; ruff clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 9, 2026

Copy link
Copy Markdown

Review Change Stack

📝 Walkthrough

Walkthrough

The PR moves card cleanup from the import page to /card-cleanup. It adds navigation access, preserves scan and deletion behavior, adds archive hash auditing, updates failed-status classification, and passes imported source folders through query parameters.

Changes

Card cleanup workflow

Layer / File(s) Summary
Archive reason classification
vireo/card_cleanup.py, vireo/tests/test_card_cleanup.py
Archive rows now distinguish unverified archives from archives with failed hash verification. Tests cover specific and mixed statuses.
Route, navigation, and import handoff
vireo/web/pages.py, vireo/tests/contracts/routes.txt, vireo/app.py, vireo/templates/_navbar.html, vireo/db.py, vireo/templates/import.html, vireo/tests/test_card_cleanup_api.py, vireo/tests/test_db.py
The application registers /card-cleanup, adds navigation metadata, removes the embedded import-page interface, and passes source folders to the new page.
Standalone scan and audit flow
vireo/templates/card_cleanup.html, vireo/tests/test_card_cleanup_api.py
The new page supports folder browsing, scan progress, manifest previews, archive audits, warnings, retries, and audit-specific deletion controls.
Deletion flow and specification
vireo/templates/card_cleanup.html, docs/superpowers/specs/2026-08-08-card-cleanup-page-design.md
The page adds deletion confirmation, streamed results, cancellation, volume suggestions, query initialization, and the approved design specification.

Estimated code review effort: 4 (Complex) | ~60 minutes

Sequence Diagram(s)

sequenceDiagram
  participant User
  participant ImportPage
  participant CardCleanupPage
  participant CleanupJobs
  User->>ImportPage: complete card import
  ImportPage->>CardCleanupPage: navigate with source and others
  User->>CardCleanupPage: start scan
  CardCleanupPage->>CleanupJobs: start and monitor scan
  CleanupJobs-->>CardCleanupPage: return manifest preview
  User->>CardCleanupPage: audit and confirm deletion
  CardCleanupPage->>CleanupJobs: verify hashes and delete entries
  CleanupJobs-->>CardCleanupPage: return audit and deletion results
Loading

Possibly related PRs

  • jss367/vireo#1436: Adds the card-cleanup implementation that this PR moves into a standalone page.
  • jss367/vireo#1137: Changes the import-page folder-browser and source-selection behavior used by the handoff.
  • jss367/vireo#1139: Changes related import-page folder-browser behavior that this PR refines after removing cleanup mode.

Suggested labels: claude-agent

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 33.33% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: moving the card cleanup workflow to a dedicated /card-cleanup page.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch card-cleanup-page

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 0f41256d30

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/templates/card_cleanup.html Outdated
document.getElementById('card-cleanup-audit-status').textContent = '';
cardCleanupSetError('');
try {
const resp = await fetch('/api/jobs/verify-hashes', {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Verify every workspace represented by the preview

When a card file matches an unverified catalog row belonging only to an inactive workspace, the scan still shows this audit callout because card_cleanup._load_catalog_by_hash() queries photos globally, but this endpoint captures only the active workspace and Database.get_integrity_photos() restricts verification to that workspace. Clicking Verify archive hashes therefore never verifies the archive copy that triggered the callout, so re-scanning leaves the file unchanged. Either scope cleanup candidates to the active workspace or run verification for every workspace represented by the pending entries.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in db739e2, taking the transparency route rather than either suggested restructuring: scoping cleanup candidates to the active workspace would contradict the parent spec (matching is deliberately global — photos are global), and fanning verification out across every represented workspace is a backend change out of this PR's scope. Instead the callout's cost line now states the audit covers only the current workspace's folders, and — once a verification has run — files still pending on the next scan get a specific hint (their archive copies were not covered; switch to the workspace holding those folders and verify there) instead of silently looping back to the same callout.

@jss367 jss367 added the claude-agent PR managed by Claude PR Agent label Aug 9, 2026 — with Claude
The card scan matches photos across all workspaces, but verify-hashes
covers only the current workspace's folders — so the audit button could
never verify some of the files the callout counts, and a re-scan would
loop back to the same callout. The cost line now states the scope, and
after a verification has run, files still pending on the next scan get
a specific hint (other workspace's folders — switch there and verify)
instead of a silent loop.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: db739e28e6

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +755 to +761
document.getElementById('card-cleanup-audit-text').textContent =
cardCleanupFileCount(pending) +
(pending === 1 ? ' is' : ' are') +
' kept only because the archive ' +
'copy has never been checksum-verified. Vireo will not delete a card ' +
'file until its archive copy passes a checksum check, and the ' +
'integrity audit is what performs that check.';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Distinguish failed hash checks from unverified copies

When a card hash matches only catalog rows whose hash_status is modified, corrupt, or unreadable, qualify_rows() skips those rows but retains KEEP_NOT_VERIFIED, so this callout counts them too. Even in the active workspace, rerunning verification will reproduce the bad verdict rather than establish a verified copy, yet the UI says the copy has never been verified and later suggests the run merely missed it. Preserve the actual failed-verification reason or make the callout distinguish failed checks from rows that have never been checked.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in ae456b8.

qualify_rows() now separates NULL hash_status from failed (modified / corrupt / unreadable) and returns a distinct KEEP_ARCHIVE_HASH_FAILED reason ("archive copy failed a prior integrity check — see the Audit page") when the only non-ok rows have already been checked. A mix of NULL + failed still routes to KEEP_NOT_VERIFIED, because a NULL row could turn ok on the next audit and unlock the file, so the verify remedy stays real for that case.

The page gains a distinct hash-failed callout (same chrome as the audit callout, no verify button) that surfaces the count and points users at the Audit page instead of pretending another verify pass would help. test_hash_failed_callout_reason_stays_in_sync pins the coupling between the manifest reason and the callout literal.

Card-cleanup suites: 83 passed / 1 skipped. Full CLAUDE.md suite: 2075 passed / 16 skipped / 1 pre-existing env failure (test_failed_import_does_not_chainchmod 0 doesn't restrict root in this container; same as main).


Generated by Claude Code

Pre-existing coupling failure surfaced while running the routine's
test suite after the maintainer's UI transparency fix (db739e2)
addressed the Codex P2 finding on the audit-callout scope
mismatch. The card_cleanup nav id was added to ALL_NAV_IDS earlier
in this PR but the guard's expected set was not updated.

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Codex P2 review addressed. Looks like you got there first with db739e28 (UI transparency: state the workspace scope in the cost line, and specifically hint at "other workspace's folders" after a verify run) — that's a valid resolution and I've deferred to your design choice.

My initial pass took the other lever Codex suggested — scoping _load_catalog_by_hash and fetch_rows_by_hash to the active workspace so the scan and verify-hashes agreed on which photos count. I've dropped that so I don't unilaterally reverse your call on "keep the scan global and explain the limitation." Happy to bring it back if you'd prefer the behavior change over the text-only fix; it's noted in the transcript.

Pushed one small thing: the test_all_nav_ids_covers_every_page guard in vireo/tests/test_db.py still expected the pre-card_cleanup nav-id set, so it was failing on this branch. One-line fix, unrelated to the Codex thread.

Test suite from CLAUDE.md passes locally except pre-existing environmental failures noted in the PR description (exiftool/systemd/macOS-only trash tests).


Generated by Claude Code

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Acknowledged — nothing to push from this run.

Sticking with the text-only fix in db739e28 (state the workspace scope in the cost line, name "other workspace's folders" after a verify run). Reversing to the catalog-scoping approach mid-review would be a unilateral design flip and I don't have a reason to prefer it over your call here — the scan staying global is a defensible choice as long as the UI is honest about the split, which it now is. Happy to revisit if a future run turns up a concrete case where the text-only fix isn't enough (e.g. users hitting the loop despite the "switch workspace" hint).

Test guard fix in 7dd6b1c5 noted; that was the previous routine run picking up the ALL_NAV_IDS coupling failure while re-running the suite.


Generated by Claude Code

Codex flagged that qualify_rows() dropped rows whose hash_status was
'modified', 'corrupt', or 'unreadable' by continuing the loop but
leaving the default reason at KEEP_NOT_VERIFIED. Kept files backed
only by such rows then said "not verified — run the integrity audit"
in the manifest and, worse, added to the audit callout's count. But
running verification again just reproduces the same bad verdict; the
remedy for these files lives on the Audit page (accept / restore /
investigate), not in another verify run.

Fix: qualify_rows() now tracks NULL vs. failed hash_status separately
and falls back to a new KEEP_ARCHIVE_HASH_FAILED reason ("archive copy
failed a prior integrity check — see the Audit page") when the only
non-ok rows have already been checked. A mix of NULL + failed still
routes to KEEP_NOT_VERIFIED — a NULL row could turn "ok" on the next
audit and unlock the file, so the audit remedy is still real.

The card-cleanup page gains a distinct hash-failed callout (same
chrome as the audit callout, no button) that surfaces the count and
points users at the Audit page. Its coupling to the manifest reason
is pinned by test_hash_failed_callout_reason_stays_in_sync, mirroring
the existing audit-callout guard and asserting the two tail literals
do not overlap.

Also fix an unrelated pre-existing failure on this branch:
test_all_nav_ids_covers_every_page was missing 'card_cleanup' in its
expected set after the /card-cleanup page landed in 9c4ac52.

Test suite: 82 passed / 1 skipped in card-cleanup files (incl. three
new tests: modified/corrupt/unreadable each routing to the failed
reason, NULL+failed mix keeping the audit remedy, and the callout
sync guard). Full CLAUDE.md suite: 2074 passed / 1 pre-existing
machine-local exiftool failure unrelated to this diff.

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: ae456b8823

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +823 to +824
'would only reproduce that verdict — open the Audit page to accept ' +
'the current bytes, restore from backup, or investigate.';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Direct failed hashes to the owning workspace

When a card hash matches only a failed catalog row from an inactive workspace, this newly added callout sends the user to the Audit page, but _load_catalog_by_hash() scans photos globally while Database.get_integrity_flagged() filters to the active workspace. The flagged archive copy therefore will not appear on that Audit page, leaving the suggested accept/restore remedy unavailable. Include the same cross-workspace guidance used by the verification callout, or identify the workspace that owns the failed row.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 1213fec, taking the same transparency route used for the verify-hashes callout in db739e2 rather than reshaping the scope of _load_catalog_by_hash / Database.get_integrity_flagged.

Rationale: scoping the card scan's catalog lookup to the active workspace would contradict the parent spec ("photos are global — matching is deliberately global"), and expanding get_integrity_flagged to cross workspaces is out of this PR's scope. Both would be design flips I shouldn't make unilaterally after the maintainer picked the transparency route in the previous round.

Fix: card-cleanup-hash-failed-scope span inside the hash-failed callout names the current-workspace scope of the Audit page and tells the user to switch workspaces if the flagged archive copy lives in another workspace's folders. test_hash_failed_callout_states_audit_workspace_scope pins the element id and the two literal phrases so a reword can't silently drop the guidance.


Generated by Claude Code

… P2)

Third Codex P2 review on this PR points out the same workspace-scope
asymmetry the verify-hashes callout already addresses: the card scan
matches photos globally (_load_catalog_by_hash) but the Audit page's
listing (Database.get_integrity_flagged) is filtered to the active
workspace. A KEEP_ARCHIVE_HASH_FAILED entry backed only by a flagged
row in an inactive workspace's folder would send the user to an empty
Audit page — remedy unreachable.

Same transparency route taken for the verify-hashes callout (db739e2):
keep matching global (per the parent spec — photos are global), and be
honest about the workspace scope of the suggested remediation. The
hash-failed callout now names the current-workspace scope of the Audit
page and directs the user to switch workspaces if the flagged copy
lives elsewhere.

Pinned in test_hash_failed_callout_states_audit_workspace_scope so a
reword does not silently drop the guidance.

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Addressed the third Codex P2 (workspace scope of the hash-failed callout's Audit remedy) with the same transparency route you took for the verify-hashes callout in db739e2:

  • card_cleanup.html: adds a card-cleanup-hash-failed-scope hint span inside the hash-failed callout that names the current-workspace scope of the Audit page and tells the user to switch workspaces if the flagged archive copy lives in another workspace's folders. Matching, not restructuring — no backend/API changes; _load_catalog_by_hash and get_integrity_flagged keep their existing scopes.
  • test_card_cleanup_api.py: new test_hash_failed_callout_states_audit_workspace_scope pins the callout element id and the two literal phrases so a reword can't silently drop the guidance.

Rationale for choosing the text-only fix again (over scoping the catalog lookup or expanding the Audit query): matches the parent spec's "photos are global" design and the precedent you set in the previous round; both would be design flips I shouldn't make unilaterally.

Tests: test_card_cleanup.py + test_card_cleanup_api.py — 82 passed, 1 skipped (unchanged skip). Full CLAUDE.md suite otherwise green apart from the same pre-existing environmental failures noted in the PR description (macOS trash/send2trash, missing exiftool, systemd-inhibit); none touch this change.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 1213fec57d

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

'The verification is still running but this page lost track of it — ' +
'check the Jobs page.';
}
document.getElementById('card-cleanup-audit-rescan-btn').style.display = '';

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep deletion disabled until the post-audit rescan

When a preview contains both deletable and unverified files, completing or cancelling Verify archive hashes restores the Delete button's pre-audit enabled state in cardCleanupSetBusy(false), while this handler only exposes the re-scan button. Because verification can change previously ok rows to modified/corrupt/unreadable, the old confirmation can then advertise stale file and byte totals; the backend rechecks and skips invalid rows, but it may still delete the remaining valid subset despite the user confirming a different set. Disable deletion and replace its hint here until the requested re-scan produces a current manifest.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 97ba478.

cardCleanupFinishAudit now disables the Delete button and replaces its hint with "Verification may have changed which files count as verified — re-scan the card before deleting." on every terminal outcome (completed, cancelled, failed, lost-track). The Re-scan button next to the audit status is the visible remedy; the next successful scan calls cardCleanupResetPreviewloadManifest, which re-enables Delete from the fresh totals.

This closes the window where cardCleanupSetBusy(false) at the top of cardCleanupFinishJob restored the Delete button's pre-verify enabled state — even though verification could have flipped previously-ok rows to modified / corrupt / unreadable and the old confirmation dialog would then advertise stale totals against a set the user never agreed to. Backend still re-checks and skips invalid rows on delete, so this is defence-in-depth for the confirmation dialog's honesty.

test_finish_audit_disables_delete_until_rescan pins the coupling: the served page's cardCleanupFinishAudit body must both disable the Delete button and carry the specific "re-scan the card before deleting" hint, so a reword can't quietly drop the guardrail.


Generated by Claude Code

Codex P2 on 1213fec: cardCleanupSetBusy(false) restores the Delete
button's pre-verify enabled state at the top of cardCleanupFinishJob,
but verification can flip previously-ok rows to modified/corrupt/
unreadable. The confirmation dialog would then advertise stale file
and byte totals, and while the backend re-checks and skips invalid
rows on delete, the user would still be confirming a set that no
longer matches what would actually be deleted.

cardCleanupFinishAudit now disables the Delete button and replaces
its hint with a re-scan instruction on every terminal outcome (the
re-scan button is already surfaced next to the audit status). The
next successful scan calls cardCleanupResetPreview + loadManifest,
which restores Delete based on the fresh totals — no other flow
changes needed.

test_finish_audit_disables_delete_until_rescan pins the coupling:
the served page's cardCleanupFinishAudit body must both disable
the Delete button and carry the specific "re-scan the card before
deleting" hint, so a reword can't quietly drop the guardrail.

Tests: CLAUDE.md suite — 2158 passed, 17 skipped, 1 failed. The
one failure (test_failed_import_does_not_chain) also fails on main
in this environment (chmod 0 does not restrict root, so the test
cannot force an unreadable file); pre-existing, unrelated to this
change.

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Addressed Codex's P2 on 1213fec (Delete stays enabled after verify) in 97ba478.

cardCleanupSetBusy(false) at the top of cardCleanupFinishJob restores whatever the Delete button had before the verify run, which was the pre-audit enabled state whenever the preview held deletable files. Verification can flip previously-ok rows to modified/corrupt/unreadable, so the old confirmation dialog would then advertise stale file/byte totals; the backend re-checks and skips invalid rows on delete, but the user would still be confirming a set that no longer matches what would actually be deleted.

Fix: cardCleanupFinishAudit now disables the Delete button and replaces its hint with a re-scan instruction on every terminal outcome (the re-scan button is already the visible remedy next to the audit status). The next successful scan calls cardCleanupResetPreviewloadManifest, which re-enables Delete from the fresh totals — no other flow changes needed.

test_finish_audit_disables_delete_until_rescan pins the coupling: the served page's cardCleanupFinishAudit body must both disable the Delete button and carry the specific "re-scan the card before deleting" hint, so a reword can't quietly drop the guardrail.

Tests: CLAUDE.md suite — 2158 passed, 17 skipped, 1 failed. The one failure (test_failed_import_does_not_chain) also fails on main in this container because chmod 0 doesn't restrict root; pre-existing environmental issue, unrelated.


Generated by Claude Code

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@vireo/templates/card_cleanup.html`:
- Around line 571-576: Update the error handling in cardCleanupState.es and the
related cardCleanupFinishJob flow so a transient SSE disconnect continues
polling while the job status remains running or queued. Do not permanently close
tracking or clear jobId during that state; only invoke the lost-track handling
when polling confirms the job has disappeared.
🪄 Autofix

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 5acd31d1-c706-4346-bf53-9659cb0f37f3

📥 Commits

Reviewing files that changed from the base of the PR and between 40cf6c1 and 97ba478.

📒 Files selected for processing (12)
  • docs/superpowers/specs/2026-08-08-card-cleanup-page-design.md
  • vireo/app.py
  • vireo/card_cleanup.py
  • vireo/db.py
  • vireo/templates/_navbar.html
  • vireo/templates/card_cleanup.html
  • vireo/templates/import.html
  • vireo/tests/contracts/routes.txt
  • vireo/tests/test_card_cleanup.py
  • vireo/tests/test_card_cleanup_api.py
  • vireo/tests/test_db.py
  • vireo/web/pages.py

Comment thread vireo/templates/card_cleanup.html
The post-drop poll gave up after 30 seconds and re-enabled the buttons
while the job kept running on the server — the exact orphaning the busy
discipline exists to prevent, on a page whose scans and audits run for
many minutes. Poll indefinitely (1s for 30 attempts, then 10s), render
live progress from the polls, and stop only on a terminal status or
several consecutive 404s (server restart pruned the job), which gets
the honest lost-track copy.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Routine fired on a review with an empty top-level body (the review's only inline entry was your own reply on the resolved SSE-polling thread, which CodeRabbit verified as fixed in 96137e3b). No actionable request to address; not pushing.

All five prior review threads have landing commits — db739e28 (workspace-scope text), ae456b88 (distinguish failed vs unverified), 1213fec5 (hash-failed callout workspace scope), 97ba478b (delete disabled after verify), 96137e3b (SSE terminal-state polling). Ping me with a /claude-fix comment if there was something specific in that reply you wanted me to act on.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 96137e3ba0

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/templates/card_cleanup.html Outdated
Comment on lines +1091 to +1092
cardCleanupSetBusy(true);
cardCleanupWatchJob(data.job_id, 'delete', 'Starting deletion…');

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Lock the page before posting the delete job

When a mixed preview exposes both Delete and Verify, a user can confirm deletion, dismiss the dialog while this POST is pending, and start verification or another scan because the page is not marked busy until after the response arrives. Both server jobs then run, and whichever response finishes later calls cardCleanupWatchJob, closes the other job's event stream, and overwrites jobId, leaving one job—including potentially the destructive deletion—without the correct progress, cancellation, or result UI. Set a pending/busy state before issuing the delete request and clear it if the request definitively fails.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 012aec2c.

cardCleanupConfirmDelete was issuing POST /api/card-cleanup/delete before cardCleanupSetBusy(true). Because the confirm dialog is dismissable while the request is in flight (backdrop click / Escape), Scan and Verify stayed live in that window — a second job could start and its watch would then overwrite the delete's jobId, orphaning the destructive path.

Fix: move cardCleanupSetBusy(true, { deleteHint: 'Deletion is starting…' }) to run before the fetch (mirroring cardCleanupStartScan / cardCleanupStartAudit). Both failure paths — !resp.ok and the network-error catch — now call cardCleanupSetBusy(false) so a definitive failure hands the buttons back instead of leaving a dead page. card_cleanup.html:1076–1108.

test_confirm_delete_locks_page_before_post pins the coupling: cardCleanupSetBusy(true must appear before the /api/card-cleanup/delete fetch inside the handler, and both failure paths must release the lock. Rewording is fine; re-opening the gap isn't.


Generated by Claude Code

…econd job (Codex P2)

cardCleanupConfirmDelete previously issued POST /api/card-cleanup/delete
before calling cardCleanupSetBusy(true). The confirm dialog can be
dismissed (backdrop click or Escape) while the request is pending; in
that gap Scan and Verify stayed live, so a user could kick off a second
job whose watch would overwrite the delete's jobId — including the
destructive path. Move the busy call to run before the fetch (mirroring
cardCleanupStartScan / cardCleanupStartAudit), and add cardCleanupSetBusy(false)
on both the !resp.ok branch and the catch so a definitive failure hands
the buttons back instead of leaving a dead page.

test_confirm_delete_locks_page_before_post pins the coupling: setBusy
must appear before the /api/card-cleanup/delete fetch, and both failure
paths must release the lock. Rewording is fine; re-opening the gap
isn't.

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Addressed Codex's latest P2 (commit 96137e3b, inline at card_cleanup.html:1092) in 012aec2c.

The gap: cardCleanupConfirmDelete was issuing POST /api/card-cleanup/delete before cardCleanupSetBusy(true). The confirm dialog is dismissable while the request is in flight (backdrop click or Escape), and in that window Scan and Verify stayed live — a user could kick off a second job whose watch would overwrite the delete's jobId, orphaning the destructive path.

Fix: move cardCleanupSetBusy(true) to run before the fetch, mirroring the pattern already used by cardCleanupStartScan and cardCleanupStartAudit. Both failure paths (!resp.ok and the network-error catch) now call cardCleanupSetBusy(false) so a definitive failure hands the buttons back instead of leaving a dead page.

test_confirm_delete_locks_page_before_post pins the coupling: cardCleanupSetBusy(true must appear before the /api/card-cleanup/delete fetch inside the handler, and both failure paths must release the lock. Rewording is fine; re-opening the gap isn't.

Tests: test_card_cleanup_api.py — 22 passed, 1 skipped. Full CLAUDE.md suite — 2075 passed, 16 skipped, 1 failed. The one failure (test_failed_import_does_not_chain) is the same pre-existing env issue noted on prior rounds (chmod 0 doesn't restrict root in this container), unrelated to this change.


Generated by Claude Code

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Re-read every outstanding review and comment per your /claude-fix. All five prior threads have landing commits, and I verified each fix is present on the branch head (96137e3b):

  • Codex — Verify every workspace represented by the previewdb739e28 (verify-hashes callout states its current-workspace scope; post-audit hint names other-workspace folders). Already replied on-thread previously.
  • Codex — Distinguish failed hash checks from unverified copiesae456b88 (KEEP_ARCHIVE_HASH_FAILED reason + distinct callout that points at Audit rather than another verify pass). On-thread reply posted just now.
  • Codex — Direct failed hashes to the owning workspace1213fec5 (card-cleanup-hash-failed-scope span names the Audit page's current-workspace scope). On-thread reply posted just now.
  • Codex — Keep deletion disabled until the post-audit rescan97ba478b (cardCleanupFinishAudit disables Delete and sets a re-scan hint on every terminal outcome, until the next successful scan re-enables it from fresh totals). On-thread reply posted just now.
  • CodeRabbit — SSE polling orphaning a running job96137e3b (polls to terminal state indefinitely, 1s → 10s cadence, only exits on terminal status or 3 consecutive 404s). Already confirmed as addressed by CodeRabbit; thread resolved.

Nothing was missed and no new code changes were needed on this pass — the fixes were all in place, but the middle three Codex threads had no on-thread response yet, so the reviewer would have had to scroll the PR conversation to see where each landed. That's fixed now.

Tests re-run to confirm state: CLAUDE.md suite from docs/README — 2075 passed, 16 skipped, 1 pre-existing environmental failure (test_failed_import_does_not_chainchmod 0 doesn't restrict root in this container; also fails on main). Card-cleanup suites specifically: 83 passed / 1 skipped.


Generated by Claude Code

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Duplicate /claude-fix fire — the routine got re-triggered by the on-thread review-replies I just pushed (11:42:55–11:43:07 UTC), and my previous comment 9 seconds ago (5231336813) already re-audited every thread and confirmed each landing commit is present on branch head 012aec2c. Nothing new to push.

Re-verified on this pass anyway, since the fire is explicit:

  • card_cleanup.html:794–804 — verify-hashes callout states current-workspace scope; post-run hint names other-workspace folders (db739e28).
  • card_cleanup.py:249–379qualify_rows() returns KEEP_ARCHIVE_HASH_FAILED when the only non-ok rows have already been checked; card_cleanup.html:841–848 renders the distinct callout that points at Audit rather than a second verify pass (ae456b88).
  • card_cleanup.html:846–848 — hash-failed callout carries card-cleanup-hash-failed-scope naming the Audit page's current-workspace scope (1213fec5).
  • card_cleanup.html:909–919cardCleanupFinishAudit disables Delete and sets the re-scan hint on every terminal outcome (97ba478b).
  • card_cleanup.html:620–641 — SSE-drop fallback polls to a terminal state (1s → 10s cadence) and only bails on 3 consecutive 404s (96137e3b); CodeRabbit already confirmed this thread as addressed.
  • card_cleanup.html:1069–1097cardCleanupSetBusy(true) runs before the delete POST; both failure paths clear it (012aec2c).

Skipping a suite rerun since the branch head hasn't moved since the previous run's 2075 passed / 16 skipped / 1 pre-existing env failure result. If a specific thread was meant and I missed it, drop the thread link and I'll take another look.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 012aec2c51

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment thread vireo/templates/card_cleanup.html Outdated
Comment on lines +1104 to +1106
} catch (e) {
cardCleanupCloseConfirm();
cardCleanupSetBusy(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the page locked after an ambiguous delete failure

When the server accepts this POST and queues the deletion but the connection drops before its response arrives, fetch rejects here even though the destructive job is still running. This unconditional unlock leaves jobId unset and re-enables Scan and Verify, allowing another job to overwrite the page state while the deletion proceeds unseen. The newly added catch path is fresh evidence beyond the previously reported pending-POST race; only unlock after an HTTP response proves no job was queued, and treat network errors as an unknown-running state that directs the user to Jobs.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 5afcb0b.

The catch block on the delete POST no longer calls cardCleanupSetBusy(false). fetch rejects for any network-layer failure, including the case where the server accepted the POST and queued the destructive job before the connection dropped — unlocking there was the exact orphan the single-busy-state owner exists to prevent, and it's especially wrong for the destructive path because a second job's watch would then overwrite jobId while the deletion ran unseen.

New behavior:

  • !resp.ok branch (unchanged): the server proved nothing was queued, so it still hands the buttons back.
  • catch (e) branch: leaves Scan / Verify / Delete disabled and shows an unknown-outcome error telling the user to check the Jobs page for a delete queued around now, then reload this page to work with the card again. A page reload is the honest reset — it rebuilds the busy state from a fresh manifest once the user has confirmed what actually happened on the server.

test_confirm_delete_locks_page_before_post was updated to pin both invariants explicitly instead of a global >= 2 count:

  • The !resp.ok sub-branch must call cardCleanupSetBusy(false).
  • The catch (e) sub-branch must not call cardCleanupSetBusy(false), and its body must mention the Jobs page.

The >= 2 shape would have kept passing if a future refactor slid the release back into the catch, so the split-per-branch assertion is what actually pins the Codex P2 guarantee.

Card-cleanup API suite: 22 passed / 1 skipped. CLAUDE.md test suite: 2075 passed / 16 skipped / 1 pre-existing env failure (test_failed_import_does_not_chainchmod 0 doesn't restrict root in this container; also fails on main).


Generated by Claude Code

…Codex P2)

Codex P2 review on commit 012aec2 (card-cleanup delete flow): the newly
added catch block on the POST /api/card-cleanup/delete request called
cardCleanupSetBusy(false) unconditionally. `fetch` rejects for any network
failure, including the case where the server accepted the POST and queued
the destructive delete before the connection dropped. Unlocking there
re-enables Scan and Verify while the deletion runs unseen server-side, so
a second job's watch can overwrite this page's jobId — the exact orphan
the single-busy-state owner exists to prevent.

Change: the catch no longer unlocks. It leaves Scan / Verify / Delete
disabled and shows an unknown-outcome error that tells the user to check
the Jobs page for a delete queued around now, then reload this page to
work with the card again. A page reload is the honest reset: it rebuilds
the busy state from a fresh manifest once the user has confirmed what
actually happened on the server.

The !resp.ok branch is unchanged — an HTTP response proves nothing was
queued, so it can still hand the buttons back.

test_confirm_delete_locks_page_before_post now pins both invariants:
- !resp.ok branch: cardCleanupSetBusy(false) must appear (unlock).
- catch branch: cardCleanupSetBusy(false) must NOT appear, and the body
  must mention the Jobs page.

Full card_cleanup_api suite: 22 passed / 1 skipped. CLAUDE.md test suite:
2075 passed / 16 skipped / 1 pre-existing env failure
(test_failed_import_does_not_chain — chmod 0 doesn't restrict root in
this container; also fails on main).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01EES6embmjg1M1aY3zATMaM

jss367 commented Aug 9, 2026

Copy link
Copy Markdown
Owner Author

Routine fired on jss367's empty review at 11:48:56 UTC (commit 012aec2c). The actionable item was Codex's fresh P2 review of that same commit (11:47:09 UTC, card_cleanup.html:1106): the network-error catch in cardCleanupConfirmDelete unconditionally released the page-wide busy lock, even though a dropped connection is ambiguous — the server may already have accepted the POST and queued the destructive job — so a second job could hijack the delete's jobId.

A parallel routine got there first: 5afcb0ba keeps the catch from unlocking (only the !resp.ok branch does, since an HTTP response proves nothing was queued), and directs the user to the Jobs page + a reload. Tests updated to pin both invariants; test_confirm_delete_locks_page_before_post now requires the !resp.ok branch to unlock and requires the catch to NOT unlock and to mention the Jobs page.

I independently produced the same fix locally before fetch surfaced the parallel commit, verified 5afcb0ba on the branch head against my local run — test_card_cleanup_api.py: 22 passed / 1 skipped, and the full CLAUDE.md suite: 2075 passed / 16 skipped / 1 pre-existing env failure (test_failed_import_does_not_chain — the same container chmod 0 issue seen on every recent round; also fails on main). Discarded my duplicate commit; nothing pushed from this run.


Generated by Claude Code

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 5afcb0ba52

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

cardCleanupWatchJob(data.job_id, 'audit', 'Starting hash verification…');
} catch (e) {
// Nothing started, so nothing is running: give the buttons back.
cardCleanupSetBusy(false);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Keep the page locked after an ambiguous audit start

When POST /api/jobs/verify-hashes reaches the server but its response is lost or cannot be parsed, this catch restores the previously enabled Delete button even though api_job_verify_hashes() may already have started verification. The user can then launch deletion concurrently; delete_verified() trusts the currently committed hash_status plus archive size/mtime rather than re-hashing archive bytes, so it can qualify a row on its old ok verdict while the audit is detecting same-metadata corruption and delete the good card copy. Unlock only after a definitive HTTP failure proves no job was queued; treat network/parsing failures as potentially running and direct the user to Jobs, as the delete-start path already does.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 16a3f8e.

cardCleanupStartAudit was catching every failure — network error, !resp.ok, JSON parse — through one arm that called cardCleanupSetBusy(false). That's the same gap the delete-start fix in 5afcb0b closed on the destructive path: only an HTTP response proves the server didn't queue the job. A fetch rejection or an unreadable 2xx body could mean api_job_verify_hashes() is already running, and unlocking there is dangerous because delete_verified trusts qualify_rows(fetch_rows_by_hash(...)) — i.e. the currently committed hash_status plus archive size/mtime — rather than re-hashing archive bytes, so a concurrent delete could qualify a row on its old ok verdict while the audit is flipping it to modified/corrupt/unreadable and remove the good card copy.

Fix (mirrors the confirm-delete pattern):

  • Fetch-rejection catch: keeps Scan/Verify/Delete disabled, tells the user to check the Jobs page for a verify-hashes job that started around now, then reload this page to work with the card again.
  • !resp.ok branch: the server proved nothing was queued — hand the buttons back before showing the error. The error message still comes from the response body (best-effort JSON parse, default text otherwise) so a definitive failure isn't swallowed.
  • JSON-parse catch on a 2xx response: same reasoning as the fetch catch — the server returned OK, so the audit may already be running; keep locked, route to Jobs.

test_start_audit_keeps_page_locked_on_ambiguous_start pins all three branches explicitly (not a global count), so a future refactor can't quietly re-open any single gap: !resp.ok must unlock; both catches must not unlock and must mention the Jobs page.

Tests re-run against the branch: test_card_cleanup_api.py — 23 passed / 1 skipped (unchanged). Focused subset (test_card_cleanup*, test_app, test_config) — 586 passed / 3 skipped, no regressions.


Generated by Claude Code

Codex flagged that `cardCleanupStartAudit` released the page-wide busy
lock in its catch on `POST /api/jobs/verify-hashes`, even though a
`fetch` rejection (or an unreadable 2xx body) can't prove the server
didn't already queue `api_job_verify_hashes()`. If the audit is
silently running while Scan/Verify/Delete come back live,
`delete_verified` trusts the currently committed `hash_status` plus
archive size/mtime rather than re-hashing archive bytes — so a
concurrent delete could qualify a row on its old `ok` verdict while
the audit is flipping it to modified/corrupt/unreadable, and remove
the good card copy of a file whose archive copy is silently rotting.

Mirror the delete-start pattern from commit 5afcb0b:
- Split the try/catch so the fetch failure is caught separately, keeps
  the page locked, and directs the user to the Jobs page with a
  reload-to-recover message.
- Handle `!resp.ok` inline: the server proved nothing was queued, so
  hand the buttons back before showing the error.
- Handle the JSON parse of a 2xx response with its own catch: the
  server returned OK, so the audit may already be running — keep
  locked, route to Jobs (defence-in-depth for an unlikely path).

`test_start_audit_keeps_page_locked_on_ambiguous_start` pins the
coupling: the !resp.ok branch must unlock, the fetch-rejection catch
and the JSON-parse catch must not unlock and must mention the Jobs
page. A refactor that quietly re-opens any of the three gaps fails.

Tests: vireo/tests/test_card_cleanup_api.py — 23 passed, 1 skipped.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01K1RkzznNE4qYa9wfFwepBo

@chatgpt-codex-connector chatgpt-codex-connector Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Codex Review

Here are some automated review suggestions for this pull request.

Reviewed commit: 16a3f8e4d8

ℹ️ About Codex in GitHub

Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you

  • Open a pull request for review
  • Mark a draft as ready
  • Comment "@codex review".

If Codex has suggestions, it will comment; otherwise it will react with 👍.

Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".

Comment on lines +648 to +653
cardCleanupSetBusy(false);
const status = job && job.status;
const jobError = job && (job.errors || [])[0];
if (kind === 'scan') {
if (status === 'completed') {
await cardCleanupLoadManifest();

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Keep the scan locked until its manifest is rendered

When a completed scan is still awaiting cardCleanupLoadManifest(), this unlocks Scan and clears jobId first. If the user changes the source and starts another scan during that request, scanJobId points to the second scan, but the first request can subsequently render its old manifest and re-enable Delete even while the second scan runs; if the second manifest becomes available before the click, the confirmation displays the first scan's source and totals while the delete POST operates on the second scan. Keep the first scan's ownership/busy state through manifest loading, or discard manifest responses whose job ID is no longer current.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Addressed in 859214c.

The race: cardCleanupFinishJob unlocks the buttons and clears jobId before awaiting cardCleanupLoadManifest() for scan #1. During that await, a user who changes the source and clicks Scan again sets cardCleanupState.scanJobId = jobId2 (and re-locks the page via cardCleanupSetBusy(true)). When scan #1's response then arrives, cardCleanupRenderManifest renders scan #1's totals and source over scan #2's state and, worse, re-enables the Delete button based on scan #1's deletable.count — the confirmation dialog would then advertise scan #1's numbers while the delete POST (which uses cardCleanupState.scanJobId, now jobId2) would operate on a different set the user never confirmed. Two secondary paths have the same problem: the 404 handler nulls scanJobId (clobbering scan #2's identity), and the error banner would misattribute scan #1's failure to scan #2.

Fix (took the discard-stale-response lever rather than moving cardCleanupSetBusy(false) inside the manifest branch, so the timing of button re-enable stays as it is — the target invariant is "no stale render/mutation/error," not "buttons stay locked longer"): capture scanJobId into a local at the top of cardCleanupLoadManifest, then guard both the success and error paths with if (cardCleanupState.scanJobId !== requestedScanJobId) return;. On the success path the guard runs immediately after resp.json() — before the 404 branch, the !resp.ok throw, and cardCleanupRenderManifest — so a stale response touches nothing. On the catch path the guard drops fetch and JSON-parse errors that belong to scan #1 once scan #2 has taken over. card_cleanup.html:688–716.

test_load_manifest_discards_stale_scan_response pins both branches explicitly rather than a count: the success guard must appear before min(idx(scanJobId = null), idx(cardCleanupRenderManifest)), and the catch body must also contain a state.scanJobId !== comparison — so a refactor can't quietly re-open either.

Card-cleanup API suite: 24 passed / 1 skipped (existing skip).


Generated by Claude Code

Codex P2 review on commit 16a3f8e flagged a race in
cardCleanupLoadManifest: cardCleanupFinishJob unlocks the buttons and
clears jobId BEFORE awaiting the manifest fetch, so a user who changes
the source and starts scan #2 during that await sets scanJobId to
jobId2. When scan #1's response then arrives, cardCleanupRenderManifest
renders scan #1's totals and source over scan #2's state — and, worse,
re-enables the Delete button based on scan #1's deletable count. The
confirmation dialog would advertise scan #1's numbers while the delete
POST (which uses cardCleanupState.scanJobId, now jobId2) would operate
on a different set the user never agreed to.

Fix: capture scanJobId at request time and drop the response on both
the success and error paths when the state has moved on by the time
the response arrives. This also protects the 404-clears-scanJobId
path from clobbering scan #2's identity, and prevents scan #1's
fetch/parse error from surfacing as if scan #2 had failed.

test_load_manifest_discards_stale_scan_response pins the coupling for
both branches (success-path guard must run before ANY state mutation
or render; catch guard must be present) so a refactor can't quietly
re-open either.

Tests: test_card_cleanup_api.py — 24 passed / 1 skipped (existing
skip). Full CLAUDE.md suite green apart from the pre-existing
environmental failure (test_failed_import_does_not_chain — chmod 0
doesn't restrict root in this container; also fails on main).

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
vireo/tests/test_card_cleanup_api.py (1)

585-589: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Consider a marker helper so template refactors fail with a readable message.

Line 588 bounds the !resp.ok arm at "let data;". Line 601 shows resp is assigned rather than declared at the fetch, so let resp; is hoisted above it. If a later edit hoists let data; the same way, section.index("let data;", ok_branch_start) raises ValueError instead of failing the assertion, and the report gives no hint about which marker moved.

A shared helper keeps the same invariants and names the missing marker.

♻️ Proposed helper for marker lookup
def _marker(section, marker, start=0, *, what):
    idx = section.find(marker, start)
    assert idx != -1, f"expected {marker!r} in {what}; the template changed"
    return idx
-    ok_branch_start = section.index("if (!resp.ok)")
+    ok_branch_start = _marker(section, "if (!resp.ok)",
+                              what="cardCleanupStartAudit")
     # The branch closes before the JSON re-parse below; scope to just that
     # arm by ending at the trailing "return;" and its closing brace.
-    ok_branch_end = section.index("let data;", ok_branch_start)
+    ok_branch_end = _marker(section, "let data;", ok_branch_start,
+                            what="cardCleanupStartAudit")
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@vireo/tests/test_card_cleanup_api.py` around lines 585 - 589, In the test
marker-scanning logic around the !resp.ok branch, add and reuse a shared _marker
helper that uses find, asserts a missing marker with a descriptive
template-change message, and returns its index. Replace direct section.index
calls such as the "let data;" boundary lookup (and other nearby marker lookups)
with this helper while preserving the existing search offsets and branch-scoping
invariants.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Nitpick comments:
In `@vireo/tests/test_card_cleanup_api.py`:
- Around line 585-589: In the test marker-scanning logic around the !resp.ok
branch, add and reuse a shared _marker helper that uses find, asserts a missing
marker with a descriptive template-change message, and returns its index.
Replace direct section.index calls such as the "let data;" boundary lookup (and
other nearby marker lookups) with this helper while preserving the existing
search offsets and branch-scoping invariants.

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2677897b-a6f6-47d1-a632-360ab21be8d0

📥 Commits

Reviewing files that changed from the base of the PR and between 97ba478 and 859214c.

📒 Files selected for processing (2)
  • vireo/templates/card_cleanup.html
  • vireo/tests/test_card_cleanup_api.py

@github-actions
github-actions Bot merged commit c21d55f into main Aug 9, 2026
31 checks passed
@github-actions
github-actions Bot deleted the card-cleanup-page branch August 9, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

claude-agent PR managed by Claude PR Agent

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants