Files
offlineu/OfflineU-project-summary.md
T

6.1 KiB

OfflineU — Project Summary (for Claude Code handoff)

What this is

A fork of the self-hosted course viewer 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 trackingLesson.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.

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).