- Build Next.js as a standalone server and copy only that into the
runtime image, instead of the whole node_modules tree
- Mark three dynamic file reads with turbopackIgnore so the standalone
trace does not pull in the entire project
- Deploy workflow prunes unused images and build cache older than a day
at the start and end of every run, including failed ones, and logs
disk usage before building
Failed builds were leaving their layers behind until the server's disk
filled, which broke the deploy, the database and Gitea sign-in.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Watch progress and unlocking:
- Player samples the playhead and reports the ranges actually played,
so skipped sections earn no credit and early plays are not lost
- Server merges ranges into compact segment rows; progress never regresses
- Single 80% unlock rule enforced server-side (lib/watch-progress.ts);
the Unlock button and automatic unlock now agree
- Lock state and progress bar update live from each progress save
- Playlist is derived from the video, so unlocks work for shared
playlists and for links from watch history and liked videos
- First/previous video decided by playlist position, not index numbers
Access control:
- Video API and HLS/MP4 routes require a signed-in viewer who is
enrolled and has the video unlocked; thumbnails require sign-in
- Enrolment sync reads course codes from the allow-list, not the request
- /api/admin/meta is admin-only
- Account status, role and levels are re-checked from the database
during a session, so deactivated students lose access within a minute
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
Video records store url = /uploads/videos/<id>.mp4, but nothing served
that path: Next.js only serves static files from public/, so the
player's MP4 fallback 404'd and videos were unplayable until HLS
transcoding finished. (Thumbnails hit the same problem historically —
see scripts/update-thumbnail-urls.ts.)
Add a route handler at the exact stored URL so no DB migration is
needed. Unlike the HLS route it streams from disk via createReadStream
instead of buffering the file, and implements HTTP Range requests
(206/416, open-ended and suffix ranges) — browsers require ranges for
<video> seeking, and files can be multi-GB.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>