Commit Graph

13 Commits

Author SHA1 Message Date
twotalesanimation ec125261e4 Slim the Docker image and clean up after every deploy
Deploy Vault CMS / deploy (push) Successful in 1m15s
- 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>
2026-11-11 20:43:34 +02:00
twotalesanimation 3083a48b5f Re-trigger deploy after freeing disk space on the server
Deploy Vault CMS / deploy (push) Successful in 2m31s
Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 18:04:17 +02:00
twotalesanimation 9a73058eb4 Upgrade Next.js to 16.4.6 and show locks on latest videos
Deploy Vault CMS / deploy (push) Failing after 4m1s
- Upgrade next and eslint-config-next from 16.3.5 to 16.4.6. The older
  version has critical advisories, which made the Docker build's
  `pnpm audit --audit-level critical` step fail and blocked deploys.
- /api/videos/latest now reports whether the viewer can open each video;
  the dashboard shows a lock and the reason instead of opening the player.

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 17:56:19 +02:00
twotalesanimation c98b5c5692 Load watch progress in one request on dashboard and player
Deploy Vault CMS / deploy (push) Failing after 5s
Add GET /api/progress/all, which returns the signed-in user's progress
and merged watch segments for every video (or one playlist) using two
queries.

- Dashboard: replaces two sequential requests per video with a single
  request on load
- Player sidebar: replaces one request per playlist video with a single
  request per playlist

Co-Authored-By: Claude Fable 5.2 <noreply@anthropic.com>
2026-11-11 17:48:35 +02:00
twotalesanimation a834caa511 Fix watch tracking, video unlocking and access-control gaps
Deploy Vault CMS / deploy (push) Failing after 38s
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>
2026-11-11 16:58:07 +02:00
twotalesanimation 08b6e58b8e Patch React2Shell RCE and harden auth, uploads and containers
Deploy Vault CMS / deploy (push) Successful in 2m23s
- Upgrade next 16.0.3 -> 16.3.5 and react/react-dom 19.2.0 -> 19.3.0
  (CVE-2025-55182 / CVE-2025-66478 unauthenticated RCE), next-auth -> 4.24.15
- Require admin role on /api/users, /api/courses and /api/enrollments,
  which were unauthenticated
- Replace ALLOWED_ADMINS checks that allowed any signed-in user when the
  variable was empty with the role-based requireAdmin
- Sanitize upload filenames, restrict thumbnail extensions, store edited
  thumbnails in UPLOADS_DIR, tighten traversal checks in file routes
- Dockerfile: Node 22, pinned pnpm, non-root user, critical audit gate
- Compose: read-only rootfs, dropped caps, no-new-privileges, Postgres
  password from env
- Exclude .env from Docker build context; add security headers
- Deploy with git reset --hard instead of git pull
- Remove stale package-lock.json

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
2026-09-15 11:52:33 +02:00
twotalesanimation a863a5cbf3 Serve uploaded MP4s at /uploads/videos with Range support
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>
2026-08-01 17:41:37 +02:00
twotalesanimation cc37509cce runner test
Deploy Vault CMS / deploy (push) Successful in 1m28s
2026-08-01 17:03:04 +02:00
twotalesanimation ae30260486 Runner push 2026-08-01 17:00:07 +02:00
twotalesanimation 459d05f834 Fix type errors that failed the Docker build
- Cast the transcoder upload body to node:stream/web ReadableStream;
  the DOM ReadableStream type lacks the async-iterator members
  Readable.fromWeb expects
- Exclude transcoder/ and transcoder-remote/ from the root tsconfig:
  they are standalone sub-projects with their own tsconfig and
  dependencies (e.g. archiver) that aren't installed in the root
  node_modules, so next build's typecheck can never resolve them

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 16:38:59 +02:00
twotalesanimation 4a2342dc7d Updated Dockerfile 2026-08-01 16:33:18 +02:00
twotalesanimation 4cd8986e69 Stream video uploads to disk instead of buffering in memory
req.formData() parsed the entire multipart body into RAM, so large
uploads (1.6GB+) OOM-killed the server. Bridge the web Request body to
a Node stream and parse it with formidable, which writes the file to
disk as it arrives:

- Temp dir is /uploads/tmp (same filesystem as the destination) so the
  post-parse move is an atomic rename, not a second multi-GB write
- Mark the bridged request as chunked when a proxy strips
  content-length, otherwise formidable assumes an empty body
- Track formidable temp files via fileBegin so aborted uploads get
  their partial files cleaned up
- Remove the dead formidable branch gated on (req as any).req, which
  is always null in the App Router

Also fix the nginx example in VPS_UPLOAD_CONFIG.md: client_max_body_size
was 1024M, below the app's 2.5GB limit, and would 413 large uploads at
the proxy before they reached the app.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-01 15:54:57 +02:00
twotalesanimation 81ad7e4ea9 Initial commit 2026-06-11 10:46:09 +02:00