Commit Graph
48 Commits
Author SHA1 Message Date
rmsitzandClaude Sonnet 5 4f25d4201b Add course outline sidebar to the lesson page
Shows every section and lesson in the course - videos and standalone
documents alike - so jumping to a different section no longer means
backing out to the course page first. Current lesson is highlighted,
its section auto-expands and scrolls into view; sticky on desktop,
stacks below the player on narrow viewports. Replaces the old
"Lessons in this section" list, which only showed the current
section.

The tree-rendering markup (shared with the loaded-course dashboard
view) is now a single macro in templates/_course_tree.html instead of
being duplicated, so both views stay in sync going forward.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-26 09:13:09 -04:00
rmsitzandClaude Sonnet 5 72c7f67c3d Remove unreliable duplicate-lesson-file matching
Filename-similarity matching within a folder can't distinguish a real
re-download duplicate from a course that splits one topic across
several numbered files ("...Part 1"/"...Part 2", "-1"/"-2"/"-3") -
both look like near-100% matches by name alone, and the latter is a
common, completely normal pattern. This produced hundreds of false
positives in practice. Removes Duplicate Lesson Files entirely
(detection function, its two API routes, and the File Management UI
section); whole-course-folder matching (Duplicate Courses) is
unaffected and stays the only duplicate scan in the app.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-25 08:25:37 -04:00
rmsitzandClaude Sonnet 5 8220bb542a Widen thumbnail sampling window past platform bumpers
Many course platforms (Pluralsight, Packt, ...) open with a generic
branded bumper animation running 10-20s before the actual course
title card. The 8s candidate window landed squarely inside that
bumper and grabbed the platform logo instead of anything
course-specific. Candidates now spread across the first ~25s.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 20:33:37 -04:00
rmsitzandClaude Sonnet 5 de9c05d0d0 Fix race condition in thumbnail candidate generation
Concurrent requests for the same course's thumbnail (e.g. the
Precompute background scan overlapping an ordinary page load) shared
fixed candidate filenames, so one request's cleanup could delete a
temp file the other was still using, crashing with ENOENT. Candidate
filenames are now unique per call.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 20:27:08 -04:00
rmsitzandClaude Sonnet 5 36d9e7f89b Add autoplay, progress rings, duplicate-lesson detection, title-card thumbnails
- Auto-play next lesson on end, with a cancelable countdown and a
  Settings toggle (default on).
- Grid-view course cards show a small progress ring (checkmark at
  100%) instead of a separate bar.
- Duplicate Lesson Files: scans within each course/folder for media
  files that look like the same lesson downloaded twice, with per-file
  delete and a shared ignore list with Duplicate Courses.
- Auto-generated cover art now samples a few early candidate frames
  and keeps the largest JPEG, favoring an intro title card over a
  blank fade-in or a plain presenter frame.
- Settings -> "Regenerate Thumbnails" re-runs that logic for every
  course with an auto-generated thumbnail (never touches manual
  covers), so already-cached thumbnails can pick up the improvement.
- Add .gitignore for __pycache__/.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 20:20:36 -04:00
rmsitzandClaude Sonnet 5 fc83876abe Add Favorites, storage drill-down, keyboard shortcuts, and What's New
- Favorites: star toggle on course rows, the course page, and a new
  dashboard card; rebased on rename/move, backed up/restored, and
  caught by stale-reference cleanup (also fixed a pre-existing gap
  where favorites.json wasn't in the backup file list).
- Storage Usage: click a folder row to drill into its contents one
  level at a time, with a Back button.
- Lesson player: added ,/. (speed step), [/] (prev/next lesson), and
  F (fullscreen) keyboard shortcuts.
- Settings: a CHANGELOG.md-backed "What's New" list, since VERSION
  alone only ever shows the current build.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-24 19:23:40 -04:00
rmsitzandClaude Sonnet 5 ecabc828ea Cache the category tree walk (fixes ~1s File Management page load)
_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>
2026-08-24 18:06:57 -04:00
rmsitzandClaude Sonnet 5 ba8d7e2a1b Add search, bulk actions, undo, and storage usage to File Management
- 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>
2026-08-24 14:41:26 -04:00
rmsitzandClaude Sonnet 5 71d6158c60 Fix build version showing "unknown" in production
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>
2026-08-24 14:00:42 -04:00
rmsitzandClaude Sonnet 5 8d1ce72372 Fix wrapped-course false positive in picker; add build version display
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>
2026-08-24 13:52:32 -04:00
rmsitzandClaude Sonnet 5 7fbe63e05c Exclude media-free subtrees from the destination picker too
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>
2026-08-24 13:21:39 -04:00
rmsitzandClaude Sonnet 5 62bb5e1318 Exclude course/item leaves from destination picker; ease new-folder UX
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>
2026-08-24 13:13:09 -04:00
rmsitzandClaude Sonnet 5 bcd1734c94 Add wildcard and regex modes to Bulk Rename
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>
2026-08-24 11:45:52 -04:00
rmsitzandClaude Sonnet 5 49b68b8e51 Add delete and ignore actions to Duplicate Courses
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>
2026-08-24 11:11:41 -04:00
rmsitzandClaude Sonnet 5 9f0f49a399 Move File Management link into the bottom tab bar
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>
2026-08-24 10:37:56 -04:00
rmsitzandClaude Sonnet 5 9ff8c70311 Tighten Sort Unsorted matching; consolidate file management tools
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>
2026-08-24 10:29:55 -04:00
rmsitzandClaude Sonnet 5 4eda493e00 Add Unsorted folder auto-sort with keyword matching
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>
2026-08-24 09:10:19 -04:00
rmsitzandClaude Sonnet 5 52371dd02f Convert all Figma theme backgrounds to dark/colored, dedupe again
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>
2026-08-24 08:33:34 -04:00
rmsitzandClaude Sonnet 5 722b381913 Add 42 curated themes from Figma's website color schemes
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>
2026-08-24 08:08:56 -04:00
rmsitzandClaude Sonnet 5 acabb89981 Add auto-thumbnails, library-wide remaining time, backup restore
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>
2026-08-23 22:50:48 -04:00
rmsitzandClaude Sonnet 5 24a07dd3e6 Add duration prewarm button; show video lengths in the loaded-course view
Settings gets a "Precompute Video Lengths" button that walks the whole
library in a background thread, populating every course's persistent
ffprobe duration cache up front instead of paying that cost the first
time each course card is viewed. Progress polls live while it runs and
picks back up correctly if you navigate away mid-scan.

Separately, the loaded-course lesson tree only ever showed a lesson's
duration once you'd actually played it (from progress.json) - a freshly
opened course had no time info anywhere, including the course header's
"~X remaining" line, which existed but was always empty as a result.
apply_progress_to_tree now falls back to the same ffprobe cache for any
lesson without a known duration yet, so per-lesson lengths and the
remaining-time estimate work from the very first visit. Refactored the
duration-cache read/write into a shared helper so the library browser,
the prewarm button, and this tree view all go through one path.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:20:10 -04:00
rmsitzandClaude Sonnet 5 8f7808ab46 Show course runtime on library cards via ffprobe
Add ffmpeg to the Docker image so ffprobe can read each video/audio
file's duration server-side. Results are cached to a small persistent
per-course JSON file keyed by (relative path, size, mtime), so a file
only gets probed once - a container restart or the library scan's
in-memory cache expiring never re-runs ffprobe on unchanged files.

Course cards in the library browser (grid, list, and search) now show
total runtime alongside the media-file count, so you know the time
commitment before opening a course. Durations under a minute are
suppressed rather than showing a noisy "0m".

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 22:03:04 -04:00
rmsitzandClaude Sonnet 5 e2db5ecef8 Add per-course progress badges to library cards, replace alert() with toasts
Compute a cheap, uncached completion percentage per course (completed
lessons / media files) and surface it as a badge + thin progress bar on
library cards in both list and grid view, and in search results - untouched
courses stay unbadged so the list doesn't get noisy with "0%".

Replace the native alert() dialogs used for bulk-select validation and
course-load errors with a small slide-in toast component that matches the
rest of the UI instead of a jarring browser popup.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 21:22:00 -04:00
rmsitzandClaude Sonnet 5 3039d1206d Polish dashboard: consistent icon set, flatter cards, merged activity card
Replace card-header emoji with a matching stroke-style SVG icon set that
inherits theme color via currentColor. Drop the accent-stripe border from
every card and keep it only on the Your Courses card, the one primary
action. Merge Continue Watching and Recently Viewed into a single
true-recency list so an old completed item can no longer outrank a newer
in-progress one across two separately-capped lists.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 21:11:36 -04:00
rmsitzandClaude Sonnet 5 21af7ae349 Add colored grid-card fallbacks, bottom tab bar, dashboard reorder
Three visual/structural changes:

- Grid cards without a thumbnail (nearly all real courses in this
  library) showed the same generic graduation-cap icon, making the
  poster grid hard to visually scan. Replace it with a deterministic
  color+initial per course name (same idea as Slack/Google Photos
  default avatars), so cards without real cover art still look
  distinct from each other.

- Notes/Help/Settings only existed as small footer text links,
  meaning a scroll to the bottom of every page to navigate. Add a
  persistent bottom tab bar (Home/Notes/Help/Settings, current page
  highlighted via a new active_tab context var) to the dashboard,
  Notes Hub, Settings, and Help pages. Deliberately left the lesson
  page alone - it already has its own fixed-bottom notes panel, and a
  second fixed bar there would just eat into an already content-dense
  page.

- The no-course dashboard stacked five informational/queue cards
  (Library Stats, Next Up, Pick Back Up, Recently Added, Continue
  Watching, Recently Viewed) before "Your Courses," so the actual
  library browser was the last thing on the page. Move it up to right
  after the Surprise Me banner - it was already unconditional, so this
  only changes where it renders, not when.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 15:30:44 -04:00
rmsitzandClaude Sonnet 5 9e7dc85558 Add Resume button, Surprise Me random pick, and bulk select
Three quality-of-life additions to the Library browser and course
pages:

- A course page shows a "Resume: <lesson>" button when there's a
  last-visited lesson, using Course.last_accessed_path (already
  populated by apply_progress_to_tree, just not surfaced before) -
  no more scrolling the tree to find where you left off.

- "Surprise Me" picks a random not-yet-fully-watched course and loads
  it, for when there's too much library to decide what to watch.
  Reuses the already-cached get_all_course_dirs() and the same
  per-course completed/total shape _scan_library_activity() already
  computes for Stale Courses - no new scanning.

- A select-mode toggle in the Library browser adds checkboxes to every
  row/card (list and grid) with a bulk-action bar to hide or queue
  several courses/folders at once, via two new endpoints
  (/api/hidden-paths/bulk, /api/next-up/bulk) that reuse the existing
  single-item set_path_hidden()/set_path_queued() in a loop. Selection
  is scoped to the current folder view and clears on navigation.

Also fixes runTranscriptSearch() going silently blank on zero results
instead of showing a "no matches" message, and updates the Help page
to cover all of this.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 15:06:29 -04:00
rmsitzandClaude Sonnet 5 2941910c54 Cache library filesystem scans to fix ~90s NAS page loads
There was no caching anywhere in the app, so every request re-walked
the SMB-mounted library from scratch - the dashboard alone triggered
2 duplicate full-library directory walks, 100+ individual
.offlineu_progress.json opens, and ~10-15 full per-course recursive
rglob() scans, none of it shared between requests. Measured ~90s per
dashboard load against the real mounted NAS, with a second immediate
reload taking just as long - proof nothing was being reused.

Add a minimal in-process TTL cache (5 min) and apply it at the actual
hot spots: the shared course-directory listing (get_all_course_dirs,
replacing 6 independent iter_all_courses() walks), the per-course
media-count/thumbnail summary (_course_summary, also now reused by
list_library_directory instead of a third duplicate implementation),
the per-course tree scan (get_course_tree - safe to cache since
progress is always re-applied fresh on top, never baked into the
cached structure), and the transcript-search subtitle index (the
worst offender - previously re-read and lowercased every subtitle
file in the library on every search).

Invalidates immediately on hide/show, rename, and library-path
changes; a "Refresh Library" button on Settings covers files added
directly on the NAS outside the app.

Measured after the fix, same real NAS mount: dashboard ~90s -> 1.3s
warm, Notes Hub ~1.15s warm, Library browser ~3.6s warm.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 14:27:24 -04:00
rmsitzandClaude Sonnet 5 bc6725ee4c Add section lesson list, section progress, and library grid view
Three comparison-driven upgrades against typical course/media platforms:

- Lesson page gets a collapsible "Lessons in this section" list so you
  can jump between siblings without leaving the page, instead of only
  global Prev/Next buttons. find_lesson_in_tree() now also returns the
  DirectoryNode that owns the lesson (its .lessons list is the sibling
  set) since DirectoryNode has no parent pointer; a new
  get_section_lessons() resolves each sibling's progress the same way
  view_lesson() already does for the current lesson.

- Course tree section headers show "X/Y watched" instead of a flat item
  count, via DynamicCourseParser._calculate_completion_stats (already
  computed recursive completion for any node, just never called
  per-section) exposed as a Jinja global.

- Library browser gets a list/grid toggle for browsing large category
  folders by poster-style thumbnail instead of a 32px-icon list.
  Client-side only, persisted via localStorage, defaults to the
  existing list view; both folder browsing and search results already
  funnel through the same render function so grid mode covers both.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-23 14:04:14 -04:00
rmsitzandClaude Sonnet 5 3fedf849d3 Stop resetting lesson progress on view, and add Notes Hub search
view_lesson() called update_lesson_progress() with no arguments on
every page load, which defaults completed=False and progress_seconds=0
and silently overwrote whatever was already saved - just opening an
already-watched lesson reset its progress/completed state unless the
client happened to report real values within the next 15 seconds.
Replaced with touch_lesson_accessed(), which only bumps last_accessed.

Also fixes the "Resume from Xs" feature this fed into: it read a value
that was never populated on this route, and even when given a real
one, checked activeMedia.duration synchronously before metadata had
loaded, so it silently never applied. Now waits for loadedmetadata (or
resolves immediately if already available) and defers to an explicit
note-timestamp jump (?t=) when both are present.

Notes Hub gets a live search box filtering by note text, lesson title,
or course name (client-side, no reload), with a "no notes match" state
- useful now that a single category folder can hold dozens of courses'
worth of timestamped notes.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 22:00:35 -04:00
rmsitzandClaude Sonnet 5 68639a2b5b Fix chapter-name regex missing underscore-joined folder names
_SECTION_NAME_RE used \b after keywords like "chapter", but \b doesn't
fire between "Chapter" and an immediately-following "_" since
underscore counts as a word character too - so a course whose chapter
folders were named "Chapter_1-Introduction" (no space) failed to match
and got misclassified as a plain folder instead of a course.

Swap \b for a (?![a-z]) negative lookahead, which correctly rejects
only a following letter (e.g. "Chapterhouse") while accepting a digit,
underscore, hyphen, space, or end of string.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 20:41:11 -04:00
rmsitzandClaude Sonnet 5 21a468fb7f Fix course-detection heuristic swallowing category folders as one course
_looks_like_course() treated any folder with 2+ media-holding subfolders
as a single multi-section course, with no check that those subfolders
actually looked like sections. A category folder holding many unrelated
courses (e.g. Claude/, General/) could accidentally satisfy that if
several of its courses happened to keep episodes directly at their own
top level rather than nested under a chapter folder, collapsing the
whole category into one bogus course.

Now every media-holding subfolder must read as a chapter/section label
(Section 1, Module 2, or a numbered chapter like 01-Introduction) for
the parent to count as one course - verified against the real library
mount that ChatGPT/Claude/Copilot/General all correctly show as
folders again, while individually-structured courses still detect
correctly.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 20:33:02 -04:00
rmsitzandClaude Sonnet 5 8b365c4463 Add timestamped notes: multi-note support, quick-capture hotkey, live saved indicator
Replace the single free-text note per lesson with a list of timestamped
note entries, migrated transparently from the old format. Adds a
floating notes panel on the lesson page with an always-reachable
quick-capture bar (press N to pause and jump in), inline edit/delete,
click-to-seek timestamps, and a live "Saved Xs ago" indicator. Notes
Hub, the study guide export, and the Outline push all updated to
render multiple timestamped notes per lesson.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 20:17:20 -04:00
rmsitzandClaude Sonnet 5 0454834e62 Add Notes Hub, transcript search, stale nudges, study guide export, Next Up queue, activity heatmap
Six more dashboard features, all built on the per-course progress-file
scanning infrastructure from earlier sessions:

- Notes Hub (/notes): every lesson note across the library in one place,
  newest first, reusing the existing /recent/open cross-course jump route.
- Transcript search: opt-in checkbox on the Library search box, searching
  inside .srt/.vtt files. Kept cheap by doing a raw substring match as the
  filter step and only extracting a snippet for files that actually match.
- Stale course nudges ("Pick Back Up"): courses with progress that haven't
  been touched in 14+ days.
- Study guide export: compiles a course's notes into one downloadable
  markdown file, section/lesson structure preserved.
- Next Up queue: an ordered, persisted "what to tackle next" list with
  up/down reordering and a queue button on every course row.
- Activity heatmap: a 90-day contribution-style calendar in the Library
  Stats card.

_scan_library_activity() now does one walk over every course's progress
file per dashboard load; library stats, stale courses, and the heatmap all
derive from that single scan instead of three independent ones.

Also refactored the duplicated lesson-progress-key lookup (used in three
places now) into _resolve_lesson_progress_key(), and flagged a pre-existing
bug found along the way (not fixed here, kept out of scope): subtitle files
never actually attach to a Lesson object, so the video player's caption
track has never had anything to render.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 19:04:01 -04:00
rmsitzandClaude Sonnet 5 145f91a647 Fix note-field space bar hijacking video, rework Outline topics as documents
- Lesson page's global keyboard shortcuts (space/arrows for play-pause/
  seek/volume) fired regardless of focus, so typing a space in the notes
  textarea toggled the video instead of typing. Now skipped entirely
  whenever a text field/select has focus.
- Outline topics no longer spawn their own top-level collection each time.
  A topic is now a document inside one fixed, user-configured collection
  (Settings -> Outline Integration -> Default collection), and a lesson's
  note becomes a child document nested under its topic - matching how
  notes actually get organized in a real Outline instance instead of
  cluttering the collections list with one per topic.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 12:27:00 -04:00
rmsitzandClaude Sonnet 5 5fe3fafc7a Add Outline integration: push lesson notes to a topic collection
Pick a topic on a lesson's note and it pushes to that topic's collection
in a self-hosted Outline instance when you leave the page; notes without a
topic stay local-only. Settings gets a new Outline Integration card
(URL/token, Save, Test Connection).

Credential handling: the API token lives in its own outline_config.json
in DATA_DIR, deliberately kept out of the general settings flow (GET
/api/settings is fetched on every page load by theme.js - no place for a
secret to ride along). GET /api/outline/config only ever returns whether
it's configured, never the token itself.

The topic chooser reflects Outline's live collection list rather than a
locally cached copy, and resolves a newly-typed topic name to a real
collection immediately (find-by-name-or-create) rather than waiting until
the note is pushed. That's not just an optimization: the push itself fires
via navigator.sendBeacon() on pagehide, which can't read a response, so a
page that fires pagehide more than once for the same load (a back/forward-
cache restore, for instance) would otherwise re-send the same "create a
new topic" intent every time and spawn duplicate collections. Verified
live against a local mock Outline server that firing pagehide repeatedly
for the same lesson creates the collection/document once and updates
thereafter.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 12:12:50 -04:00
rmsitzandClaude Sonnet 5 a6c49d3762 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>
2026-08-22 11:36:05 -04:00
rmsitzandClaude Sonnet 5 dbf9528fea Speed up library search on large libraries
search_library_courses was walking via list_library_directory, which
computes a full recursive file count (rglob) and thumbnail lookup for
every course at every level - turning a search into an O(every file in
every course) scan regardless of how few results actually match. Give
search its own lightweight directory-only walk and defer the expensive
per-course lookups until after a name match is confirmed, so cost now
scales with matches found rather than total library size.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 09:22:54 -04:00
rmsitzandClaude Sonnet 5 38963b1d55 Add search, thumbnails, continue watching, playback speed, PWA install, sort, notes, and mark-watched
Eight usability additions on top of the mobile redesign: course cover-image
thumbnails, a debounced library search, a Continue Watching section split
from Recently Viewed, per-lesson playback speed control, an installable
PWA manifest/icons, name sort in the library browser, per-lesson notes, and
a bulk mark-course-as-watched action. Also fixes two real bugs found along
the way: lesson_view.html was missing the viewport meta tag (so lesson
pages weren't actually mobile-responsive), and update_lesson_progress
always overwrote the whole progress entry, which would have silently
deleted a saved note on the next routine autosave.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-08-22 09:06:44 -04:00
rmsitz c92268e04c New mobile layout, new regular screen, added the ability to edit directory names from settings 2026-08-22 08:37:25 -04:00
rmsitz fa00a0b0c4 Fixed themes, empty folder showing and single section course detection 2026-08-21 09:49:44 -04:00
rmsitz 0e9fff65b0 Adding ability to hide / show courses 2026-08-20 20:47:38 -04:00
rmsitz b06b8cf94c Added additional themes 2026-08-20 20:09:58 -04:00
rmsitz 699ca7d6df Added some functionality so we can modify the size of the playing video 2026-08-20 19:50:20 -04:00
rmsitz 5b8bef1451 Added a back to main clicking on the link, added help page, added a last 5 viewed 2026-08-20 18:57:27 -04:00
rmsitz 66dff7e69f New main page way of browsing the courses 2026-08-20 18:48:17 -04:00
rmsitz a604569a45 Adding a new settings page and other tweaks for how it looks 2026-08-20 17:04:21 -04:00
rmsitz fc62d77131 PDF inline fix 1 2026-08-20 16:41:20 -04:00
rmsitz 6f0fc6cb60 Add library browser feature 2026-08-20 13:47:51 -04:00