Sort Unsorted's destination picker still listed a course whose real
video content sat two levels deep inside its own "display name"
folder (a common release-bundle extraction shape), alongside empty
leftover folders from the extraction that neither of the two earlier
exclusion checks could safely rule out - an empty folder is
indistinguishable from a legitimate freshly-created category. Detect
this case by comparing token similarity between a folder's own name
and its single course child's name: high overlap means they're naming
the same thing (a redundant wrapper), not a deliberate category
holding one course.
Also bake the build's git commit + build time into the Docker image
(git rev-parse at build time, .git deleted again immediately after) so
a push's effect on the running container is actually visible - shown
on the Settings page and in /health - instead of having to guess
whether a webhook rebuild picked up the latest commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The previous leaf-item fix only caught a single stray file with no
subfolders - it missed a course whose bundled extras (downloaded
source code, a Python virtualenv/dependency dump, project assets)
have plenty of subfolders but no video/audio anywhere in them, which
still left the course itself, and every one of those subfolders,
selectable as destinations. Add a subtree-wide media check (bounded
depth) so a folder with subfolders but no media anywhere beneath it is
excluded the same way, while a folder that still has a real course's
media somewhere inside it - or is simply empty, e.g. a freshly-created
category - stays pickable.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The destination picker (Sort Unsorted, Manage Library Move) was
listing individual course/item folders as pickable destinations
whenever they held content course detection doesn't recognize as
video/audio - ebooks, audiobooks in unsupported formats, etc. Add a
leaf-item check scoped to the picker's own category index so these no
longer show up, without touching the shared course-detection heuristic
used elsewhere (Library browsing, search, stats).
Also replace "Create new folder"'s single free-text path field with a
parent-folder picker plus a plain new-folder-name field and a live
"Will create: X/Y" preview, so the resulting path is confirmed before
applying instead of hand-typed blind.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plain-text substring matching couldn't handle a batch of
slightly-different patterns (e.g. several release-group suffixes) in
one pass. Add a match-type selector: wildcard (shell-style */?) for
the common case, and full regex (with backreferences in the
replacement) for anything more precise. Invalid regex is caught and
reported inline instead of failing the request.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Delete permanently removes a course from disk (confirmed with the full
path first) and cleans up any Hidden/Next Up/Recently Viewed
references to it. Ignore marks a specific pair as confirmed-not-
duplicates so it stops resurfacing in scans, without suppressing
either course's other matches; ignored pairs are listed and reversible
under "Ignored matches," and now travel with backup/restore.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the dashboard-only footer link with a "Files" item in the
persistent bottom tab bar (Home/Notes/Help/Settings) on every page, so
it's reachable the same way everywhere instead of only from the
dashboard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Fix Sort Unsorted picking a wrong destination on words that are common
clutter across many categories (a creator's name, generic marketing
filler) by ranking matches on a folder's own deliberate name over
incidental sibling-course words, and downweighting borrowed words that
recur across many categories. Add a folder-picker/create-new/rename UI
to override any proposal.
Turn /unsorted into a general File Management page: move Refresh
Library, Bulk Rename, and Manage Library (hide/rename) here from
Settings, add a Move action to Manage Library using the same picker,
and add Duplicate Courses and Clean Up Stale References tools. Add a
footer link to the page from the dashboard.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Scans an Unsorted folder at the library root and proposes destinations
for new courses by matching keywords against the existing category
tree, suggesting new subfolders when nothing matches closely, and
flagging items for manual review when nothing matches at all. Review
and apply happen on a dedicated /unsorted page linked from Settings;
nothing moves until the user confirms.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The 34 kept themes were split 24 light-mode / 18 dark-mode - the light
ones all shared the same washed-out near-white background (L=0.95),
too bright per feedback. Added a force_mood override to build_theme()
and regenerated all 24 as dark instead, same accent character, colored-
dark background rather than near-white.
That conversion collapsed a lot of previously-distinct light backgrounds
into similar dark neutrals, so re-ran the same color-distance dedup pass
across the full set: 8 more turned out to be near-twins once everything
converged to dark (including a 3-way orange-on-dark cluster trimmed to
one). Net: 53 candidates -> 34 kept, all dark/colored, zero white
backgrounds - down from the 42 (24 light/18 dark) shipped last commit.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sourced from Figma's "53 Unique Website Color Schemes" resource page,
which only shows rendered mockup screenshots (no raw hex data) - extracted
dominant colors via canvas pixel sampling, then a small pipeline
(tools/generate_website_scheme_themes.py) maps each scheme's real colors
into this app's palette shape, with contrast enforced against both the
background and the white text every .btn uses.
Cut 11 of the original 53 after review: 2 for a neon-bright accent (the
saturation cap only bounded lightness at first, so ~28 of 53 accents
landed at pure 100% saturation - fixed at the pipeline level, calibrated
against what the existing hand-picked themes actually use), and 9 for
being near-duplicates of another kept scheme once mapped into actual UI
colors (measured by real color distance, not eyeballed) - including one
pixel-identical pair and a 4-way cluster trimmed to its two most distinct
members.
Settings dropdown is grouped into <optgroup>s (Base, Editor Themes, and
5 categories matching the source page's own curation) rather than one
flat 54-option list.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Auto-generated course thumbnails: find_course_thumbnail() falls back to
grabbing a frame from a course's first video via ffmpeg when there's no
manual cover image, cached to .offlineu_thumbnail.jpg so it only ever
runs once. Folded into the "Precompute" prewarm button, now "Precompute
Lengths & Cover Art".
Library-wide "Remaining" stat: sums every course's already-cached
duration (no ffprobe, just a JSON read) into the dashboard's Library
Stats card, alongside the existing "Watched" figure.
Backup restore: new /api/backup/restore endpoint and a "Choose Backup
File..." control in Settings, with zip-slip and missing-course guards.
Also fixed a gap in the export itself - next_up.json and
outline_config.json weren't being backed up before, so restore wouldn't
have been a true round trip.
Move Surprise Me from a full-width button above the course list to a
dice-icon button in the dashboard header, swapping with the course-name
badge depending on whether a course is loaded.
Update README to cover all of the above.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Replace the original upstream template README (public-project framing,
roadmap items already built, links to a different GitHub repo) with
documentation matching the actual private deployment: real
docker-compose.yml, the Gitea->Dockhand rebuild-on-push behavior, env
vars/CLI flags, data file locations, and the full current feature set
(theming, notes hub, video duration/ffprobe caching, backup export,
Outline integration) that had never been documented.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>