After email login, auto-enroll was redirecting to /lms or the admin
/courses/:id/lessons route. Unverified users cannot use the LMS yet
(VerifyEmailModal on app routes), which felt like a permission error.
Enroll without verification, but stay on the enroll page with a verify
prompt instead of redirecting until email is confirmed. Also fix custom
domain org lookup to use hostname without backend port.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* 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>
When no custom favicon is set, fall back to the organization logo on
public org-site pages instead of the default ClassroomIO icon.
For OG images, only apply the theme-color background behind the logo
when rendering initials; real logo images now render without the tint.
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(dashboard): show course cards in landing page settings preview
Fetch public courses for the org and pass them to the theme preview
on the landing page settings screen, matching the editor preview.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* refactor(dashboard): skip redundant public course fetches
Add loadPublicCoursesIfNeeded so settings, editor, and live landing
page reuse cached courses for the same org site instead of clearing
and refetching on every mount.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(dashboard): always refetch courses on live org landing page
Keep loadPublicCoursesIfNeeded for settings preview and editor only.
The public-facing landing page should fetch fresh on each visit.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* feat(ui): add shared empty state for org landing page catalogs
Introduce OrgLandingPageCoursesEmpty using the dashboard Empty pattern
with landing-theme colors. Show it across all ten org landing templates
when no courses are published, and wire translated labels through
buildOrgLandingPageProps.
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(dashboard): address Greptile review on public courses cache
- Add coursesLoaded prop so empty catalog state waits for fetch
- Deduplicate concurrent public course requests via in-flight promise
- Refresh courses on settings mount; reuse cache in editor only
- Expose invalidatePublicCourses for explicit cache clearing
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
* fix(dashboard): load landing preview courses when org store hydrates
Replace onMount course fetch with a reactive effect keyed on the route
slug and currentOrg.siteName so public courses load after app init.
Mark the fetch as complete in finally so failed requests show the empty
state instead of a permanently blank catalog section.
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>
- 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
---------
* feat(lms): add student certificates page and sidebar nav
Add a Certificates item to the student LMS sidebar that lists courses
where the student has earned a certificate. Each course card includes a
View Certificate action linking to the existing per-course certificate
page.
* refactor: reuse isOrgOnFreePlan helper from org store
* chore(i18n): add certificates translations for all locales
---------
* 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.
A subdomain like xxazzz.myclassroomio.com set isOrgSite=true with a null
org, and the course page fell back to resolving the org from the course
itself — so courses rendered under subdomains that don't exist. Guard
the (org-site) layout so an unresolved org site 404s instead.
A student opening /org/[slug] on self-hosted stayed on the admin
dashboard. The existing routeUserToNextPage redirect is gated by
shouldRedirectOnAuth (login/signup/onboarding only) and runs once at app
init, so it never caught this route. Add a reactive guard in the admin
org layout that sends students to /lms, gated on
appInitApi.isInitializedAndReady so the role is actually known before
deciding (the student flag defaults to false while loading).
The /org/[slug] routes loaded 250+ JS chunks and, after a deploy, every
chunk missed Cloudflare's edge cache and stampeded the single origin (up
to 48s to serve one static file) — the "keeps loading" lockout.
- tenant-router: opt /_app/immutable/* into the edge cache via
cf.cacheEverything; setting Cache-Control alone never populated it
- course/api: mark the 14 API singletons /* @__PURE__ */ so the barrel
tree-shakes instead of dragging every API into each importer
- courses page: import directly instead of via the feature barrels
Cuts the courses route from 257 to 225 static chunks and keeps static
assets off the origin after the first edge fill.
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
- Updated the CSS classes for the AI assistant bar to adjust positioning based on the new side panel width variable.
- Introduced a new `onWidthPreview` prop in the SidePanelRail component to handle live resizing and update the side panel width dynamically.
- Added width state management in the SidePanelStore to ensure proper width handling during panel interactions.
- Improved layout handling in the course layout component to accommodate the new side panel width adjustments.
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 'tldts' and 'tldts-core' to the Vite SSR noExternal configuration.
- Refactored error handling in +error.svelte and auth-failed/+page.svelte to use a goHome function for navigation instead of href attributes.
- 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.