_build_category_index() was the one tree-scanning function never
wrapped in the app's existing 5-minute cache - it ran fresh on every
File Management page load and Sort Unsorted scan (not a manual "this
may take a moment" action like Duplicate Courses or Storage Usage), so
the cost was invisible until it was already slow. Measured ~1.0-1.1s
consistently against the live NAS-mounted library at 161 courses,
confirmed via profiling that a single call makes dozens of iterdir()
round-trips - each one a network hop over SMB. Now shares the same
cache_get_or_compute pattern and invalidate_cache() call sites as
get_all_course_dirs(), so no new invalidation logic was needed.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- Manage Library search: find any course/folder by name anywhere in
the tree instead of expanding levels one by one; results carry the
same Move/Rename/Hide actions.
- Bulk select: every row gets a checkbox, so items found via search or
expanded across different tree levels can be hidden, shown, or moved
to the same destination together in one batch.
- Undo last action: a "Last action: ... [Undo]" bar appears after any
move/rename (Sort Unsorted apply, Bulk Rename apply, a Manage
Library move/rename, a bulk move) and reverses the whole batch.
Deliberately never covers Hide/Show (already a one-click toggle) or
Delete (permanent by design) - only ever move/rename, which are
trivially reversible. Re-checks each item before reversing it, so a
partial failure reports exactly what did and didn't reverse.
- Storage Usage: disk usage per top-level library folder, largest
first, with a simple proportional bar; a manual scan since it reads
every file's size.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous approach baked git rev-parse --short HEAD into the image
at Docker build time, but Dockhand's build context doesn't reliably
have .git available, so it silently fell back to "unknown" in
production even though the build itself succeeded. Replace it with a
plain VERSION file committed to the repo (a timestamp + short
description, updated by hand alongside each commit) that ships via the
same COPY . . as everything else - no git access needed inside the
build at all.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>