* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* chore(mcp): bump version to 0.0.10
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(newsfeed): improved the newsfeed ui
* feat(newsfeed): updated the ui
* feat(newsfeed): some improvements
* feat(newsfeed): cleanup
* chore(newsfeed): added translations
* fix(newsfeed): fixing comments and reviews from greptile and coderabbit
* fix(newsfeed): fixing comments and reviews from greptile and coderabbit, improved replies to a reply
* fix(newsfeed): fixing reviews
* fix(ui): expose cancelLabel prop in comment-tree-input and pass localized text
---------
Co-authored-by: Rotimi Best <best@classroomio.com>
* Update documentation for ClassroomIO: change Fumadocs to Blume, revise MDX format conventions, and clarify sidebar navigation requirements.
* docs: update README and scripts for improved preview and error handling
* docs: update Discord link in configuration and documentation
* docs: add token-auth route and update documentation formatting
* Fix formatting in auto-comment-issues.yml
* Fix JSON syntax by adding a comma
* fix(docs): declare Blume's Astro build deps so Cloudflare build resolves them
blume build generates an Astro project under apps/docs/.blume/ that imports
astro, @astrojs/mdx, @tailwindcss/vite, tailwindcss, @tailwindcss/typography,
@shikijs/twoslash and @scalar/astro. Under pnpm's isolated node_modules these
are nested inside blume and not resolvable from the docs app, so a clean
--frozen-lockfile build (Cloudflare) fails at 'Cannot find module astro/config'.
Declare them directly in @cio/docs, pinned to blume's resolved versions.
* refactor(docs): update API reference paths and documentation for clarity
* fix: change platofrm url to docs home page
* Refactor documentation for self-hosting ClassroomIO
- Removed the Supabase Cloud setup guide as it is no longer needed.
- Updated the Enterprise SSO documentation to include a reference link for SSO & licensing.
- Added new documentation for Content Security Policy (CSP) configuration.
- Created a new section for configuring SMTP for email notifications.
- Introduced a comprehensive configuration index for self-hosted ClassroomIO, detailing environment variables and their usage.
- Added documentation for optional integrations including Google OAuth, OpenAI, and Unsplash.
- Documented the generation and usage of required secrets for ClassroomIO.
- Created a section for SSO & licensing, detailing the requirements for Enterprise features.
- Added object storage configuration documentation, covering both bundled MinIO and external S3-compatible services.
- Documented versioning and upgrade processes for ClassroomIO.
- Updated the Coolify and Docker self-hosting guides to streamline configuration references and upgrade instructions.
- Enhanced the Railway self-hosting guide with links to relevant configuration sections.
- Updated token authentication documentation to reference SSO & licensing.
* Add comprehensive guides for ClassroomIO features and functionalities
- Introduced a new guide for the Student Dashboard, detailing navigation and features available to students.
- Added a guide on using Tags for organizing and filtering courses within ClassroomIO.
- Created a guide on how to take attendance in courses, including marking attendance and related functionalities.
- Developed a Terminology guide to clarify key terms used throughout ClassroomIO.
- Implemented a Token Auth guide for signing users into ClassroomIO from external applications using JWT.
- Added instructions for using math in the editor with LaTeX support.
- Created a guide for customizing the welcome email sent to students upon enrollment in courses.
- Added a guide explaining what ClassroomIO is and its key features.
- Updated the home documentation to provide a clearer overview of ClassroomIO and its resources.
- Enhanced self-hosted configuration documentation with a new guide for deploying ClassroomIO on Dokploy.
- Updated links in the self-hosted configuration documentation for better navigation.
- Added new images to support the Dokploy deployment guide.
- Updated asset packaging script to handle redirects for the new guide structure.
- Fixed footer link to point to the correct Getting Started guide.
* chore: uprage version
* feat(docs): add DigitalOcean deployment guide and flatten guides structure
- Add a guide for deploying ClassroomIO on DigitalOcean using Docker Compose
and Caddy for automatic HTTPS.
- List DigitalOcean as a deployment option in the self-hosting index.
- Move guides out of content/docs/guides into content/docs (student-dashboard,
tags, terminology, token-auth, what-is-classroomio, audience, cohorts,
community, course, enterprise-sso, mcp, organization, quickstart/*).
- Rename home.mdx to index.mdx.
- Update package-assets script to emit redirects for the old guides paths.
---------
Co-authored-by: Rotimi Best <best@classroomio.com>
* fix(dashboard): course landing page editor section and currency bugs
- Raise currency select z-index so the dropdown is clickable in the
full-screen editor shell
- Show dashed top/bottom borders and section title on hover and when
active in EditableLandingSection
- Map hero/header section keys in the course editor preview context
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): format course landing page pricing with Intl currency
Use getCourseCurrencyFormatter (matching dashboard getCurrencyFormatter)
so NGN and USD prices render as proper locale currency strings instead of
raw numbers with a currency code suffix.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): prefix group class for landing section hover badge
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(ui): remove unused formatCourseCurrency helper
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor: move currency formatter to @cio/utils
Shared course price formatting belongs in @cio/utils, not the UI
package. Dashboard re-exports the helper to keep existing imports.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
* feat(notifications): add email notification preferences
Implement the email notification preferences PRD with org-level defaults
and personal opt-outs enforced at the email enqueue chokepoint.
- Add toggle map, profile.settings jsonb, and shouldSendEmail resolver
- Extend profile and organization update APIs with emailNotifications
- Pass preference context through all toggleable email send call sites
- Add Settings > Notifications page with personal and org admin sections
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(notifications): group email toggles into course, cohort, and enrollment sections
Organize personal and org notification settings into scannable sections
so users can find course- or cohort-related emails quickly.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(notifications): use Exercise instead of Quiz in toggle copy
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(notifications): only mark certification email sent when enqueued
Skip setMemberCertificationEmailSent when notification preferences or
missing Redis prevent the completion email from being queued, so the
email can still send if preferences change later.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(notifications): address PR review feedback
- Add EmailPreferenceLookupCache to avoid N+1 settings lookups in batch
processors and multi-recipient email enqueue paths
- Replace $effect store hydration in notifications settings with onMount
and explicit sync after save
- Remove thin api email-preferences re-export; import from db queries
- Hide tutor-only personal toggles (newStudent, newSubmission) for students
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(notifications): hydrate toggles after store load and add profile settings migration
- One-time store hydration via onMount plus guarded effect when profile/org
load after mount, without clobbering in-progress edits
- Add drizzle migration for profile.settings jsonb column
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(notifications): add email preferences to LMS settings
Students manage personal email opt-outs from /lms/settings/notifications,
matching the org settings page but scoped to the LMS navigation students use.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(notifications): store per-org personal prefs in organizationmember_email_notifications
- Add organizationmember_email_notifications table keyed by organizationmember
- Expose GET/PUT /organization/member/email-notifications for current org member
- Resolve personal email prefs by profileId + organizationId at send time
- Migrate legacy profile.settings.emailNotifications into per-membership rows
- Update org and LMS settings UI to load/save org-scoped member preferences
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(notifications): address PR review on locales and backfill script
Move profile email-notification backfill out of the migration SQL into
a dedicated db script with dry-run support. Translate personal preference
description strings in all non-English locale files.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(i18n): translate notification preference strings in all locales
Complete settings.notifications and settings.tabs.notifications_tab
translations for da, de, es, fr, hi, pl, pt, ru, and vi.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* feat(course): improve exercise progression grading UX
---------
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(notifications): add email notification preferences
Implement the email notification preferences PRD with org-level defaults
and personal opt-outs enforced at the email enqueue chokepoint.
- Add toggle map, profile.settings jsonb, and shouldSendEmail resolver
- Extend profile and organization update APIs with emailNotifications
- Pass preference context through all toggleable email send call sites
- Add Settings > Notifications page with personal and org admin sections
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(notifications): group email toggles into course, cohort, and enrollment sections
Organize personal and org notification settings into scannable sections
so users can find course- or cohort-related emails quickly.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(notifications): use Exercise instead of Quiz in toggle copy
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(notifications): only mark certification email sent when enqueued
Skip setMemberCertificationEmailSent when notification preferences or
missing Redis prevent the completion email from being queued, so the
email can still send if preferences change later.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(notifications): address PR review feedback
- Add EmailPreferenceLookupCache to avoid N+1 settings lookups in batch
processors and multi-recipient email enqueue paths
- Replace $effect store hydration in notifications settings with onMount
and explicit sync after save
- Remove thin api email-preferences re-export; import from db queries
- Hide tutor-only personal toggles (newStudent, newSubmission) for students
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(notifications): hydrate toggles after store load and add profile settings migration
- One-time store hydration via onMount plus guarded effect when profile/org
load after mount, without clobbering in-progress edits
- Add drizzle migration for profile.settings jsonb column
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
* fix(org): auto-enroll idempotency, pending invites, and tenant enrollment
- Map Postgres unique violations to 409 CONFLICT instead of 500
- Link email-only roster rows before signup policy gates (preserve role)
- Return pendingInvite when an active org invite exists; surface modal globally
- Enroll at signup via create-profile hook when cio-org-id header is present
- Gate client auto-enroll on session orgRoles and pending invite check
- Throw 409 when org email row is linked to a different profile (Greptile)
- Enforce free-plan student cap via assertStudentCapacityOrThrow on new enrolls
- Hide student-only orgs from admin org switcher; redirect students off /org/[slug]
- Route tutor/admin pending-invite accepts on tenant sites to app host
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* perf(dashboard): parallelize account and pending invite fetch on tenant bootstrap
Start GET /account and GET /invite/organization/pending together when a
logged-in user visits a tenant site without session membership. Auto-enroll
still runs sequentially after both complete when no invite is found, then
refetches /account if enrollment succeeds.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix: address PR review — restore AppModeWatcher and drop signup ctx enrollment
Better Auth database hooks do not receive frontend cio-org-id headers in ctx,
so enrollFromSignupOrgContext could never work. Revert to client-side auto-enroll
fallback and restore main's AppModeWatcher in the root layout.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(org): rename auto-enroll-student endpoint to auto-join
POST /organization/auto-join replaces /auto-enroll-student to clarify this
joins org membership, not course enrollment. Renamed service to autoJoinOrg.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* 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.
* fix(dashboard): surface save errors, force public site light mode, global verify modal
- Show snackbar with first validation error when course update fails client-side validation
- Force light mode on public org-site pages; lesson player and dashboard keep theme selector
- Mount VerifyEmailModal in root layout for all authenticated routes; keep trigger=app email guard
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix: instructor editor sync, auto-enroll after auth, email verification API
- Initialize instructor form from org fallbacks and batch metadata updates
- Auto-complete course enrollment after login/signup return from enroll page
- Block new org auto-enroll and course enroll until email is verified
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix: address PR review on instructor setter, color scheme, auto-enroll
- Use existing setter with batched metadata.instructor updates in InstructorForm
- Allowlist public org-site paths so /lms and /courses/:id keep theme selector
- Auto-enroll logged-in users via Better Auth session; remove sessionStorage flow
- Retry enrollment after email verification without duplicate Join Course clicks
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix: allow enrollment without verification; gate dashboard only
- Remove API email verification checks from enroll and auto-enroll flows
- Move VerifyEmailModal to (app) layout so public org-site pages stay usable
- Unverified users can enroll but hit the verification modal in LMS/dashboard
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix: address PR feedback on verify modal, routes, instructor form
- Restore currentOrg.id guard on VerifyEmailModal open condition
- InstructorForm imports currentOrg directly instead of prop drilling
- Move public org-site light-mode route patterns to routes constants
- Reset enrollment attempt key on enroll failure to allow retry
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(org): serve OG images from CDN for Discord embeds
Discord times out on slow dynamic OG generation (~3-5s). Persist generated
images to R2, point meta tags at the media CDN, and warm the API endpoint
on org-site page loads so crawlers get a fast static PNG URL.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* docs: add org-site Open Graph and favicon guides
Document user-facing sharing behavior and contributor architecture
for dynamic OG images, favicon fallback, R2/CDN caching, and embed
cache invalidation.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(org-site): address PR review on OG CDN fallback and docs
- Fall back to API OG URL when CDN object is not yet available (HEAD check)
- Cancel warm-fetch response body to avoid connection pool exhaustion
- Align tenant-router OG edge cache TTL with one-hour API revalidation
- Fix favicon docs navigation and add Custom Domain tab screenshot
- Replace Chiberry Artistry example with AI Academy
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(org-site): accept docker hostnames for OG warm URL
Use isValidHttpUrl for PRIVATE_SERVER_URL so internal values like
http://api:3081 work for SSR warm fetches in Docker Compose.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(org): add dynamic open graph images for school sites
Generate branded 1200x630 OG images for org public sites via GET /org/og/:siteName.png.
Paying cloud orgs include their landing-page tagline; free plans show a Powered by
ClassroomIO watermark. Dashboard org-site meta tags now point at the dynamic image.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(org): enable custom favicon upload and org-site rendering
Allow org admins to upload a favicon from domain settings, persist it via
PUT /organization, and render it on public org sites with logo fallback.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(org): scope custom favicon to public org-site routes only
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(api): inline org-site OG rendering, remove @cio/og-images package
Move template and render helpers into apps/api/src/utils/org-site-og.ts.
Certificates stay in a shared package because they support multiple templates;
org OG is a single API-only concern.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* chore: remove packages/og-images directory
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(api): mount org OG image under /org-site/og
Align with existing org-site public routes (/org-site/course) instead of a
top-level /org prefix.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(plans): consolidate isOrgOnFreePlan into options-based utility
Move orgId guard and self-hosted/plan checks into @cio/utils/plans.
Dashboard and API callers pass plans, isSelfHosted, and optional orgId
directly; remove the dashboard store wrapper.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(org): require orgId for free-plan check and translate favicon strings
Treat missing orgId as not on free plan so nav gating matches prior
behavior when currentOrg is unset. Pass organization.id from OG service.
Add favicon settings translations for all non-English locales.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(dashboard): move meta tag helpers to metaTags.server.ts
Keep PRIVATE_SERVER_URL resolution in a server-only module so dynamic
private env imports cannot leak into universal or client bundles.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(api): return Zod validation errors from public API routes
Public API handlers now surface ZodError details as 400 VALIDATION_ERROR
responses instead of generic 500 INTERNAL_ERROR payloads. Course import
services also rethrow ZodError so validation failures are not wrapped as
server errors.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(utils): centralize course type values without db dependency
Define COURSE_TYPE_VALUES once in @cio/utils/constants and use it for
ZCourseType validation and the Drizzle COURSE_TYPE pgEnum. Removes the
circular @cio/utils -> @cio/db import while keeping course-import schemas
aligned with all four course types including PUBLIC.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(validation): default missing course metadata allowNewStudent to true
Courses stored without allowNewStudent in metadata failed structure export
validation. Align Zod preprocessing with runtime behavior where absent
values mean enrollments are open.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* chore(db): drop unused COURSE_TYPE_VALUES re-export from schema
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(cohorts): rename Programs to Cohorts with data migration
- Add cohort database tables and Drizzle migration alongside legacy program tables
- Introduce migrate-programs-to-cohorts script to copy existing program data safely
- Rename API routes (/cohort), services, queries, validation, and error codes
- Rename dashboard feature module, routes (/cohorts), navigation, and translations
- Update docs, emails, and internal compliance cron endpoints for cohort terminology
- Keep legacy program tables in place for rollback until explicitly removed
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(db): restore migrate:answer-data script alongside cohort migration
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(db): cast program enums when migrating data to cohort tables
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(db): truncate cohort tables before program-to-cohort copy
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(db): report accurate row counts in programs-to-cohorts migration
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(automation): align public API plan gating across cloud and self-hosted
Add shared canUsePublicApi helper so cloud allows Early Adopter and
Enterprise while self-hosted requires Enterprise. Update the API key
dashboard page and backend key creation checks to use the same rules.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* docs(api): document bearer auth on public API reference
Add shared OpenAPI enrichment with bearerAuth security scheme and
authentication instructions. The interactive reference now loads an
enriched spec with Authorize support, and the API guide explains how
to create and use org API keys.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(dashboard): add API quick-start code snippets on automation page
Mirror the MCP client setup section with cURL, JavaScript, and Python
examples that show how to authenticate public API requests with org keys.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* ci(openapi): republish spec when public API OpenAPI helpers change
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(dashboard): add Go snippet to API quick-start examples
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* docs(api): use /org/*/api and clarify dashboard key generation path
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(api): escape backticks in OpenAPI description template literal
Greptile flagged that unescaped backticks around * in
PUBLIC_API_OPENAPI_DESCRIPTION evaluated to NaN and dropped auth docs
from the Scalar spec. Also use plan-specific snackbar message on upgrade.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
- Add Types section to the Filter sidebar with all 4 course types
- Add full-text search input centered in the hero (title + description)
- Add Pricing section (Free / Paid) based on course cost
- Compact hero prop across all 10 themes to halve vertical padding
- Children snippet support in all hero themes for the search input
- All filters are URL-driven (?types=, ?search=, ?tags=, ?pricing=)
- Full-stack: validation schema, DB queries, service, API route, server load, UI
* 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 <rotimi-best@users.noreply.github.com>
* docs: cross-link upload limits in README, Railway, Coolify, and Cloudflare guides
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* 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
---------
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.
* 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 <rotimi-best@users.noreply.github.com>
* fix(i18n): add localized reset progress strings for all locales
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
* 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 <rotimi-best@users.noreply.github.com>
---------
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
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.
Public course slugs carry a -<timestamp> uniqueness suffix that pushes
them past the 80-char ZSlug cap, so the org-site lookup returned a 400
and the page fell through to the landing page instead of redirecting to
the first lesson. The course.slug column is unbounded, so validate the
public-course route param with a no-max ZCourseSlug.
- Allow coursesNo to be omitted in instructor metadata (not required)
- Allow reward description to be omitted when reward object is present
- Replace raw {@html} with SafeHtmlContent in course-pricing,
course-info-blocks, and embed landing-page components
Serve widgets from short public URLs via tenant-router with R2-backed
assets and runtime API_URL injection, so copy-paste snippets hide infra
paths. Update widget payloads, CSP, upload purge URLs, and dev-only API
inject; document format/stage workflow in AGENTS.md and fix ui-prefix-check
YAML syntax.
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.
- Add paginated recommended courses to LMS dashboard (top 3, list
view)
and explore page (12 per page); exclude courses blocking new
students
- Polish course preview modal: remove padding, render requirements as
HTML, round banner image, secondary close button
- Redirect to course start page after successful enrollment instead of
/lms;
keep spinner active until navigation completes
- Auto-enable "allow new students" when a course is published
- Move Explore after My Learning in LMS sidebar nav
- Move Certificates after Content in course sidebar
- Add hiddenColumns prop to CourseListRow; show "Learn more" button
instead of actions menu in LMS explore context
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
- Added support for connecting, refreshing, and removing custom domains through Cloudflare.
- Updated API routes and validation schemas to handle new domain actions.
- Refactored domain management logic in the dashboard to accommodate Cloudflare integration.
- Enhanced user experience with clear status messages and DNS instructions for domain setup.
- Updated translations and UI components to reflect new domain management features.
- Added `AssignCoursesModal`, `AudienceMemberRow`, `AudienceTable`, and related components for improved audience management UI.
- Implemented audience invite actions including resend and revoke functionality.
- Updated audience API to support importing members and assigning courses.
- Refactored audience data handling to include status and invite management.
- Improved overall user experience with pagination and selection features in the audience table.