100 Commits
Author SHA1 Message Date
Lucas WintherandClaude Opus 5.5 6f0a297345 merge: v1.1 navigation motion
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:59:26 +01:00
Lucas WintherandClaude Opus 5.5 89db7f88b7 docs: record loading-line motion exception (R20)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:59:26 +01:00
Lucas WintherandClaude Opus 5.5 bc267c6562 fix(web): keep focus on the dock or outline control that navigated; announce only
Navigations started from the dock (Previous/Next) or the course outline leave focus on the control used and only
announce the new title; other navigations (content links, library, back/forward) still focus the new h1.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:56:37 +01:00
Lucas WintherandClaude Opus 5.5 4379d77b9c test(web): deterministic motion e2e (fetch gate, condition waits) and new frame capture
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:49:49 +01:00
Lucas WintherandClaude Opus 5.5 b5e00af4f7 fix(web,learner): navigation swaps at once; content-only entrance, loading line, focus (R19)
Drops document-level View Transitions and the lesson hold: they dropped clicks for 120-180 ms, froze the screen
while route code loaded, and let the dock act on a stale lesson. Navigation now swaps immediately; only the
content region enters (opacity and a 6 px rise, 160 ms); a 2 px loading line appears when code or data takes over
150 ms; the dock works from the outline and the URL, so a second Next advances again; focus moves to the new h1
and a polite live region announces it. Route chunks of a feature preload together so the sidebar never drops.
Scroll restore stops on user input; pdf.js never starts a load after cleanup.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:49:49 +01:00
Lucas WintherandClaude Opus 5.5 305c262ff1 docs: record motion plan deviations
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:26:15 +01:00
Lucas WintherandClaude Opus 5.5 89ff72cc28 fix(web): keep the sidebar through route transitions; end them within 120 ms
Router updates now run in a React transition, so a route whose code is still loading keeps the current page up
and the view transition captures the new page with its sidebar (it used to drop out and pop back on the first
course-to-lesson visit). The root group's 250 ms UA morph is off: pointer input goes to the document while a
transition runs, so it stays as short as the old page's 120 ms fade. The pointer-events rule had no effect.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:24:43 +01:00
Lucas WintherandClaude Opus 5.5 44f1b1d53d merge: v1.1 edit imported courses in the studio
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:01:40 +01:00
Lucas WintherandClaude Opus 5.5 f02e30450c fix(studio): review round 2 - a conversion that fails after its commit recovers without a restart
- The convert job reloads the course on success and on failure, so learners
  get the intact import (rolled back) or the published studio course, never a
  course whose folder moved away.
- POST /convert on a course with an incomplete conversion (.converting-<id>,
  staging or retired folders) queues a job that recovers and finishes it
  instead of answering 409; the studio page shows "Conversion incomplete" with
  Retry for the owner.
- The 409 for pending import jobs names their kinds and ids; the dead withLock
  in git.ts is gone; the dot-folder discovery rule is documented in
  docs/operations.md and spec section 5.
- Route tests (SQLite and Postgres) inject a failed cross-filesystem copy, a
  checksum mismatch and a pre-commit failure.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 21:00:45 +01:00
Lucas WintherandClaude Opus 5.5 57fe83f5de chore: motion frame capture script
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:59:50 +01:00
Lucas WintherandClaude Opus 5.5 6bb7681306 test(web): reduced motion disables all motion
Also turns off the fold height transition, which the universal selector does not reach (::details-content).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:55:32 +01:00
Lucas WintherandClaude Opus 5.5 771d755da4 fix(studio): review round 1 for imported-course editing
- Media exclusions move from the editable root .gitignore to the repo's
  info/exclude, and dirtyFiles never counts files listed in media.json, so an
  author's or remote's .gitignore cannot block publish or Sync. Publish and
  Sync refuse commits that track a file over referenced media.
- Importer jobs (code-repo, pdf-deck, transcribe) and the cover route run
  under the course lock now shared with the studio (core withCourseLock) and
  re-check the course inside it; after conversion they fail with "this course
  is now edited in the studio". Convert answers 409 while an import job for
  the course is queued or running, and the job re-checks.
- Crash-safe conversion: the import folder is hidden (.converting-<id>) before
  the published worktree exists; cross-filesystem moves copy into a staging
  dir, fsync, verify size and sha256, then rename; the source is retired as
  .converted-<id>.<n>. Recovery runs at server start and before every convert;
  a re-run of a finished conversion reports success. The loader skips
  dot-folders.
- Minor: no raw NUL bytes in source, link fallback only on EXDEV/EPERM/EMLINK,
  referenced media are not read whole by the editor, missing media are logged,
  mediaDir JSDoc, docs on per-author copies across filesystems.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:53:50 +01:00
Lucas WintherandClaude Opus 5.5 370d381ff4 feat(learner): progress, toast and menu motion
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:53:34 +01:00
Lucas WintherandClaude Opus 5.5 13393e10ba merge: v1.1 AI schema work (tool calls, mentor table, citations, sealed MCP secrets)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:51:35 +01:00
Lucas WintherandClaude Opus 5.5 d5e836cc79 feat(learner): cross-fade deck slides
The deck viewer draws the next slide into a second canvas and fades it in over the current one, 4 px from the
direction of travel; the stage never blanks while pdf.js draws. A newer draw into a canvas cancels the older one.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:50:54 +01:00
Lucas WintherandClaude Opus 5.5 f2169a1235 fix(ai): bind sealed secrets to purpose and row; never drop unreadable secrets
Review fix round 1.
- Secret box v2 tokens: one HKDF key per purpose (byok, server-api-key,
  mcp) and AES-GCM AAD bound to the row (user id + provider; server id +
  field). MCP env and headers are sealed per field. open() refuses v1
  tokens; the data upgrade re-seals v1 keys, v1 plaintext MCP values and
  the draft secretCipher, and marks ai_secrets_v2 only when nothing
  legacy is left (retried until the previous secret is back).
- Unreadable secrets are reported (admin: unreadableSecrets with server
  and field; user: byok.keyUnreadable), logged at startup, and kept on
  save unless replaced or cleared (clearApiKey, clearSecrets). The admin
  and account pages show the warnings.
- allowedOrigins and provider resolution no longer open MCP secrets.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:49:04 +01:00
Lucas WintherandClaude Opus 5.5 a83ac02a90 feat(learner): module lists and folds open with motion
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:47:12 +01:00
Lucas WintherandClaude Opus 5.5 a953e35178 feat(learner): hold the lesson until the next one loads, swap with a view transition
The sidebar highlight moves at once; the lesson column keeps the current lesson until the next has loaded, then
swaps inside a view transition, and the route scroll (top, or the saved position on back) is applied then.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:42:15 +01:00
Lucas WintherandClaude Opus 5.5 3988addaed fix(learner): destroy decks through the pdf.js loading task
pdf.js 6 documents have no destroy(); leaving a lesson whose deck had loaded threw in the effect cleanup and
unmounted the app.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:41:23 +01:00
Lucas WintherandClaude Opus 5.5 08e62ff683 test(e2e): narrow the Postgres URL for the typechecker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:38:49 +01:00
Lucas WintherandClaude Opus 5.5 260f7b52c8 docs(ai): contract requests 3-5 settled in v1.1
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:37:33 +01:00
Lucas WintherandClaude Opus 5.5 d607938630 feat(web): view transitions on route changes; content enter animation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:36:45 +01:00
Lucas WintherandClaude Opus 5.5 5c63f17cff feat(ai): structured tool calls on ChatMessage (contract request 3)
ChatMessage gains toolCalls and an opaque providerData; providers may
yield providerData once per turn. The assistant loop appends one
assistant message per model turn. Anthropic replays its own raw blocks
from providerData (no in-memory map) or maps text + tool_use blocks;
OpenAI maps toolCalls to tool_calls.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:36:38 +01:00
Lucas WintherandClaude Opus 5.5 f834088588 docs: imported courses in the studio (media kept out of git, moved to <data>/media/<id>)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:35:36 +01:00
Lucas WintherandClaude Opus 5.5 4a49e28fcc test(e2e): import, make editable, edit, publish; learner keeps progress and notes, media keep Range
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:35:10 +01:00
Lucas WintherandClaude Opus 5.5 ddc66f5be5 feat(ai): encrypt MCP env and headers at rest and mask them in the admin API
env/headers are sealed into secretCipher with the BYOK AES-GCM/HKDF box.
The admin GET returns masked values; a mask posted back for the same
server and key keeps the stored value, any other mask is a 400. The chat
and the test button use the stored values. upgradeAiData seals plaintext
v1 rows.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:33:56 +01:00
Lucas WintherandClaude Opus 5.5 20246b442f feat(studio): "Make editable in the studio" panel for imported courses
Owner sees what conversion does, starts it, follows progress and lands in the
workbench; other authors are told only the owner can. Existing tokens only.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:33:55 +01:00
Lucas WintherandClaude Opus 5.5 635fd66d04 feat(ui): scroll keeper with deferred settle
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:33:44 +01:00
Lucas WintherandClaude Opus 5.5 afa2338ad3 feat(studio): owner-only "Make editable in the studio" route and conversion job
POST /api/studio/courses/:id/convert (owner only, CSRF-guarded by core) queues
studio.convert-import; GET /api/studio/jobs/:id reports progress. Read-only
responses say whether the caller may convert. Resolves TODO(studio) (ruling R9).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:32:59 +01:00
Lucas WintherandClaude Opus 5.5 b1d17956f4 feat(ui): view-transition helper and held-value hook
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:32:57 +01:00
Lucas WintherandClaude Opus 5.5 17b6ae3828 feat(ui): motion tokens, keyframes and reduced-motion switch
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:32:21 +01:00
Lucas WintherandClaude Opus 5.5 eed59d4ef2 docs: v1.1 M navigation motion plan
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:31:36 +01:00
Lucas WintherandClaude Opus 5.5 df6d235070 feat(studio): convert an imported course into a studio repo, media kept outside git
Small files are committed unchanged (visibility and source.kind stay); large media
are referenced from .stcourses/media.json, ignored, and hard-linked into every
worktree from <dataDir>/media/<id>, where the import folder is moved. Studio writes
to referenced media are refused and never write through a shared inode.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:31:19 +01:00
Lucas WintherandClaude Opus 5.5 a995900fba feat(ai): per-course mentor preferences table (contract request 4)
0002_ai_mentor_prefs.sql adds ai_mentor_prefs(user_id, course_id, off).
upgradeAiData copies v1 config_json.mentorOff toggles into it once
(marked ai_data_v11 in schema_migrations); the settings repo awaits the
upgrade before first use and aiFeature starts it at construction.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:30:59 +01:00
Lucas WintherandClaude Opus 5.5 60179407f4 feat(studio): media references for imported courses (path, size, sha256, .gitignore)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:28:39 +01:00
Lucas WintherandClaude Opus 5.5 d34a5c1f0c feat(ai): citations column on ai_messages (contract request 5)
New packages/feature-ai/migrations/0001_ai_message_citations.sql adds
ai_messages.citations_json and folds v1 'sources' rows into the assistant
row before them. The feature writes and reads the column directly.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:28:23 +01:00
Lucas WintherandClaude Opus 5.5 5b8ba24b00 docs: v1.1 S plan (edit imported courses in the studio)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:27:40 +01:00
Lucas WintherandClaude Opus 5.5 fbfe16534a docs: v1.1 work package A plan (AI contract requests 3-5, MCP secrets)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:26:09 +01:00
Lucas WintherandClaude Opus 5.5 fd5c72afc2 docs: v1.1 plan (motion, studio import editing, perf, AI schema)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 20:21:39 +01:00
Lucas WintherandClaude Opus 5.5 58e0b20c95 merge: Phase 1 feature packages and Phase 2 integration
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:51:19 +01:00
Lucas WintherandClaude Opus 5.5 db5a604647 fix(ui): cover tiles keep the monogram for landscape images; quiet pill header nav
Landscape images (imported video frames, slide pages) would lose most of the
picture in a 3:4 crop, so the tile keeps its monogram; portrait and
near-square images (w/h <= 1.05) fade in over it, cropped from the centre.
The header nav follows the mockup's segmented-control style: muted text, no
underline, the current area raised in accent.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:47:01 +01:00
Lucas WintherandClaude Opus 5.5 8045c80d93 fix: final-review wave (CSRF guard, fail-closed owner bootstrap, streamed zips, e2e visibility suite)
- Critical 1 (CSRF): core wrap refuses every POST/PUT/PATCH/DELETE whose Origin
  (Referer as fallback) is not publicUrl's origin or STC_ALLOWED_ORIGINS, and
  any body that is not application/json (multipart only on declared upload
  routes). Better Auth routes go through the same guard; only declared
  identity-provider callbacks (SAML ACS/SLO, OAuth callbacks) are exempt.
  Better Auth trustedOrigins use the same list. Session cookie stays Lax
  (Strict breaks OAuth/OIDC/SAML returns); documented.
- Important 4 / learner minor 4: the first-owner claim and the role row are
  one transaction guarded by "no owner yet"; only a unique violation means
  learner; any other error fails the sign-up with 500 and removes the
  half-created user. Serialised per DB handle (Bun's SQLite connection).
- Important 2 / learner minor 12: code zips stream entry by entry (large
  files stored in 256 KB chunks with a data descriptor and incremental
  CRC-32), so memory stays at one chunk; the cap is now 1 GiB (ZIP32).
- Important 3 / learner minor 10: the e2e runs the production wiring
  (productFeatures order, AI drafts through the real sink) against a
  restricted course and asserts 404/401 for learner and anonymous on
  course, lesson, media, cover, code, zip, notes, progress, attempts, AI
  chat, library, search and AI status; a real Range request (206, 416);
  forged writes (foreign Origin, text/plain, no Origin) are 403 and the MCP
  test command never runs.
- Minors 7-9: compose passes every documented variable; the health check
  uses STC_PORT; productFeatures checks the studio-before-AI order.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:42:17 +01:00
Lucas WintherandClaude Opus 5.5 69aaedcb43 build: docker image and compose
Dockerfile on oven/bun:1.3 with ffmpeg, git, unzip and python3-pymupdf, a
production install, non-root user, /data volume and a health check;
docker-compose.yml (project stc) with an optional postgres:17 profile and
read-only course and import mounts; docs/operations.md with configuration,
mounts, security headers and the measured idle memory (101.4 MiB for app plus
Postgres after 5 minutes idle).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:17:58 +01:00
Lucas WintherandClaude Opus 5.5 d71a95dbc1 fix(web): styled role-filtered nav, hidden on phones; clearer studio preview label
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:07:45 +01:00
Lucas WintherandClaude Opus 5.5 777ae2cc37 feat: resolve Phase 1 contract requests and compose every feature
Contracts and core:
- Feature gains auth and stop; FeatureFactory; core composeServer builds the
  context, runs the factories, then the job queue from the union of their jobs
  (R8/R14). startServer uses it; main.ts composes learner, import, studio, AI.
- AppConfig/loadConfig own the layout and settings: courseDirs include
  <dataDir>/courses and <dataDir>/worktrees, studioReposDir, cacheDir,
  importRoots, uploadMaxMb, aiAllowedBaseUrls, transcribeUrl, python.
  resolveConfig derives the same for tests; cachePaths shares the importer's
  cache layout.
- canView: the global author role views every course (R14); the learner,
  studio and AI packages all call it.
- migrate(db, packages): core first, then packages in name order.
- Media: nosniff on every response, a sandbox CSP on svg/html/js; a Range on
  an empty file is 416.
- courseUrl/lessonUrl and the lesson route patterns live in @stc/contracts.

Packages:
- import, studio and AI read AppConfig instead of process.env; the importer
  refuses ids used by studio/repos.
- learner migrations run through core migrate; library covers fall back to
  the importer's cached candidate behind a canView-checked route.
- studio preview renders through the learner LessonView (anchors honoured,
  draft URLs); AI drafts go to the studio draft branch for studio courses (as
  the requesting user) and to <dataDir>/drafts otherwise, and the generator
  page says which.

Web: tsconfig paths for @stc/<feature>/web browser entries; features.ts
imports them by package name; nav.roles hides links by role.

Tests: compose, config, canView, media headers, migrations order, covers,
preview, nav, SPA bundle without server code, and an end-to-end flow on
SQLite and Postgres.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 15:02:46 +01:00
Lucas WintherandClaude Opus 5.5 427eda6e93 merge: ai
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:44:06 +01:00
Lucas WintherandClaude Opus 5.5 2854c26a7d merge: studio
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:42:51 +01:00
Lucas WintherandClaude Opus 5.5 627ebe0029 merge: import
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:41:36 +01:00
Lucas WintherandClaude Opus 5.5 d8d62696da merge: learner
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:40:29 +01:00
Lucas WintherandClaude Opus 5.5 0da35ec6f8 fix(learner): remove the SPA proxy; CSP as a meta tag in index.html (ruling R16)
The proxy, its internal server, Unix socket and NO_PROXY changes are gone; main.ts serves the HTML
import directly again. apps/web/index.html carries the SPA policy as <meta http-equiv=CSP> ahead of every
script (script-src 'self' plus the hash of Bun's dev-server helper, no unsafe-inline/eval; no
frame-ancestors, which meta cannot carry). Dev HMR connects under it. The runner frame keeps its header CSP.

Tests: the served index.html (prod and dev, in a subprocess) has the meta policy and every inline script
is hash-covered; the XSS browser test fails with the sanitizer disabled and passes with it, and the meta
CSP alone blocks unsanitized handlers (with a no-CSP control); no source assigns proxy env vars, and the
feature leaves them untouched at runtime. Contract request 5 asks core for frame-ancestors/X-Frame-Options.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:38:04 +01:00
Lucas WintherandClaude Opus 5.5 213f2247b4 fix(learner): SPA proxy forwards no credentials and never goes through HTTP_PROXY
spaWithCsp proxies GET and HEAD only (405 otherwise, no body buffering) and forwards only accept,
if-none-match, if-modified-since and range. The internal server listens on a Unix socket in a private
0700 temp dir. Bun 1.3.14 ignores proxy: false for this hop, and over a socket with a proxy set it sends
an absolute-form target that breaks asset routing, so localhost, 127.0.0.1 and ::1 are appended to
NO_PROXY/no_proxy (existing entries kept). accept-encoding is no longer forced; fetch decompresses and
the encoding headers are dropped.

Test: in a subprocess with HTTP(S)_PROXY and ALL_PROXY set to a dead proxy, the SPA and an asset load
with the CSP, POST gets 405, and the upstream sees no Cookie, Authorization or X-Forwarded-For.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 14:27:56 +01:00
Lucas WintherandClaude Opus 5.5 c7b9e1f98e fix(learner): review round 1: sanitize-html, sandboxed runner frame, one visibility rule, no Tab capture
C1: lesson HTML goes through a sanitize-html allowlist on the fully rendered output (no on* attributes, no
style, no svg/math/iframe/object/embed/form; URLs limited to http(s), mailto, relative and anchors). Raw tags
outside the allowlist are escaped as text. The SPA document is served with a CSP (script-src 'self', no inline
script) through spaWithCsp in main.ts; contract request §5 asks core to own it.

C2: exercises run in <iframe sandbox="allow-scripts"> (opaque origin, no cookies) loaded from
/api/learner/runner-frame, whose CSP allows only its own script (by hash) and blob: workers, and connect-src
'none'. postMessage only, both sides check event.source; the timeout removes the frame. The worker stubs now
also cover Worker, SharedWorker and caches.

I1: learner visibility calls core canView with per-course authorship only, like /api/media (contract request §6).
I2: Tab and Shift+Tab move focus; Enter keeps the previous line's indentation.

Tests: XSS unit cases, headless-Chrome checks for the exploit lesson and for fetch, import, Cache.add, nested
Worker, img, EventSource, WebSocket, XHR and sendBeacon from learner and test code (plus a negative control),
visibility on both dialects, SPA and runner-frame CSP headers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 05:20:43 +01:00
Lucas WintherandClaude Opus 5.5 061ae7a2d9 feat(learner): search results show lesson matches cleanly; contract notes for the browser entry and LessonView; plan marked done
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:54:53 +01:00
Lucas WintherandClaude Opus 5.5 89c51f013d feat(learner): web shell, library, lesson views (LessonView), decks, code files, exercises, notes, account and admin pages
Tasks 10-12 of the WP-A plan in one commit (the route modules import each other):
- header extras portalled into the Phase 0 header: course switcher, search, Reading/Work, account
- course frame with outline sidebar (mobile overlay, Escape closes), progress, gating locks
- library: featured course, continue row, New and collection rows, course list, filters, search
- LessonView: text (track switcher, folds, ordered hint tiers), video (resume, done near the end,
  timestamp notes, transcript), companion decks rendered with pdf.js (thumbs, arrows, fullscreen,
  per-slide notes), code files (tree, tabs, copy, zip, command), exercises (Web Worker runner or
  local command with optional pasted evidence), floating action bar
- notes list, account (email/password, providers, SSO by email) and owner admin pages
- pdf.js worker served from /api/learner/pdf.worker.mjs
- browser entry @stc/feature-learner/web (the package index also carries the server half)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:53:36 +01:00
Lucas WintherandClaude Opus 5.5 f3ce2e34bf fix(ai): lessonUrl builder in one place; authoring follows core canView
- Learner links go through lessonUrl/courseUrl (src/web/urls.ts).
- Generation, the studio course list and file listing require an authoring
  right and core canView, so a global author needs a grant for a restricted
  course. Contract requests 8 (global author in canView) and 9
  (STC_AI_ALLOWED_BASE_URLS in AppConfig).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:49:48 +01:00
Lucas WintherandClaude Opus 5.5 ead7327bb1 fix(ai): solution and hidden-file guard runs on the canonical real path
read_file and read_lesson URL-decode, posix-normalise, reject .. and absolute
paths, resolve through CourseRepository.resolvePath (symlinks followed), then
guard the real course-relative path and compare real paths with every
manifest browser.solution. Hidden files are refused too.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:48:56 +01:00
Lucas WintherandClaude Opus 5.5 cfc41c6a63 fix(ai): stdio MCP servers get PATH, HOME, LANG and their configured env only
STC_SECRET, database URLs and API keys are no longer inherited by MCP child
processes. Test: a fake server reports its environment.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:47:47 +01:00
Lucas WintherandClaude Opus 5.5 a13cc66ebb fix(ai): BYOK endpoints limited to official presets and admin-allowed origins (SSRF)
- A user's own key goes only to https://api.anthropic.com, https://api.openai.com/v1
  or an origin listed in STC_AI_ALLOWED_BASE_URLS / the server config's
  allowedBaseUrls; anything else is a 400, and a stored key whose origin was
  later removed stops being used.
- User-scope provider calls never follow redirects, and their errors reach the
  user only as the HTTP status plus a fixed message; the full text is logged.
- The admin's server-scope provider may still use any base URL.
- Admin UI: allowed origins field; MCP stdio env editable, HTTP headers kept.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:47:25 +01:00
Lucas WintherandClaude Opus 5.5 2d162a4282 fix(studio): review round 1 (symlink-safe writes, remote allowlist, upload cap/types/no-overwrite, owner rule at publish/sync/resolve, studio/repos layout and read-only imports, side-effect-free list)
C1: writes lstat every path component and refuse symlinks (dangling included),
check the parent's real path after mkdir, and open with O_NOFOLLOW.
I1: only https://, ssh:// and git@host: remotes; protocol.file/ext/fd=never on
every fetch and push; global/system git config ignored.
I2: STC_UPLOAD_MAX_MB cap (default 100) by Content-Length and while streaming;
extension allowlist (415); stcourse.yaml refused; 409 unless overwrite=true;
sandboxing CSP on draft raw files.
I3: owner-only visibility/source.kind rule checked on the merged result at
publish and sync, and at resolve; publish also validates the merged tree.
I4: bare repos in <dataDir>/studio/repos/<id>.git; imported courses read-only.
I5: list status comes from the bare repo; drafts are created lazily.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:43:53 +01:00
Lucas WintherandClaude Opus 5.5 0ff8e6a5de docs(ai): mark plan tasks done
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:40:02 +01:00
Lucas WintherandClaude Opus 5.5 1f111b80db docs(ai): contract requests; dev fake provider returns a draft file
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:39:58 +01:00
Lucas WintherandClaude Opus 5.5 a47e6e0971 feat(ai): my_progress tool so the assistant sees the learner's progress
Spec 9.2 has the assistant retrieve from the learner's notes and progress;
progress was missing. Reads progressRepo(db).get for the signed-in user.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:37:25 +01:00
Lucas WintherandClaude Opus 5.5 6fb0019cd9 feat(ai): read the importer's deck and transcript cache; studio DraftWriter fits DraftSink as is
Retrieval now reads <dataDir>/cache/decks/<courseId>/<deckId>/text.json and
<dataDir>/cache/transcripts/<courseId>/<lessonId>.json (skipped when the lesson
has a transcript file). DraftSink returns { location?, branch?, commit? }, so
the studio's createDraftWriter(ctx) needs no adapter; generateDraft computes
paths itself and reports the draft branch.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 04:36:48 +01:00
Lucas WintherandClaude Opus 5.5 08d3e3f642 feat(ai): assistant panel, AI settings and studio generator UI
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:30:22 +01:00
Lucas WintherandClaude Opus 5.5 99c0e4929f feat(learner): lesson markdown pipeline with track variants, folds, hint tiers and safe HTML
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:29:14 +01:00
Lucas WintherandClaude Opus 5.5 667f3e814b fix(studio): forbid ext:: remotes; type the per-dialect test table
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:27:30 +01:00
Lucas WintherandClaude Opus 5.5 2849d3c911 docs(studio): contract requests (ctx feature factories, worktrees course dir, LessonView swap, web subpath, nav roles, DraftWriter)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:26:58 +01:00
Lucas WintherandClaude Opus 5.5 9ff5862382 feat(learner): JS/TS exercise runner in a Web Worker with timeout and network blocked
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:26:32 +01:00
Lucas WintherandClaude Opus 5.5 7adf9765b1 fix(import): allowed import roots, hostile zip symlink removal, tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:26:22 +01:00
Lucas WintherandClaude Opus 5.5 0992df7257 feat(studio): studio web UI (course list, manifest, lessons with live preview, decks, attachments, exercises, conflicts, sync) and dev entry
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:26:18 +01:00
Lucas WintherandClaude Opus 5.5 752e44696f fix(learner): zip buffer types
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:25:30 +01:00
Lucas WintherandClaude Opus 5.5 34826762a8 feat(learner): section code listing and zip download
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:25:18 +01:00
Lucas WintherandClaude Opus 5.5 a57222ecc3 feat(ai): HTTP routes, chat streaming and generation job
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:25:10 +01:00
Lucas WintherandClaude Opus 5.5 478ebeb255 fix(learner): narrow lesson target result for the type checker
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:24:28 +01:00
Lucas WintherandClaude Opus 5.5 96c147e3a2 feat(learner): progress, notes and exercise attempt API
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:24:16 +01:00
Lucas WintherandClaude Opus 5.5 5f85ce1d4b feat(learner): library, search, course outline with gating, lesson payload API
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:23:30 +01:00
Lucas WintherandClaude Opus 5.5 3cec42bba5 feat(ai): house-style generator writing drafts through a DraftSink
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:22:38 +01:00
Lucas WintherandClaude Opus 5.5 c4df51f7ca feat(import): UI polish, contract requests, report
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:22:22 +01:00
Lucas WintherandClaude Opus 5.5 a8a8e32bcb feat(learner): owner admin API for roles, groups, course access and library curation
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:20:49 +01:00
Lucas WintherandClaude Opus 5.5 90a477691b feat(import): admin UI at /admin/import, dev server, extraction cleanup
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:20:40 +01:00
Lucas WintherandClaude Opus 5.5 49d1df0f13 feat(ai): assistant prompt, course tools and tool loop
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:20:31 +01:00
Lucas WintherandClaude Opus 5.5 668ae379bf feat(learner): Better Auth sessions, owner bootstrap, claim groups; server takes feature factories
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:20:06 +01:00
Lucas WintherandClaude Opus 5.5 24cd6329cb feat(ai): MCP client over stdio and streamable HTTP
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:19:13 +01:00
Lucas WintherandClaude Opus 5.5 9f96e293e1 feat(import): wire feature into server and web shell; browser-safe web entry
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:18:26 +01:00
Lucas WintherandClaude Opus 5.5 497ce91216 feat(studio): /api/studio routes, author access, acceptance tests
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:18:25 +01:00
Lucas WintherandClaude Opus 5.5 50e1e49b6d feat(ai): BM25 course retrieval over lessons, transcripts, code, deck text and notes
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:18:21 +01:00
Lucas WintherandClaude Opus 5.5 9aa475329a feat(learner): Better Auth adapter over Bun.SQL for SQLite and Postgres
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:17:44 +01:00
Lucas WintherandClaude Opus 5.5 596099a13f feat(import): owner-only API routes and importFeature
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:17:31 +01:00
Lucas WintherandClaude Opus 5.5 7c3f613feb feat(import): optional transcription job
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:17:07 +01:00
Lucas WintherandClaude Opus 5.5 5a215744e5 feat(learner): package scaffold, auth tables migration, migrations runner
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:16:31 +01:00
Lucas WintherandClaude Opus 5.5 06b6ea6168 feat(studio): published-worktree watcher and DraftWriter API for the AI package
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:16:13 +01:00
Lucas WintherandClaude Opus 5.5 0e090a0e15 feat(import): code-repo job
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:16:12 +01:00
Lucas WintherandClaude Opus 5.5 76ea0d5101 feat(ai): OpenAI-compatible provider (covers Ollama)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:16:06 +01:00
Lucas WintherandClaude Opus 5.5 524b1b4645 feat(import): code-repo job
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:16:00 +01:00
Lucas WintherandClaude Opus 5.5 0b117159cd docs: WP-A learner package plan
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:15:48 +01:00
Lucas WintherandClaude Opus 5.5 9738da458d feat(import): video-folder and pdf-deck jobs, deck cache
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:15:44 +01:00
Lucas WintherandClaude Opus 5.5 3c2220145b feat(ai): Anthropic provider over fetch with streaming tool use
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:15:33 +01:00
Lucas WintherandClaude Opus 5.5 c2385b472c feat(studio): publish as merge, update draft, conflict resolution, explicit sync
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:15:20 +01:00
Lucas WintherandClaude Opus 5.5 07e2223c8a feat(import): Packt parser and packt-video job
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:15:09 +01:00
Lucas WintherandClaude Opus 5.5 a6f887f31c feat(studio): git wrapper and StudioStore (create course, drafts, save as commit)
Co-Authored-By: Claude Opus 5.5 <[email protected]>
2026-10-30 03:14:47 +01:00