# OfflineU — Project Summary (for Claude Code handoff) ## What this is A fork of the self-hosted course viewer [WhiskeyCoder/OfflineU](https://github.com/WhiskeyCoder/OfflineU) (Flask app), customized for personal use. Deployed via Docker (Dockhand on a Ugreen NAS), built locally from a private Gitea repo rather than pulling the upstream image. ## Deployment - `docker-compose.yml`: `build: .` + `pull_policy: build` (forces a real rebuild every deploy — without `pull_policy: build`, Compose can silently reuse a stale cached image). - `network_mode: host`, `PUID=1000`, `PGID=10`, `TZ=America/New_York`. - Volumes: courses at `/volume1/files/training` → `/app/courses`, progress/settings data at `/volume2/docker/offlineu/data` → `/app/data`. - Deployed from Gitea via Dockhand's "Deploy from Git" stack type. ## Files touched this session - `offlineu_core.py` — all backend logic (Flask routes, settings, progress tracking) - `templates/course_dashboard.html` — main dashboard / library browser - `templates/lesson_view.html` — individual lesson page - `templates/settings.html` — new settings page - `templates/help.html` — new help page - `static/theme.js` — shared script that applies persisted display settings ## Features added, roughly in order 1. **Library browser** — lazy, drill-down directory browsing instead of typing a full path. `/library` lists one directory level at a time; a folder is treated as a "course" once it (or 2+ of its immediate subfolders) contain video/audio files directly. Starts collapsed at the top level. 2. **PDF preview fix** — lesson pages were fetching PDFs as text and dumping raw bytes. Now PDFs (and HTML) render in an iframe; `.docx/.doc/.rtf` show a "no preview, download instead" message rather than garbled text. 3. **Full-width, resizable layout** — removed a hardcoded 800px video cap and 1000px page cap. Video player supports native corner-drag resizing (`resize: both` + `object-fit: contain`), and the last-used size persists via settings. 4. **Settings system** (`/settings`, `/api/settings`) — server-side persisted (`/app/data/settings.json`), covering: - Theme (dark/light + 10 named presets: Dracula, Tokyo Night, Catppuccin Mocha, Ayu Dark, GitHub Dark, Atom One Dark, Houston, Night Owl, Dainty, Matcha) - Accent color (auto-syncs to a theme's signature color on switch, independently editable after) - Font family (system/sans/serif/monospace/Monaspace) and size - Layout width, density, card style, corner radius - Library root override (point the browser at a specific subfolder) - Video player size All applied via CSS custom properties, fetched once per page load by `static/theme.js`. 5. **Header/footer redesign** — branded gradient header, sticky footer holding Settings + Help links (consolidated from several scattered links). Clicking the "OfflineU" wordmark anywhere calls `/reset_course` (clears the loaded course) rather than just `/` (which would keep showing the same course). 6. **Help page** (`/help`) — "How to Use" / "Supported File Types" moved here from the cluttered main dashboard. 7. **Recently Viewed** — tracks the last 5 lessons viewed across *all* courses (`/app/data/recent_views.json`), shown only on the no-course-loaded library screen (not on an individual course's page). Clicking an entry loads that lesson's course if it isn't already active, then jumps to the lesson (`/recent/open`). 8. **Watch-progress tracking** — `Lesson.duration_seconds` added alongside the existing `progress_seconds`. Lesson rows in the course tree and in Recently Viewed show either a green checkmark (completed), an accent-colored "NN% watched" badge + thin progress bar (in progress), or a plain "○" (untouched). 9. **Library curation** (Settings → Curate Library) — hide individual courses or entire folders from the browser without touching anything on disk. Persisted in `/app/data/hidden_paths.json`. Hiding a folder hides everything inside it. `/library` (normal browsing) filters hidden items out; `/library/manage` (used by the Settings UI) includes them flagged `hidden: true` so they can be un-hidden. ## Rough edges fixed this session - **Single-section course detection** — `_looks_like_course()` in `offlineu_core.py` now also recognizes a course with exactly one section subfolder, *if* that subfolder's name reads as a section label ("Section 1", "Module 2", "Chapter 3", etc. — see `_SECTION_NAME_RE`). This deliberately doesn't touch the general "publisher folder holding one course" case (e.g. `Pluralsight/Docker Deep Dive/`), since that subfolder is named after the course, not a section. - **Empty folder after hiding all its courses** — added `_contains_visible_course()`, used by `list_library_directory()` when `skip_hidden=True`, so a folder whose every course has been individually curated out no longer appears as a drillable (but empty) directory in normal browsing. The Settings curation UI (`skip_hidden=False`) is unaffected — it still needs to show those folders so courses can be un-hidden. - **Matcha theme accuracy** — replaced the hand-approximated Matcha palette with real values sourced from lucafalasco/matcha's published VS Code theme JSON. - **Dainty theme swapped for Nord** — turned out "Dainty" (HotWordland/dainty-vscode) is a Lab-space theme *generator*, not a fixed palette — there was no single official hex set to verify against, so approximating it was never really fixable. Replaced it with Nord, which has a well-documented, fully verifiable official palette (nordtheme.com). Anyone with `theme: "dainty"` already saved in `settings.json` falls back gracefully to the `dark` default on next load. ## Mobile UI overhaul + in-app renaming (this session) - **Fixed the mobile text-overlap bug** — `.lesson-item`/`.tree-header` in `course_dashboard.html` had no `min-width: 0` or truncation on their flex children, so long course/lesson names (especially scene-release-style dotted names like `Udemy.crash.course.electronics...Mimir` with zero whitespace to wrap on) would overflow and visually overlap neighboring text on narrow screens. Fixed with `min-width:0` + ellipsis truncation (desktop/tablet) and a `@media (max-width: 600px)` block that stacks title/metadata into separate lines with `overflow-wrap: break-word` (the dotted-name case needed this specifically — plain `white-space: normal` doesn't create a break opportunity in a string with no spaces). - **Library browser redesigned** — replaced the nested-indent accordion (`AI` → `ChatGPT` → course, each level eating horizontal space) with a single-level drill-down + breadcrumb (`Library / AI / ChatGPT`), the standard mobile file-browser pattern. Tapping a folder now replaces the list instead of nesting under it. In-course lesson tree keeps its old expand-in-place behavior (shallower, wasn't the reported problem). - **Rename courses/folders from Settings** — "Curate Library" is now "Manage Library": each row gets a ✏️ button alongside Hide/Show that renames the directory in place on disk via a new `POST /api/rename-path`. Handles path-traversal/collision validation, and rebases any `hidden_paths.json` / `recent_views.json` entries nested under the renamed path (progress files need no rebasing — they live inside the directory and are keyed relative to it). If you rename your *currently loaded* course's folder, the app resets to the library view rather than serving a stale path. ## Known limitations still open - App is unauthenticated by design (matches upstream) — settings and hidden-path curation apply app-wide, not per-browser/per-user. ## Workflow that's been in use Edit locally → `git add . && git commit -m "..." && git push` to the private Gitea repo → redeploy the stack in Dockhand (which builds from the fresh `git pull` + `pull_policy: build`).