b267db631973c8e80949de415628b2e35b33dec0
28
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
96bade402a |
fix: sequential progression bug in course with ordered contents (#889)
* fix: sequential progression bug in course with ordered contents * test(utils): add course progression tests and unify navigation ordering Reuse flattenNavigableItems in getOrderedNavigableContent so next/previous navigation follows the same section-aware order as progression locks. Add vitest coverage for grouped multi-section flattening and sequential access rules. Co-authored-by: Rotimi Best <[email protected]> * fix: address PR review comments for course progression Simplify grouped flatten ordering, only block progression for lessons and exercises explicitly, integrate flattenNavigableItems in the sequential access test, and include @cio/core watch in api:dev:full. --------- |
||
|
|
20e34e29dd |
feat(exercise): add open-ended thumbs up/down question type (#780)
* feat(ui): add THUMBS question type renderer - Add seven new renderer files under renderers/thumbs/: thumbs-correct.ts, take.svelte, edit.svelte, preview.svelte, thumbs-review-toggle.svelte, review.svelte, submission.svelte - Register THUMBS in renderer-contract.ts with edit/take/preview/submission/review - Export ThumbsEdit, ThumbsTake, ThumbsPreview, ThumbsSubmission, ThumbsReview from exercise-question/index.ts - Add THUMBS_FIXTURE, four thumbs label keys, and Thumbs story in storybook exercise-question files - Add THUMBS case to question-type-modes.svelte submission builder Co-authored-by: Rotimi Best <[email protected]> * feat(question-types): add THUMBS contracts, scoring, and seed Introduce the THUMBS question type (id 14) with boolean answer data, codec, auto-grading, Zod validation, DB seed/schema docs, and binary normalize support shared with TRUE_FALSE. Dashboard wiring is deferred. Co-authored-by: Rotimi Best <[email protected]> * feat(dashboard): wire THUMBS question type into exercise UI Connect THUMBS option sync in edit/view modes, labels, picker icon, and locale strings so the existing contracts surface in the dashboard. Co-authored-by: Rotimi Best <[email protected]> * fix(ui): polish thumbs question UI and move premium upgrade icon Widen thumbs Yes/No toggles, inherit selected icon color via custom class, replace badge-based review correctness with solid fill states and bare Check/X icons, add a success pill in preview, and place the premium upgrade icon at the end of the question type select row. Co-authored-by: Rotimi Best <[email protected]> * fix(ui): align thumbs review colors with prototypes Tune correct/incorrect selected states so review buttons use dark emerald/red content colors on solid fills, matching the design comps. Co-authored-by: Rotimi Best <[email protected]> * fix(ui): address remaining PR review comments Use a shared timestamp local in thumbs edit fallback options. Move border-beam animation-delay after the animation shorthand. Alias --spacing to --ui-spacing for tw-animate compatibility. Co-authored-by: Rotimi Best <[email protected]> * fix(db): sync question_type sequence before seeding new types Avoid duplicate key errors when question_type_id_seq lags behind rows inserted via restore or explicit ids (e.g. THUMBS on restored databases). Co-authored-by: Rotimi Best <[email protected]> * fix(exercise): resolve thumbs review label ICU crash Remove {value} ICU placeholder from thumbs.review.correct_answer_label since labels are resolved at map-build time without context. Append the dynamic yes/no label in thumbs-review-toggle instead. Also bind question points through the questionnaire store index to avoid the Svelte non-reactive property binding warning in edit-mode. Co-authored-by: Rotimi Best <[email protected]> * docs(agent): add THUMBS guidance and sync question type docs Add shared agent question-type guide helpers in @cio/question-types and use them across the teacher prompt, add/update_questions tool schemas, legacy ai-assistant schemas, and MCP exercise tools. - Document when to use THUMBS (Yes/No sentiment) vs TRUE_FALSE (factual) - Fix stale MCP question type ids and add THUMBS=14 - Exclude disabled MATCHING/HOTSPOT from enabled type lists Co-authored-by: Rotimi Best <[email protected]> * fix(thumbs): clarify auto-grading and add editable option labels THUMBS is auto-graded like TRUE_FALSE; the difference is thumbs UI with customizable Yes/No-style labels and the teacher selecting the correct option. Render take/preview/review from question.options labels and add label inputs in the thumbs edit renderer. Update agent and MCP docs to stop implying THUMBS is manual or non-gradable. Co-authored-by: Rotimi Best <[email protected]> * chore(mcp): bump version to 0.0.10 Co-authored-by: Rotimi Best <[email protected]> * fix(question-types): make THUMBS open-ended with no correct answer THUMBS is a sentiment/poll question type, not an auto-graded binary like TRUE_FALSE. Remove correct-answer UI, scoring, and validation; update agent and MCP docs accordingly. Co-authored-by: Rotimi Best <[email protected]> * fix(dashboard): stop treating THUMBS as auto-graded in editor validation Normalize thumbs questions on load/save, show an Open-ended badge instead of Auto, and only enforce points/correct-answer rules on auto-gradable types. Co-authored-by: Rotimi Best <[email protected]> * revert(dashboard): remove unsolicited open-ended grading badge THUMBS uses the existing Manual badge like other non-auto-gradable types. Co-authored-by: Rotimi Best <[email protected]> * fix(exercise): show question title validation errors in editor Display mapped title errors beneath the question title input instead of only highlighting the question card border. Pass the exercise entity to mapZodErrorsToTranslations so messages resolve to validations.exercise.* keys, and use a translated snackbar key for save validation failures. Co-authored-by: Rotimi Best <[email protected]> * fix(mcp): build workspace deps before MCP publish bundle The MCP esbuild step imports @cio/question-types and @cio/utils dist outputs. CI failed on a clean checkout because mcp:build only built the MCP package itself. Build those dependencies first so 0.0.10 can publish. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]> |
||
|
|
2a53d675f6 |
fix(jobs): survive Redis Cloud maintenance reconnects without crashing API (#775)
* fix(jobs): survive Redis Cloud maintenance reconnects BullMQ duplicates ioredis connections without inheriting error listeners, so Redis Cloud maintenance (ECONNABORTED) crashed the API process. Attach error handlers on duplicates, harden reconnect options for failover, and add API process-level safety nets. Co-authored-by: Rotimi Best <[email protected]> * refactor(jobs): address review feedback on Redis resilience fix - reconnectOnError returns 2 for READONLY/LOADING so the failed command is resent against the newly promoted primary after failover - export prepareRedisConnection and test the real implementation instead of a reimplemented copy; add a no-double-listener regression case - API uncaughtException now reports to Sentry and fails fast (flush + exit 1) so the orchestrator restarts cleanly instead of running in an undefined state Co-authored-by: Rotimi Best <[email protected]> * fix(api): don't fail-fast on transient Redis errors in process guard End-to-end testing (restarting Redis to simulate Cloud maintenance) revealed node-redis surfaces reconnect failures as an uncaught AggregateError [ECONNREFUSED], which the fail-fast uncaughtException guard turned into a process exit. Classify transient Redis/socket errors (incl. AggregateError sub-errors) and log-and-continue for those; still fail fast on unknown errors. Co-authored-by: Rotimi Best <[email protected]> * fix(api): address PR review on Redis process guards and logging - Fail fast on non-transient unhandledRejection (Greptile) - Filter Sentry OnUncaughtException/OnUnhandledRejection integrations to avoid double-capture; keep custom guards with transient Redis filtering - Make wrapDuplicate idempotent; throttle reconnect/error logs during maintenance - Drop READONLY/LOADING from node-redis transient classifier (command-level only) Co-authored-by: Rotimi Best <[email protected]> * fix(api): rate-limit transient Redis warnings in process guards Reuse logTransientRedisEvent from core so maintenance bursts do not flood logs from uncaughtException/unhandledRejection handlers. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]> |
||
|
|
0f23453f91 |
fix(api): use admin app URL for teacher auto-enroll email links (#771)
* fix(api): use admin app URL for teacher auto-enroll links Teacher payment-request emails now build course and grant-access URLs with getAppBaseUrl() instead of getDashboardBaseUrl(org), so links point at app.classroomio.com rather than a custom or tenant domain. Document the teacher vs student email link rule in AGENTS.md. Co-authored-by: Rotimi Best <[email protected]> * docs: surface email link URL rules where emails are wired Document getAppBaseUrl vs getDashboardBaseUrl in dashboard-url.ts, packages/email/README.md, enqueueTransactionalEmail JSDoc, and AGENTS.md so staff vs learner hosts are discoverable when building email links. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]> |
||
|
|
27229cf8fe |
fix: canonical exercise completion, teacher course completion & upsert member fixes (#767)
* fix(db): preserve not-found error and dedupe member lookup in upsert (#763) Re-throw Organization member not found so the service layer can return 404. Fetch existing notification prefs by resolved member id instead of calling getOrganizationMemberEmailNotifications, which repeated the lookup. Co-authored-by: Rotimi Best <[email protected]> * fix(course): single source of truth for exercise completion; let teachers complete courses Exercise completion was computed four different ways (course progress, enrolled courses, content items, progression locks), and two of them counted any submission as complete, ignoring the exercise's 'passed' completion policy and pass threshold. My Learning and certificate eligibility could therefore report 100% while the course itself showed an unpassed exercise. - Add isExerciseCompletedSql, a policy-aware SQL predicate in the db query layer, and route every completion computation through it (getCourseProgress, getEnrolledCourses, getCourseContentItems, getCompletedExerciseIdsForMember, isExerciseCompletedForMember). - Remove the per-exercise N+1 pass checks in progression and the duplicated policy interpretation in the submission service. - Fix infinite "Checking eligibility" modal on the certificates page: open the completion modal in its final state instead of 'checking' with no follow-up. - Align teacher experience with students: certificate eligibility no longer requires the student role, completion modal triggers for any enrolled member, teachers can view/download their earned certificate, and video-watch progress tracks in the student experience regardless of org role. * chore: run api:dev without prebuild; keep full build as api:dev:full * fix(dashboard): gate video watch tracking on learner view * feat: pr reviews --------- Co-authored-by: Rotimi Best <[email protected]> |
||
|
|
bfafc685e0 |
fix(exercise): persist completion policy and improve dev build workflow (#754)
* fix(exercise): persist completion policy and pass threshold on save buildExerciseUpdateFields accepted completionPolicy and passThreshold in the validation schema but never mapped them to the database update, so exercise settings appeared to save while reverting on refresh. Co-authored-by: Rotimi Best <[email protected]> * fix(db): build workspace deps before compiling @cio/db imports @cio/utils/notifications, which only exists after @cio/utils is built. Building db in isolation failed when utils dist was missing or stale. - Add a db prebuild step for question-types, utils, and email - Clean dist and tsbuildinfo before utils and email builds so incremental tsc cannot skip emission after dist was removed Co-authored-by: Rotimi Best <[email protected]> * chore(dev): build api workspace deps before api:dev Run turbo build for @cio/api and @cio/jobs-worker dependencies before starting the dev servers, so missing dist/ folders do not break startup. Co-authored-by: Rotimi Best <[email protected]> * feat(course): improve exercise progression grading UX --------- Co-authored-by: Rotimi Best <[email protected]> |
||
|
|
fd8bec3e70 |
feat(billing): enforce free-plan student cap across the app (#733)
* feat(billing): enforce free-plan student cap across the app
Free (BASIC) plan orgs were able to exceed the advertised 20-student
limit because the cap existed only as dashboard cosmetic copy with no
server-side enforcement.
Add a single source of truth (getStudentLimit / STUDENT_LIMITS) and a
central guard (assertStudentCapacityOrThrow) wired into every path that
creates a new student-role org member: self-enrolment, course-invite
acceptance, bulk audience import, and org-invite acceptance. The guard
throws UPGRADE_REQUIRED (403) and notifies org admins by email (throttled
to once per org per day) when the limit is hit.
Surface the limit in the UI: disable the audience import button and the
course new-student invite section with upgrade banners for admins, and
show a "contact your administrator" message on the public enrol page for
students (who cannot upgrade). Expose a studentLimitReached flag from the
public course endpoint so the enrol button can be disabled proactively.
Add a one-time (manual, non-cron) db script,
db:notify-students-over-limit, to email admins of orgs already over the
limit at rollout.
* feat(billing): guard remaining student-creating paths against the cap
The initial cap enforcement missed several service entry points that also
create a new student-role org member. Add the capacity guard to:
- autoEnrollStudent (public org self-join on a tenant site)
- cohort enrollment (bulk group enrollment and single member accept)
- ensureProgramCourseAccess (program/cohort-granted course access)
For the cohort and program paths, only genuinely new members are counted
(existing org members are skipped) so re-enrolling current students is
never falsely blocked. The core path enforces via the shared limit check;
it throws UPGRADE_REQUIRED without the admin email since the queue lives
in the api layer.
* fix(billing): don't show cap block to existing org members on enroll page
The public course-by-slug endpoint is unauthenticated, so the enroll
page's studentLimitReached flag is org-wide and can't tell whether the
current viewer is already an org member. An existing student of a full
BASIC org would see Join disabled even though the backend correctly lets
existing members enroll without consuming a new seat.
Only pre-block anonymous visitors (would-be new joiners). Logged-in users
are governed by the membership-aware backend, which allows existing
members and returns UPGRADE_REQUIRED (surfaced via the enroll onError
mapping) for genuinely new students.
* feat(billing): deliver org plan-limits via /account with an extensible registry
Replace the ad-hoc, per-surface audience fetches (each calling getOrgAudience
just to read a count) with a single, future-proof delivery of org usage +
limits on the /account payload, gated to admin/tutor members (students never
receive limit data; a plain COUNT, no student rows).
Model it generically so future org limits (courses, etc.) drop in without
reshaping the payload or re-plumbing the frontend:
- New packages/utils plan-limits registry: PLAN_LIMIT_RESOURCES, PLAN_LIMITS
(reuses STUDENT_LIMITS), getPlanLimit, and a JSON-safe ResourceUsage /
OrgUsageLimits shape with isResourceLimitReached / resourceLimitRemaining.
- getAccountData attaches a per-org `limits` map for admin/tutor orgs.
- Store selectors currentOrgLimits / studentUsage / isStudentLimitReached;
sidebars, upgrade card, People pages, and invite modals read these instead
of fetching audience rows.
Bring cohort surfaces to parity with course: cohort People page banner and
cohort sidebar People upgrade icon, matching the course equivalents.
Also: close the upgrade modal that opened over the invite modal (drop the
`add` param via openUpgradeModalOver), redesign the sidebar upgrade card
(progress toward the student limit), add a Progress `variant`, and document
the no-complex-expressions-in-object-literals rule in AGENTS.md.
* refactor(billing): address PR review on the limits work
- Dedupe new student profile IDs before the cohort seat check so duplicate
batch entries can't overcount against the cap (Greptile P1).
- Drive the org-sidebar upgrade indicator from a dynamic nav-item `upgrade`
flag (audience config declares `upgradeResource: 'students'`; groups set it
from a `limitsReached` map) instead of a hardcoded path check.
- Collapse the audience Import button into a single button with ternary
`disabled`/`href`.
- Simplify the notify-students-over-limit upgrade link to app.classroomio.com.
- Drop the comment on the org `limits` field.
- Add .greptile/rules.md documenting the manual one-time db/src/scripts
convention (operator-controlled reruns) so idempotency isn't reflagged.
* fix(billing): name the org in the enroll capacity message
Change the enroll-page student-limit message from "This course has reached
its student capacity" to "{orgName} has reached its student capacity", since
the cap is org-wide, not course-specific. Pass orgName from the loaded org on
the page and from the currentOrg store in the enroll onError snackbar.
* style(billing): use PremiumIcon for the Authentication upgrade indicator
Replace the RocketIcon on paid settings sub-items (e.g. Authentication) with
the PremiumIcon used on the Audience item, right-aligned (ml-auto) to match.
|
||
|
|
0b5247bed3 |
fix(org): route admin and tutor invites to app domain (#731)
Team member invite emails and link-invite copy URLs were built with the org tenant host (custom domain or siteName.myclassroomio.com). Admin and tutor invites should always point at the admin dashboard origin (app.classroomio.com in cloud). Student org invites are unchanged. |
||
|
|
c72558f8f1 |
fix(course): treat null isUnlocked as unlocked in progression logic
Items with isUnlocked = null were evaluated as teacher-locked due to `?? false` defaulting to false before negation. This caused content to appear locked immediately after a student marked a lesson complete, even in free-mode courses. Changed to `?? true` in both client-side (progression-utils.ts) and server-side (core/progression.ts) so null/undefined isUnlocked defaults to unlocked — consistent with the sidebar's own `?? true` guard. |
||
|
|
a61942feb9 |
feat: configurable upload file size limits via .env (#695)
* feat: make upload file size limits configurable via .env Add UPLOAD_MAX_*_MB environment variables for documents, images, videos, exercise assignment files, AI assistant documents, landing page images, and media thumbnails. API presign routes validate file size server-side; dashboard UI reads the same limits from layout data. * chore: wire upload limit env vars through Docker compose and docs Pass UPLOAD_MAX_*_MB from the root .env into api and dashboard containers in both compose files, document them in .env.example and self-hosting guides. Co-authored-by: Rotimi Best <[email protected]> * docs: cross-link upload limits in README, Railway, Coolify, and Cloudflare guides Co-authored-by: Rotimi Best <[email protected]> * fix: address PR review feedback on upload limits - Restore 5 MB default for thumbnail uploads (edit-asset-dialog regression) - Add getResolvedUploadLimits() fallback for Storybook/tests - Cache core getUploadLimits() like the dashboard helper - Document presign fileSize as advisory; allow zero-byte fileSize in schema - Only send fileSize to presign when greater than zero * docs: remove upload limits from Cloudflare setup guide --------- |
||
|
|
65420320e7 |
feat(ai-assistant): odt support, document-to-lesson attach, upload UX fixes
- Add .odt (application/vnd.oasis.opendocument.text) as a supported upload format; extract text via JSZip + content.xml parsing - Add attach_document_to_lesson agent tool so the AI can attach an uploaded document to a lesson as a downloadable material - Instruct the agent to create a "Course Materials" lesson (first lesson of the first section) and attach the uploaded document to it on implementation - Suppress hallucinated references: uploaded documents do not count as fetched URLs — omit the References section when no fetch_documentation_url calls were made - Surface specific upload error snackbars (file_too_large, unsupported_file_type) instead of silently logging; fix generic fallback message - Allow typing in the chat textarea while a document is uploading; only block Enter/send, not the whole input - Fix retry endpoint to return the current run instead of erroring when the run is already active (queued/running/waiting_for_input/paused) - Connect @cio/core Redis singleton in the jobs bootstrap so course-generation workers no longer fail with "The client is closed" - Expose documentAssets (assetId + fileName) alongside documentText in AgentContext and pass it through both the chat route and course runner |
||
|
|
513ab5e81f |
fix(hls): scope cookie name per-asset to prevent multi-video 403s
When a lesson has two or more uploaded videos each calls mintHlsCookie,
but all wrote to the same `cio_hls` cookie name. The last write won,
leaving every other video with a mismatched token and a 403 from the
CF Worker.
Cookie name is now `cio_hls_${assetId}` so each video owns its own
cookie. The tenant-router Worker, the Hono HLS fallback route, and the
transcripts route all read the per-asset name.
|
||
|
|
55abf982ed |
fix(question-types): realign registry ids with the question_type table
The registry hardcoded numeric ids that drifted from the database `question_type` table: MATCHING and HOTSPOT were inserted mid-list (ids 9 and 11) but never seeded, shifting ORDERING/LINK/WORD_BANK/ STAR/VIDEO_RECORDING one or two ids past their real rows. Every id emitter inherited the wrong values — the exercise editor, the AI question generator, and the grading/scoring gate — so e.g. an ORDERING question was stored as id 10 (LINK in the DB), came back as a manual type, and never auto-graded. - Move MATCHING/HOTSPOT to the end (ids 14/15) and flag them `disabled`, restoring the append-only id convention and realigning ORDERING=9, LINK=10, WORD_BANK=11, STAR=12, VIDEO_RECORDING=13 with the DB. - Add a registry test asserting every enabled type's id matches the canonical `question_type` rows and disabled types stay past the live range, so ids can't silently drift again. - Constrain the agent tool schema to enabled, DB-backed ids so the model can't emit a disabled id with no row. - Drop disabled types from the teacher prompt's type list and guidance. - Centralize the dashboard question-type picker on the `disabled` flag. This also fixes video-recording detection, which compared against the stale id 15 instead of the real 13. |
||
|
|
f18e5600f6 |
fix(media): prevent attaching an asset to a target twice
Drop `position` from the `asset_usages` unique constraint so the same asset can't be attached to one target/slot more than once, and update the insert's onConflict target + lookup to match the narrower key. Guard it at the service layer too: attachAssetService now rejects an attach when the asset already has a usage for that target with a new ASSET_ALREADY_ATTACHED (409) error, backed by an assetUsageExistsForTarget query. |
||
|
|
08ea608f94 |
feat(media): delete videos from the media library
Add an admin flow to permanently delete a media asset and clean up everything it leaves behind. - Delete dialog with a destructive, irreversible confirmation; deletion is blocked while the video is still attached and shows the in-use lessons so the admin can detach first. - Usage list resolves lesson/course context for each attachment, links to the lesson editor via an anchor (right-click "open in new tab"), and exposes a refresh button to re-check usage after detaching elsewhere. - Storage cleanup runs as a background maintenance job: the producer captures the asset's object keys/prefixes up front and an idempotent worker sweeps HLS segments, thumbnails, audio, transcript, and the raw upload from object storage. Also harden usage accounting so stale attachments cannot trap an asset: - asset_usages has no FK to its polymorphic target, so prune a lesson's usage rows whenever the lesson is hard-deleted (single + bulk paths). - Usage resolution drops orphaned lesson usages (lesson gone or course soft-deleted) so they neither appear in the list nor block deletion. Document the icon-only button convention (variant="secondary") in AGENTS.md. |
||
|
|
a695c97eda |
feat(course): reset student course progress (#671)
* feat(course): add reset student course progress action Give course admins and tutors a destructive reset-progress flow on the student people detail page. The backend clears learner progress for one (courseId, memberId) pair in a single transaction while keeping the groupmember enrolled, and logs an audit summary. The dashboard shows a confirmation dialog with impact counts from analytics before deleting. Co-authored-by: Rotimi Best <[email protected]> * fix(i18n): add localized reset progress strings for all locales Co-authored-by: Rotimi Best <[email protected]> * fix(course): scope reset progress to course data only Stop deleting org-level community questions and answers during course progress reset. Fix course newsfeed deletion order so comments on a student's posts are removed before the posts themselves. Restrict reset to student members, move analytics loading to the person detail layout, and rename impact copy to course newsfeed only. Co-authored-by: Rotimi Best <[email protected]> * fix(course): address Greptile review on reset progress Make progressImpact opt-in via includeProgressImpact query param so analytics requests avoid extra DB work unless the reset dialog needs it. Count only student-authored course newsfeed posts and comments in the impact summary to avoid double-counting replies on the student's posts. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]> |
||
|
|
6d594b4296 |
feat: harden self-hosting + add pull-based deploy, Coolify/Railway (#666)
* feat: enhance self-hosting support and CI/CD workflows - Updated .env.example to clarify auth secret generation and self-hosting mode. - Added CI workflow for build and lint checks on PRs and main branch pushes. - Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support. - Enhanced upload-openapi-spec workflow with caching for dependencies. - Removed obsolete Dockerfile for API service and added jobs worker to Docker setup. - Updated self-host documentation to reflect new jobs worker requirement. - Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation. - Enhanced docker-compose.yaml with health checks and jobs worker configuration. - Updated docker-push.sh to support multi-architecture builds and improved logging. - Revised documentation for publishing images and self-hosting to include new features and requirements. * feat(self-host): safe versioned releases + pull-based image pipeline Builds on the pull-based images compose (docker-compose.images.yaml) to make pulling pre-built images safe for self-hosters: - docker-publish: stable-releases + edge tag policy. Git tag vX.Y.Z publishes X.Y.Z/X.Y/X/latest; a push to main publishes only :edge (latest never moves on a routine merge). - Pre-publish smoke-test gate: each image is booted against ephemeral postgres+redis and healthchecked before any push (docker/smoke-test.sh). - CIO_VERSION pinning in .env.example; run-docker-full-stack.sh --images flag to pull instead of build. - Docs: SELF_HOST pull-from-images + versioning/upgrade/rollback sections, PUBLISHING_IMAGES tag policy + pnpm release flow, self-hosting.mdx quickstart. * feat: enhance self-hosting support and CI/CD workflows - Updated .env.example to clarify auth secret generation and self-hosting mode. - Added CI workflow for build and lint checks on PRs and main branch pushes. - Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support. - Enhanced upload-openapi-spec workflow with caching for dependencies. - Removed obsolete Dockerfile for API service and added jobs worker to Docker setup. - Updated self-host documentation to reflect new jobs worker requirement. - Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation. - Enhanced docker-compose.yaml with health checks and jobs worker configuration. - Updated docker-push.sh to support multi-architecture builds and improved logging. - Revised documentation for publishing images and self-hosting to include new features and requirements. * ci: run build-only on PRs, defer lint until repo-wide format * docs: point Railway template link at the new template URL * fix: address Greptile review + dashboard CI build env - docker-push.sh: only also-tag :latest for real semver releases, so VERSION=edge/main/dev can't clobber the stable latest pointer (P1). - run-docker-full-stack.sh: stop deriving OBJECT_STORAGE_PUBLIC_ENDPOINT from the dashboard origin (broke presigned URLs for videos/documents buckets); derive only the public media base URL and document the S3-API route. - ci.yml: provide PUBLIC_IS_SELFHOSTED at build time (dashboard reads it from SvelteKit's strict $env/static/public); bump cache actions v3 -> v4. - upload-openapi-spec.yml: bump cache actions v3 -> v4. - smoke-test.sh: dashboard gate now fails on 5xx instead of accepting any response. * ci: scope PR build gate to the api + dashboard closures Building every package surfaced pre-existing rot in apps that aren't part of this PR (storybook's missing @lucide/svelte import, course-app prettier debt). The gate's purpose is to verify the two Docker-image apps; the rest are built on PRs by their own Cloudflare Workers builds. Filter to @cio/api... and @cio/dashboard... so the gate covers exactly what it should. * fix(docker-push): make latest_arg always return 0 Use an `if` instead of `is_release_version && printf` so the helper returns 0 even when it emits no tag. A non-zero return inside the $(latest_arg ...) build argument is fragile under set -e (and would abort with inherit_errexit), which would break the edge/main builds the policy explicitly supports. * fix(docker): copy patches/ into jobs image so pnpm install resolves patchedDependencies The jobs Dockerfile ran `pnpm install` without copying patches/, so pnpm failed hashing package.json#pnpm.patchedDependencies (layerchart patch), breaking the self-host full-stack build. api/dashboard already copy patches/. Co-Authored-By: Claude Opus 4.8 <[email protected]> * fix(docker): build jobs worker deps in topological order The jobs Dockerfile built workspace packages with a hand-written list that omitted transitive deps (e.g. @cio/question-types, consumed by @cio/utils), so `tsc` failed with TS2307 during the self-host build. Use the recursive `pnpm -r --filter @cio/jobs-worker... build` like the api/dashboard images, which builds the full dependency closure in the correct order. * refactor(docker): move jobs Dockerfile to docker/Dockerfile.jobs and align dep install Move apps/jobs/Dockerfile -> docker/Dockerfile.jobs so all three image recipes live together in docker/, and update every reference (docker-compose.yaml, docker-push.sh, docker-publish.yml, railway.jobs.json, self-hosting.mdx). Also align its dependency install with the api/dashboard images: replace the hand-maintained list of workspace package.json copies (which silently missed transitive deps like @cio/core / @cio/ai-assistant, leaving their node_modules uninstalled and breaking the build with TS2307) with the proven pnpm-fetch pattern: fetch the full lockfile into a cached store, COPY . ., then an offline filtered install of @cio/jobs-worker... This installs the complete dependency closure regardless of how the workspace graph grows. * refactor(docker): move docker-compose.yaml to repo root for Coolify compatibility Coolify's Docker Compose build pack forces Base Directory to the repo root and resolves build contexts from there, so the compose's `context: ..` (needed when the file lived in docker/) climbed above the repo and failed with `lstat /artifacts/docker: no such file or directory`. Move docker/docker-compose.yaml -> ./docker-compose.yaml and change the build contexts from `..` to `.` so the context is the repo root with no upward path. This works identically for run-docker-full-stack.sh, manual `docker compose up`, and Coolify. Dockerfiles stay referenced as docker/Dockerfile.*. Update the references in run-docker-full-stack.sh and the docs accordingly. The images compose (docker-compose.images.yaml) is unaffected (no build context) and stays in docker/. * Add self-hosting documentation for Coolify and Railway - Created new documentation for deploying ClassroomIO on Coolify using a pull-based Docker Compose stack. - Added comprehensive guide for self-hosting ClassroomIO with Docker, covering both pre-built images and building from source. - Introduced a new meta.json file to organize self-hosting documentation pages. - Developed a Railway deployment guide with environment variable configurations for API, dashboard, and jobs worker. - Updated self-hosting blog post to link directly to the new Railway guide. - Removed obsolete Railway configuration files (railway.api.json, railway.dashboard.json, railway.jobs.json). - Enhanced run-docker-full-stack.sh script to improve MinIO credential generation and service readiness checks. - Updated docker-compose files to enforce strong secret requirements for authentication. - Adjusted self-hosting references in the Docker documentation to include new guides. * fix(docker): point references at root-level docker-compose.images.yaml The pull-based compose file moved to the repo root (alongside docker-compose.yaml). Update IMAGES_COMPOSE_FILE in run-docker-full-stack.sh, the file's own header example, and the docs (docker.mdx curl URL, coolify.mdx path + blob link, SELF_HOST.md relative link + curl URL). * fix(self-host): address review round 2 — drop unused AUTH_BEARER_TOKEN, pull-by-default, doc fixes - Remove AUTH_BEARER_TOKEN everywhere (unused in app code; PRIVATE_SERVER_KEY is the real dashboard<->api key). Simplifies run-docker-full-stack.sh secret generation. - run-docker-full-stack.sh now defaults to PULLING pre-built images; add --build for source. - docs: recommend MinIO, 'a way' (not 'recommended way') to self-host, remove Direct API Access section, trim Common Commands, SMTP vars per-line, fix PRIVATE_SERVER_KEY wording. * feat(db): versioned migrations for self-host with auto-baseline - Regenerate a single clean baseline migration; db:setup now runs drizzle-kit migrate (not push). - Add db:generate / db:migrate / db:baseline scripts. - baselineMigrationsIfNeeded(): adopt an existing (push/supabase) schema into drizzle's journal so migrate won't try to recreate existing objects ("type/table already exists"). No-ops on fresh and already-tracked databases; runs automatically in db:setup before migrate. * ci(openapi): drop stale ci.yml reference in turbo-cache comment ci.yml was removed earlier in this review; the openapi workflow's turbo cache still warms from its own prior runs via restore-keys, so this is a comment-only fix. * fix(db): make baseline squash-safe for migrate-tracked databases baselineMigrationsIfNeeded returned early whenever drizzle.__drizzle_migrations had any rows. After a migration squash, an instance tracked under the old journal (pre-squash hashes) would then let `drizzle-kit migrate` re-run the consolidated baseline — whose journal `when` is newer than every old created_at — and crash on `CREATE TYPE/TABLE "..." already exists` at startup. Now, when history exists but the consolidated baseline hash is absent, insert that hash (marking it already applied) so migrate skips it. Only the baseline entry (idx 0) is reconciled, so genuinely pending future migrations still run. Verified end-to-end against Postgres: reproduces the original crash, confirms the fix no-ops migrate on a pre-squash DB, and confirms a later 0001 migration still applies. Fresh-DB and already-adopted paths unchanged. * fix(docker): add explicit id to pnpm-store cache mounts Railway's Metal builder rejects BuildKit cache mounts that omit an explicit id (it does not default id to target like standard Docker/ BuildKit do), failing the build at parse time with: dockerfile invalid: flag '--mount=type=cache,target=/pnpm/store' is missing an id argument Add id=pnpm-store to all six cache mounts across the api, dashboard, and jobs images. Harmless on Docker Desktop / CI / GitHub Actions, and required for the Railway self-host build path. * fix(docker): drop BuildKit cache mounts for Railway compatibility Railway's builder rejects cache-mount ids not prefixed with its per-service cacheKey, which can't be hardcoded in a Dockerfile shared across CI, local Docker, and three Railway services. Remove the --mount=type=cache flag (keep --store-dir /pnpm/store); the store is baked into the fetch layer and reused by the offline install, so behaviour is unchanged. Supersedes 9c4633f2c. * fix(docker): force LF for shell scripts and Dockerfiles via .gitattributes cio-api crash-looped on Railway with 'exec /app/entrypoint.sh: No such file or directory' because the entrypoint shipped with CRLF endings (Windows checkout, core.autocrlf=true) — the '#!/bin/sh\r' shebang is unresolvable in the container. Add .gitattributes pinning *.sh and Dockerfile* to LF so container-executed files are never CRLF regardless of contributor OS. * fix(db): serialize db-setup with an advisory lock to prevent baseline double-insert Concurrent db:setup runs (e.g. multiple API replicas booting together) could both pass the baseline 'already present?' check and insert duplicate rows into drizzle.__drizzle_migrations, which has no unique constraint on hash. Wrap baseline + migrate in a pg_advisory_lock held on a dedicated connection so concurrent startups serialize: the loser blocks, then sees the baseline applied and no-ops. * docs(railway): update template deploy URL to classroomio-1 --------- Co-authored-by: Rotimi Best <[email protected]> |
||
|
|
f2d7afc590 |
fix(analytics): show per-student last seen from login and session activity (#674)
* fix(analytics): show per-student last seen from login and session activity Return ISO last-seen timestamps from course analytics instead of pre-formatted strings so the dashboard can format them per student. Batch-fetch last seen for all students and include session activity alongside login events for more accurate, distinct values. Co-authored-by: Rotimi Best <[email protected]> * fix(analytics): destructure first row from getLastLogin query results Promise.all returns an array per Drizzle select; nested destructuring is required so loggedInAt and updatedAt are read from row objects. Co-authored-by: Rotimi Best <[email protected]> --------- Co-authored-by: Cursor Agent <[email protected]> Co-authored-by: Rotimi Best <[email protected]> |
||
|
|
81294a47f9 |
feat: live class sessions, CE workflow, and docs (#683)
Adds live-class session scheduling on lessons (join link, date/time, timezone) with automatic 24h and 1h reminder emails plus a calendar invite (.ics), a time-change notification with an updated invite, a teacher-authored welcome email and session invite on enrollment, and a "Notify students" quiz nudge. LMS gets an upcoming live-sessions card and Join buttons, plus a reusable live-session card component (3 states) in @cio/ui with a Storybook story. The sidebar content tree now reflects sequential progression locking like the content list, and the course-completion confetti regression is fixed. Org-based emails no longer reference ClassroomIO by name. Docs add live-class, welcome-email, and course-progression guides, expand the certificates guide (design, signatures, corrected labels), move plan-required callouts to the top, flatten how-to-guides to the docs root with redirects, and add screenshots. Includes an add-docs-image skill. |
||
|
|
bbf02df65b |
fix(core): sanitize optional reward description in course metadata
Use sanitizeOptionalHtml for reward.description now that the DB type allows it to be omitted, fixing @cio/core build failures. Co-authored-by: Cursor <[email protected]> |
||
|
|
52d9bf18c3 |
feat: add sequential progression and video watch lesson completion
Gate student content by completion order, track per-video watch progress with seek enforcement, and auto-complete lessons at the configured threshold. Show ring progress in the lesson header, keep watch progress on rewatch, and sync progression locks client-side so the next item unlocks after completion. |
||
|
|
f615b06bf8 | refactor: update landing page metadata schema | ||
|
|
5f07184b29 | fix: NoSuchKey, The specified key does not exist. | ||
|
|
4eee866e85 |
feat: make video uploads faster
- Implemented a new endpoint to delete failed manual p1080 renditions, restoring the master manifest and updating metadata. - Enhanced the HLS upload process with better error handling and cleanup for aborted uploads. - Introduced tests for HLS encoder functionality to ensure correct selection of rungs based on video source properties. - Updated video upload component to manage reserved asset IDs and perform cleanup on abo |
||
|
|
27a612e92f |
feat: let AI chat read lesson video transcripts on demand
Add get_lesson_transcript (teacher) and read_lesson_transcript (student) tools that fetch a lesson's uploaded-video transcript from media_transcript by the video assetIds. The agents call them only when a question is about the video's spoken content, so text-only chats keep a cacheable prefix. |
||
|
|
48ce4d4d64 |
chore: update queue dashboard authentication and improve course component handling
- Replaced email/token-based authentication with HTTP Basic Auth for the queue dashboard in production. - Updated course components to use courseId instead of courseSlug for better consistency. - Adjusted role-based security checks to utilize isStudentExperience for conditional rendering. - Enhanced view-as-student functionality to redirect correctly based on courseId. |
||
|
|
fa6ca75f67 |
fix(core): declare moved deps (redis, pdf-parse, mammoth, jszip) in @cio/core
These were used by the redis client and document-parsing services that moved from apps/api into packages/core, but the dependencies stayed in apps/api, breaking the @cio/core build with TS2307 module-not-found errors. Move them (and @types/pdf-parse) to where the code now lives. |
||
|
|
d5b07d356e |
feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)
New features: - HLS video encoding/streaming (hls routes, hls-encoder) and a media transcripts pipeline (transcripts route + media-transcript queries) - AI assistant chat vs. run model: agent-course-generation worker/queue, run-monitor pane, chat-changes card, content ask-ai bar - Per-theme course-page previews across org landing-page themes (bold, classic, corporate, editorial, etc.) with a shared course token/component set - View-as-student mode and login-link / link-invite invite flows Refactor: - Extract packages/core consolidating config (env, storage, dashboard-url), services and utils previously duplicated under apps/api/src/config - Remove legacy agent service modules (chat-tools, chat-context, context-window, document, fetch-url, lesson-content) - Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding - Add format script to tenant-router so turbo run format covers it |