diff --git a/docs/contract-requests/studio.md b/docs/contract-requests/studio.md index 6fb10c8..f2511f4 100644 --- a/docs/contract-requests/studio.md +++ b/docs/contract-requests/studio.md @@ -22,7 +22,7 @@ Layout after the review (controller ruling on I4): Proposal: main.ts calls `createCourseRepository(studioCourseDirs(config))`, or `loadConfig` appends both dirs. -Imported courses (`source.kind` other than `authored`, with their root under `/courses`) are read-only in the studio for v1, with the message "Imported course: read-only in the studio for now". There is a `TODO(studio)` in `feature.ts` (`readOnlyMessage`) for converting one into a studio-managed repo. The studio refuses to create a course whose id already exists as `/courses/`. WP-B should likewise refuse an import id that `/studio/repos/.git` already uses. +Imported courses (`source.kind` other than `authored`, with their root under `/courses`) are read-only in the studio for v1, with the message "Imported course: read-only in the studio for now". There was a `TODO(studio)` in `feature.ts` (`readOnlyMessage`) for converting one into a studio-managed repo; v1.1 S resolved it with the owner-only "Make editable in the studio" action (see `docs/operations.md`, "Imported courses in the studio"). The studio refuses to create a course whose id already exists as `/courses/`. WP-B should likewise refuse an import id that `/studio/repos/.git` already uses. ## 3. Live preview: swap in the learner `LessonView` diff --git a/docs/operations.md b/docs/operations.md index 673de68..404b62e 100644 --- a/docs/operations.md +++ b/docs/operations.md @@ -22,7 +22,7 @@ Mounts (all course mounts are read-only; STCourses never changes your originals) | Host | Container | What | |---|---|---| -| volume `stc-data` | `/data` | database (SQLite), imported courses (`/data/courses`), studio repos (`/data/studio/repos`) and their published worktrees (`/data/worktrees`), caches (`/data/cache`), AI drafts for non-studio courses (`/data/drafts`) | +| volume `stc-data` | `/data` | database (SQLite), imported courses (`/data/courses`), studio repos (`/data/studio/repos`) and their published worktrees (`/data/worktrees`), media of imports made editable in the studio (`/data/media`), caches (`/data/cache`), AI drafts for non-studio courses (`/data/drafts`) | | `STC_COURSES_HOST` (default `./courses`) | `/courses` | course repos, one folder with a `stcourse.yaml` per course | | `STC_IMPORT_HOST` (default `./import-inbox`) | `/import` | files the owner may import (Packt zips, video folders, PDFs, code zips) | @@ -78,6 +78,48 @@ Without Docker: `bun install`, then `STC_SECRET=… NODE_ENV=production bun apps - Course media (`/api/media/…`) is sent with `X-Content-Type-Options: nosniff`, and SVG, HTML and JavaScript files with `Content-Security-Policy: sandbox`, so author content cannot run script on the app's origin. +## Imported courses in the studio + +Imported courses (`/courses/`) are read-only in the studio until the owner chooses **Make editable in the +studio** on the course's studio page. That runs a background job (`studio.convert-import`) which: + +1. commits the manifest, lessons, transcripts, small images, small PDFs and code into a new studio repo, + `/studio/repos/.git`, checked out as `/worktrees//published`. The manifest is committed + unchanged: the course keeps its id, its visibility (restricted after an import) and its `source.kind`, so only the + owner can later make it public or signed-in. ACL entries, progress and notes are keyed by course and lesson id + and are kept; +2. keeps **large media out of git**: every video or audio file (`mp4 m4v webm mov mkv avi mp3 m4a wav ogg oga opus + flac`) and any other file over 8 MiB. Each is listed with its path, size and SHA-256 in `.stcourses/media.json` + in the repo and excluded by one anchored line in the repo's root `.gitignore` (a `.gitignore` the course already + had is kept above that block). Git LFS is not used: it is not in the image and would need an LFS server; +3. **moves the import folder to `/media/`** (decided over keeping it in place: a manifest left in + `/courses/` would be found as a second course with the same id, and the importer's "add to course" and + transcription jobs would keep changing a folder learners no longer see). `/media` is never scanned for + courses and nothing writes to it afterwards. File permissions are left alone, because an import made in `link` + mode shares its files with your originals; +4. puts every referenced file into the published worktree, and later into each author's draft worktree, as a + **hard link** to `/media//` (a copy if they are on different filesystems). The manifest paths are + unchanged, so `/api/media//…` serves the same URLs with Range support, and no extra disk space is used. + +In the studio, referenced media cannot be replaced or deleted (save, upload and conflict resolution refuse them with +409); upload a new file under another name instead. A studio write never goes through a hard link: a file with more +than one link is replaced, not overwritten. + +Interrupted conversions are safe to retry, and a job left running by a restart is retried automatically: if no +published worktree was made, the partial repo is removed and the conversion starts again; if the repo exists but the +import folder was not moved yet, only the move is done. + +Things to know: + +- Back up `/media` together with `/studio`: the repo alone does not contain the videos. +- **Sync** pushes only the git repo. A clone of the remote has the manifest, lessons and `.stcourses/media.json`, but + not the media files themselves; copy `/media/` alongside if you move the course to another server. +- If a worktree loses a media file (for example after a manual `git clean -x`), the next publish links it back into + the published worktree, and a new draft worktree gets every file. A file missing from `/media/` shows + as a course diagnostic ("lesson file not found"), never a crash. +- After conversion the importer can no longer add decks, code or transcripts to that course (it only edits + `/courses`). Add them in the studio. + ## Memory budget The deployment must stay under 2 GB idle. Measured with this compose file (`--profile postgres`), the image built