Add Recently Added, bulk rename, time remaining, library stats, backup export

Five more usability features on top of the search/thumbnails batch:

- Recently Added: dashboard card of courses by folder mtime, separate from
  Recently Viewed (watched vs. just showed up on disk).
- Bulk find/replace rename across every course/folder name at once, with a
  mandatory preview step before anything touches disk. Refactored the
  single-item rename route to share the same validate/apply logic.
- Estimated time remaining on the loaded course's stats card, computed only
  from lessons that have actually reported a duration.
- Library-wide stats overview: total courses, lessons tracked, time
  watched, daily streak - read from each course's small progress file
  rather than re-scanning course contents.
- One-click backup/export zip of settings, hidden-paths, recently-viewed,
  and every course's progress/notes.

Also fixes a real performance bug found along the way: search was routing
through list_library_directory, which computes a full recursive file count
and thumbnail lookup for every course at every level regardless of match -
turning a search into an O(every file in the library) scan. Gave search its
own lightweight directory-only walk (iter_all_courses), now shared by
Recently Added and the stats overview too, so the expensive per-course work
only runs for courses that actually match.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-08-22 11:36:05 -04:00
co-authored by Claude Sonnet 5
parent dbf9528fea
commit a6c49d3762
4 changed files with 611 additions and 66 deletions
+42
View File
@@ -163,6 +163,48 @@ Asked for after brainstorming what could make the app nicer to use — see
avoid re-validating/re-scanning an untrusted path). Button lives in the
course stats card, behind a confirm prompt.
## Search perf fix + five more features (this session)
Also caught: search on the real deployed library was taking 3+ seconds
(scanning ~6000+ files across the real course tree). Root cause:
`search_library_courses` walked via `list_library_directory`, which computes
a full recursive file count (`rglob`) and thumbnail lookup for *every*
course at *every* level, regardless of whether it matched. Rewrote it with
its own directory-only walk (`iter_all_courses` - now shared by search,
Recently Added, and library stats below) so the expensive per-course work
only runs for courses that actually match. Verified with a synthetic
6000-file library: effectively instant for narrow queries, same cost as
before only in the pathological "everything matches" case (unavoidable -
you need real data for every result you return).
1. **Recently Added** — dashboard card showing courses by folder mtime
(`get_recently_added_courses`), separate from Recently Viewed (which
tracks what's been *watched*, not what showed up on disk).
2. **Bulk find/replace rename**`Settings → Manage Library → Bulk Rename`.
Preview (`GET /api/bulk-rename/preview`) before apply
(`POST /api/bulk-rename/apply`) is mandatory; apply takes the *exact*
list the client saw in preview (not a re-derived pattern match), and
continues past individual failures (e.g. a name collision) rather than
aborting the whole batch. Scans every directory in the library, course
and category/group folders alike (`_iter_all_directories`) - by user's
choice, since the naming problem isn't limited to course-level folders.
The single-item rename route (`/api/rename-path`) was refactored to
share the same validation/apply logic (`validate_new_name`,
`perform_rename`) - confirmed byte-for-byte identical error responses
after the refactor.
3. **Estimated time remaining** — added to `_calculate_completion_stats`;
only ever computed over lessons that have actually reported a duration
(i.e. been played at least once) - no data, no estimate shown, rather
than a misleading "0 remaining."
4. **Library stats overview** — total courses, lessons tracked, time
watched, daily streak. Reads each course's small progress JSON directly
rather than re-scanning course contents, so it stays cheap regardless of
how many files live inside each course.
5. **Backup/export**`GET /api/backup`, a `Settings → Backup & Export`
button. Zips settings/hidden-paths/recent-views plus every course's
progress+notes file (stdlib `zipfile`, no new dependency) - not the
course files themselves.
## Known limitations still open
- App is unauthenticated by design (matches upstream) — settings and hidden-path
curation apply app-wide, not per-browser/per-user.