feat(bulk): scan-file by-filter + library kebab menu with count confirmation [shot:library-scan-file-screenshot] (bookshelf-cs2zl.2) #1239
Loading…
Add table
Add a link
Reference in a new issue
No description provided.
Delete branch "bd-bookshelf-cs2zl.2"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
POST /books/bulk/filter/scan-file): triggersBulkByFilterWorkflowwithOp=scan-fileover all books matching the current view filter, bounded fan-out, ContinueAsNew pagination.BulkFilterOpScanFileconstant toBulkByFilterWorkflow,bulkByFilterScanFilehelper,ProviderIDfield carried through all ContinueAsNew epochs.LLMVisionAvailable), withAppDialog.confirmcount confirmation ("You are about to scan N files for metadata. This uses your LLM budget.") before POSTing.scan-filebulk action entry changed fromfilterUrl: null, filterNA: truetofilterUrl: "/books/bulk/filter/scan-file".ActionBulkScanFile = "BULK_SCAN_FILE"constant added.Test plan
make test— all Go tests passmake coverage— 100% coverage, zero uncovered blocksnpm run test— 4318 JS tests pass (including 7 new SCAN FILES tests + registry test)BulkByFilterWorkflow — scan-file op with ≥3 ContinueAsNew epochsverifies 10 sub-workflows spawned, ProviderID threaded throughBulkScanFileByFilterHandler— 202 success, provider_id threading, 400 on bad status/format, 500 on workflow error, magic shelf checkregisters POST /books/bulk/filter/scan-file+ permission gate entrySCAN FILESdescribe covers confirm dialog, POST body, cancel, success toast+navigate, server error, network error, AppDialog absentCloses bead bookshelf-cs2zl.2 on merge.
The outer `if (!dialog) { return; }` guard at the start of scanFiles() establishes that `dialog` (window.AppDialog) is non-null before the fetch runs. Re-checking `if (window.AppDialog)` inside .then()/.catch() created dead-code branches (always truthy at that point) that the V8 coverage tool flagged as uncovered, dropping branch coverage to 97.11%. Replace the inner window.AppDialog re-checks with direct `dialog.toast(...)` calls using the captured `dialog` variable, which is proven non-null. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>92f44da72914a877d576Security review — PR #1239 (
bd-bookshelf-cs2zl.2)New
POST /books/bulk/filter/scan-file(LLM scan-file over all filter-matching / whole-library books) + library kebab trigger.Multi-user scoping — PASS (fail-closed).
bulkScanFileFilterRequestreuses the sharedbulkFilterHandler:userIDis taken from the session (userIDFromRequest(r)), never the body;library_id/shelf_id/statusare re-scoped to that userID (ShelfUserID/StatusUserID);magic_shelf_idis ownership-checked viacheckMagicShelfAccess(404 on miss). Book-ID resolution re-resolvesuserLibraryIDsper ContinueAsNew epoch inbuildListFilteredIDsPageFnand passes them toListFilteredBookIDsPage, whose predicate is fail-closed (internal/books/filter_predicates.go:104→library_id IN (...); non-nil empty →1=0).users.GetUserLibraryIDsnormalizesnil → []int64{}, so a zero-library user takes the1=0branch — a body-suppliedlibrary_idthe user can't access yields no rows, not a cross-user leak.Auth — PASS. Route is gated
g.BulkScanFile(...)→BookBulkScanFileRequired→users.PermissionRequired(..., PermissionBulkAutoFetchMetadata)(internal/app/app.go:355), same real permission as the sibling by-IDs endpoint. Not "any logged-in user."Resource-exhaustion / cost DoS — PASS. Fan-out is bounded single-digit (
defaultFanOutConcurrency = 4,internal/wfengine/fanout.go:19) and sub-workflows route to the LLM queue (scanFileFanOutOptions) so vision activities respect the GPU/concurrency cap; the kebab entry is gated behind{{if $.LLMVisionAvailable}}and adialog.confirmcount prompt.Injection — PASS. All SQL is sqlc/parameterized;
view_queryvalidated viaParseViewQueryFilterat the boundary; status/format/metadata filters allowlist-validated; audit action is a constant.Workflow versioning — SAFE (no gate needed). The new
case BulkFilterOpScanFileinbulkByFilterApplyOpis selected by the per-instance-immutableinput.Op; in-flight instances carry a differentOpand keep their original command sequence, so replay does not diverge.Findings
[MINOR] templates/layouts/base.html:200 — count-confirmation shows 0 for the largest libraries
data-...-book-count-value="{{if .HasCount}}{{.Count}}{{else}}0{{end}}"falls back to 0 when the count is unavailable (HasCountfalse) — which per the Scale convention is exactly the large/unfiltered libraries where the "you are about to scan N files … uses your LLM budget" confirmation matters most. The most expensive case shows the least alarming number, weakening the secondary cost guard. Authorization/scoping/permission gates still fully protect the operation, so this is UX-quality, not a vulnerability. Suggest showing an indeterminate message ("all files in this library") whenHasCountis false rather than "0 files".REVIEW VERDICT: 0 blocker, 0 major, 1 minor
"[BLOCKER] internal/wfengine/bulk_by_filter_workflow.go:325, line 308 — undefined FanOut options variables
bulkByFilterScanFile()callsBoundedFanOutScanFileInput()passingscanFileFanOutOptions(line 325), andbulkByFilterLLMVision()passesllmSweepFanOutOptions(line 308). These variables are referenced but not defined in fanout.go, causing compilation failure. Fix: add variable definitions to fanout.go:var scanFileFanOutOptions = gowf.SubWorkflowOptions{
Queue: QueueDefault,
RetryOptions: gowf.RetryOptions{MaxAttempts: 2},
}
var llmSweepFanOutOptions = gowf.SubWorkflowOptions{
Queue: QueueDefault,
RetryOptions: gowf.RetryOptions{MaxAttempts: 2},
}
Verify correct queue values against project queue constants.
[MAJOR] internal/wfengine/bulk_by_filter_workflow.go — go-workflows workflow command-sequence versioning gate
The PR adds a new
BulkFilterOpScanFileoperation to the switch inbulkByFilterApplyOp()(line 292). This branch emits a newCreateSubWorkflowInstancecommand. Existing in-flight bulk-by-filter instances will continue to replay their original op path (e.g., Op == 'enrich' stays in enrich branch) and will NOT emit the new command — so this change is replay-safe. However, this is a critical go-workflows pattern. Add an explicit comment: 'Command-sequence change verified safe: new BulkFilterOpScanFile branch is opt-in via input.Op; existing instances retain their original op value and replay unaffected.' This clarifies the safety for future reviewers and operators at 3am during an outage.[MINOR] internal/books/routes_test.go:792–800 — test structure
The test 'registers POST /books/bulk/filter/scan-file' is functionally correct with proper 202 response check. No issues.
REVIEW VERDICT: 1 blocker, 1 major, 0 minor"
Security Review — PR #1234
Reviewed the diff (removes an inert
main:has(.book-show){max-width:none}rule + corrects a stale CSS comment instatic/css/main.css).Verification performed:
max-widthis set onmainanywhere else inmain.css, andmax-width:noneis the CSS initial value, so removal changes nothing rendered.max-widthis a pure layout property — notcontain,overflow, visibility,clip, or any containment/isolation control. No security-relevant affordance is dropped..reader-viewercarriesmax-width:700px; the previously-named.reader-columnhas no max-width (flex:1). The corrected reference points at the element that actually constrains width.No findings.
REVIEW VERDICT: 0 blocker, 0 major, 0 minor
Library scan-file count confirmation screenshot (library-scan-file-confirm)
Orchestrator note — the [BLOCKER] above is a false positive.
scanFileFanOutOptionsandllmSweepFanOutOptionsARE defined, in sibling files of the samewfenginepackage (bulk_scan_file_workflow.go:19andbulk_llm_workflow.go:54), andBoundedFanOutScanFileInput/BoundedFanOutLLMSweepInputinfanout.go:329/346. Same-package scope, so the code compiles — confirmed by CI being fully green (Go CI cannot pass with an undefined symbol). The reviewer expected them infanout.goand did not grep the package. No compilation issue; nothing to fix here.The [MAJOR] (add a replay-safety comment before
case BulkFilterOpScanFile) and the security [MINOR] (count-confirmation shows 0 when count is unavailable) are valid and will be folded in.feat(bulk): scan-file by-filter + library kebab menu with count confirmation (bookshelf-cs2zl.2)to feat(bulk): scan-file by-filter + library kebab menu with count confirmation [shot:library-scan-file-screenshot] (bookshelf-cs2zl.2)Library scan-file count confirmation screenshot (library-scan-file-menu-open)
Library scan-file count confirmation screenshot (library-scan-file-confirm)
Updated screenshots (supersede attachment a0a1d0da from comment #15306)
The earlier screenshot caught the modal mid-fade transition. These replacements add a 300ms CSS-settle delay after menu open and after dialog render so both states are fully visible:
1. Kebab menu FULLY OPEN — showing "Scan All Files for Metadata":

2. Count confirmation dialog FULLY rendered:

ac9ee68ad1f4b7750defLibrary scan-file count confirmation screenshot (library-scan-file-menu-open)
Library scan-file count confirmation screenshot (library-scan-file-confirm)
zombor referenced this pull request2026-07-26 19:34:42 +00:00
zombor referenced this pull request2026-07-26 19:34:50 +00:00
zombor referenced this pull request2026-07-26 19:34:56 +00:00