Commit Graph
1744 Commits
Author SHA1 Message Date
Rotimi Best 1a44983210 docs: thumbnails skill and humanizer skill 2026-07-27 13:06:43 +01:00
Victor JackandRotimi Best 776c57525b fix: changed empty import path (#783)
* changed empty import path

* org-settings to have main branch code

---------

Co-authored-by: Rotimi Best <[email protected]>
2026-07-24 15:13:54 +01:00
Abdulmuizz HamzatandRotimi Best 7e4a20474b docs: add video walkthrough for DigitalOcean deployment (#782)
Co-authored-by: Rotimi Best <[email protected]>
2026-07-24 14:32:09 +01:00
Victor JackandRotimi Best 7ece82840c Fix: Exercice saving validation error (#774)
Fixed the error that keeps previous deleted questions before saving

Co-authored-by: Rotimi Best <[email protected]>
2026-07-24 14:18:07 +01:00
Victor JackandRotimi Best 726df1f819 docs: updated READMe for jobs env (#773)
* Udated READMe for jobs env

Added instructions to clone the .env.example in the jobs directory to your .env

* corrections to greptile comment

* Greptile code review

---------

Co-authored-by: Rotimi Best <[email protected]>
2026-07-24 14:13:41 +01:00
Victor Jack 05833f441f fix: Chart Circular Dependency (#772)
* Fix: Chart Circular Dependency

* Corrections to greptile reviews

* code rabbit coding guideline

---------
2026-07-24 14:09:09 +01:00
2ceafcc6d1 fix(dashboard): stop ERR_CONTENT_DECODING_FAILED on auth/proxy (#779)
* fix(dashboard): prevent ERR_CONTENT_DECODING_FAILED on API proxy

Node fetch decompresses gzip/br upstream bodies while leaving
Content-Encoding set, so browsers fail auth/proxy responses with
net::ERR_CONTENT_DECODING_FAILED. Request identity encoding and strip
encoding headers from proxied responses. Also allow data: fonts in CSP
and skip UserJot on self-hosted.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): preserve Set-Cookie and use Jest for proxy tests

Copy Set-Cookie via getSetCookie/append so multi-cookie auth responses
are not collapsed on Expires commas. Convert proxy tests to Jest and
add CSP domain normalization coverage.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-23 17:43:52 +01:00
Abdulmuizz HamzatandRotimi Best b599f1a017 chore(docs): migrate the docs site from Fumadocs to Blume (#753)
* 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 <[email protected]>
2026-07-22 22:30:10 +01:00
Rotimi Best 5efd7a38c8 docs: add new engineer onboarding checklist 2026-07-21 02:06:06 +01:00
Rotimi Best a18be030c6 docs(prd): add calendar-invite reuse and internal-audience decisions to workshops PRD
Track prd/workshops in PRD-TRACKER.md.
2026-07-20 15:32:49 +01:00
Rotimi Best b398836c04 docs(prd): mark public-courses and reset-student-course-progress as done 2026-07-20 01:31:13 +01:00
Rotimi Best 55e6ddd37e docs(prd): add automated release process PRD 2026-07-20 01:21:38 +01:00
Rotimi Best 16803bfa3d docs(prd): add workshop assessment and certificate (CE mode) to workshops PRD 2026-07-20 00:59:53 +01:00
Rotimi Best 8e5d6e4bd9 docs(prd): add workshops PRD 2026-07-19 16:44:05 +01:00
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]>
2026-07-19 13:50:12 +01:00
e09b3caa8a fix(i18n): translate untranslated English strings in all locale files (#770)
* fix(i18n): translate untranslated English strings in all locale files

Audited all 9 non-English dashboard locale files against en.json and
found 495 unique keys (2,422 total entries) still containing English
text. Manually translated all user-facing strings across da, de, es,
fr, hi, pl, pt, ru, and vi while preserving ICU plural syntax,
placeholders, and brand names.

Remaining identical strings are intentional: international brand names
(Okta, Instagram), technical acronyms (SSO, API, cURL), URLs,
placeholders, and words that are spelled identically in the target
language (e.g. German Status, French Navigation).

Co-authored-by: Rotimi Best <[email protected]>

* fix(i18n): translate exercise plural labels in kpi_lessons_exercises

Address Greptile review: ICU plural blocks still contained English
'exercise' / 'exercises' across all nine non-English locale files.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-19 12:39:40 +01:00
Rotimi BestandRotimi Best 0822f5f5a3 fix(dashboard): course landing page editor section and currency bugs (#768)
* 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 <[email protected]>

* 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 <[email protected]>

* fix(ui): prefix group class for landing section hover badge

Co-authored-by: Rotimi Best <[email protected]>

* fix(ui): remove unused formatCourseCurrency helper

Co-authored-by: Rotimi Best <[email protected]>

* 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 <[email protected]>

---------
2026-07-19 12:09:38 +01:00
Rotimi BestandRotimi Best 6c3a982daf feat(course): auto-enrol button for paid course join requests (#769)
* feat(course): add auto-enrol flow for paid course join requests

- Extend teacher buy-request email with manual access steps and an
  Auto Enrol this Student button
- Link teachers to the course People page with grantAccess query param
- Add grant-access modal that sends the same org audience invite used
  by the Invite new students flow

Co-authored-by: Rotimi Best <[email protected]>

* fix(i18n): translate grant access modal strings

Replace English placeholders copied into non-en locale files with
proper translations for each supported language.

Co-authored-by: Rotimi Best <[email protected]>

* fix(email): shorten auto-enrol button label per review

Co-authored-by: Rotimi Best <[email protected]>

* fix(email): use American spelling Enroll in auto-enroll CTA

Co-authored-by: Rotimi Best <[email protected]>

* fix(i18n): localize Danish grant-access send invite label

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Rotimi Best <[email protected]>
2026-07-19 12:02:11 +01:00
Rotimi Best 634ffeab0f docs(prd): integration-first revision of forms product PRD
Lock core product decisions: attachment-level binding model with
form_attachment table, create-in-context authoring, purchase-request
pipeline replacing the hardcoded payment modal, per-lesson pulse
feedback, landing-page form and reviews blocks, admin/teacher-only
form creation, and free-standalone/paid-integrations commercial model.
Defer respondent save-and-resume, edit-submitted-response, and
one-question-per-page to phase 2. Add FORM-F1 to FORM-F9 tickets.
2026-07-18 12:17:40 +01:00
Rotimi BestandRotimi Best 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]>
2026-07-16 22:10:13 +01:00
ca3fdd5b6a fix(dashboard): lesson nav build error and add CI build check (#765)
* fix(dashboard): repair lesson nav build and improve locked feedback

Replace stale $isOrgStudent reference with isCourseLearnerView to fix
the Svelte build error. Disable prev/next when the adjacent item is
locked, show lock tooltips, and surface a snackbar if navigation is
blocked (including keyboard shortcuts).

Co-authored-by: Rotimi Best <[email protected]>

* ci: add package build check and AGENTS.md verification steps

Document required dashboard/API build commands for agents and add a
GitHub Actions workflow that runs those builds on PRs touching apps or
packages. Copy dashboard .env.example in CI so vite build has required
public env vars.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-16 12:34:03 +01:00
3ef286ed39 fix(dashboard): use learner view for lesson lock empty states (#764)
Lock checks gated on isOrgStudent missed org-site students where
isStudentExperience is true but org role may be unset. Introduce
isCourseLearnerView (student experience OR org student) and mirror
the exercise page branch structure so locked lessons always show
StudentContentLockedNotice instead of the teacher empty state.

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-16 11:29:59 +01:00
Rotimi Best 7a867dad45 chore(release): student course progress, email preference table, token auth docs & lesson lock fix (#762) 2026-07-16 10:49:01 +01:00
Rotimi Best c35456b821 docs: add token auth guide (#752) 2026-07-16 10:22:04 +01:00
Rotimi Best b7711ffae2 feat(dashboard): show course progress in student sidebar (#761)
* feat(dashboard): show course progress in student sidebar

Adds a progress ring with completed-lesson count to the course sidebar
header, per-section completion percentages in the content tree, and a
"Your progress" footer card with an overall progress bar plus a
lessons/exercises breakdown. All indicators are student-only and derive
from the existing per-item isComplete flags, so they update live when a
lesson is marked complete.

* feat(dashboard): show course progress ring in header on mobile

Extracts the sidebar footer progress card into a reusable
course-progress-card component and adds a course-progress-popover
(progress ring trigger, mirroring the org nav setup ring) to the course
header beside the Assistant button. Shown to students on mobile only,
where the sidebar footer card is hidden; tapping the ring opens a
popover with the linear progress bar and lessons/exercises breakdown.

* fix(dashboard): align progress ring subtitle with ring scope

The header ring percent covers lessons and exercises while the subtitle
only counted lessons, which contradicted each other on courses with
exercises. The subtitle now uses the same all-content counts. Also
drops a duplicate useSidebar import in favor of the namespace export.
2026-07-16 10:19:50 +01:00
f3d1a885b9 feat(notifications): per-org member email preferences table (#751)
* 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 <[email protected]>

* 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 <[email protected]>

* fix(notifications): use Exercise instead of Quiz in toggle copy

Co-authored-by: Rotimi Best <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-16 10:04:28 +01:00
Rotimi BestandRotimi Best 9681770270 fix(dashboard): show locked notice instead of teacher empty state on lessons (#760)
* fix(dashboard): show locked notice instead of teacher empty state on lessons

Students viewing a locked or progression-blocked lesson were falling
through to the teacher empty state with Get Started. Align lesson
access checks with the shared getStudentContentLockReason helper used
by exercises and the course layout, treat missing course content as
inaccessible for students, and use org student role for sidebar lock
blocking on the app host.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): only show lesson lock notice when lock reason is known

Address Greptile review: avoid defaulting to teacher_locked when the
lesson is missing from the content tree, and wait for course/lesson
state before dismissing the student loading spinner.

Co-authored-by: Rotimi Best <[email protected]>

---------

]Co-authored-by: Rotimi Best <[email protected]>
2026-07-16 09:56:35 +01:00
Rotimi Best c9b7c79365 fix(billing): downgrade orgs when Polar subscription is revoked (#759)
* fix(billing): downgrade orgs when Polar subscription is revoked

The Polar webhook handler had subscription.revoked as a no-op, so
subscriptions that expired or failed to renew never got the org's
organization_plan row deactivated, leaving them stuck on a paid plan
indefinitely. Handle subscription.revoked the same way as an inactive
subscription.updated event.

Also add a one-time backfill script (db:downgrade-expired-subscriptions)
that re-checks every currently-active org plan against Polar directly
and downgrades any whose subscription already ended, to fix orgs
affected before this webhook fix shipped.

* Update packages/db/src/scripts/downgrade-expired-subscriptions.ts

---------
2026-07-16 00:57:31 +01:00
Rotimi BestandRotimi Best 3980aa0a9b feat(dashboard): restyle lesson transcript and summarize actions (#758)
* feat(dashboard): restyle lesson transcript and summarize actions

Add separator lines around lesson material actions below the video,
switch transcript and summarize controls to plain ghost buttons with
hover-animated icons, and separate the two actions with a pipe when both
are visible. Display rules are unchanged.

Co-authored-by: Rotimi Best <[email protected]>

* refactor(dashboard): inline summarize action into material actions

Fold lesson-summarize-button into lesson-material-actions and remove the
standalone component. Lessons without videos use the same actions bar with
separators disabled.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): align lesson actions with note width on mobile

Constrain material actions to max-w-2xl when there is no video so they
match the note column. Improve small-screen layout with tighter padding,
flex-wrap, and hide the pipe separator below the sm breakpoint.

Co-authored-by: Rotimi Best <[email protected]>

* add script update

* fix(dashboard): tighten spacing between lesson video and note

Remove gap-2 from the lesson content stack and drop bottom margin on the
last video wrapper so material actions separators control the gap before
the note.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): show bottom separator on note-only lesson actions

Lessons without video were passing showSeparators=false, hiding both
lines. Split into top/bottom controls so summarize still gets a line before
the note without one above the action row.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): always show top separator on lesson material actions

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): reduce lesson material action separator margin to my-2

Co-authored-by: Rotimi Best <[email protected]>

* feat(dashboard): consolidate multi-video transcript actions and navigation

Register transcript sources per upload video, show a single material
actions row after all videos, and add prev/next video navigation in the
transcript side panel when multiple transcripts are available.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): address greptile review on lesson material actions

Require lessonId on material actions and stop overwriting the transcript
seek wrapper with a stale localSeekFn snapshot in the sync effect.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): stop transcript registry effect infinite loop

Replace the reactive sync effect with explicit registry updates from
loadTranscript and skip no-op transcript source writes to avoid reading
and writing transcriptSources in the same effect pass.

Co-authored-by: Rotimi Best <[email protected]>

* fix(ui): raise tooltip z-index above app bar side panel

Use z-menu-elevated for tooltip content so header icon tooltips are not
hidden behind the fixed transcript side panel (z-app-bar).

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Rotimi Best <[email protected]>
2026-07-15 22:28:40 +01:00
Rotimi BestandRotimi Best 656fa4fa7e feat(dashboard): add content page options menu (#757)
* feat(dashboard): add content page options menu

Add a three-dot menu on the course Content page with actions to unlock
all locked lessons/exercises and toggle content grouping. Reuse shared
locked-content collection logic from course settings.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): keep unlock-all enabled and add menu icons

Show a success snackbar when all content is already unlocked instead of
disabling the action. Add lock-open and table-of-contents icons to menu items.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): check courseApi.update return for grouping toggle

Use the update() return value instead of courseApi.success, which can be
false after a successful PUT if the follow-up refresh request fails.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Rotimi Best <[email protected]>
2026-07-15 22:28:23 +01:00
Rotimi BestandRotimi Best 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]>
2026-07-15 03:26:56 +01:00
fdc19aac41 feat(dashboard): add Show transcript and Summarize lesson buttons in lesson view (#748)
* feat(dashboard): add lesson transcript and summarize actions in view mode

Add Show transcript and Summarize lesson outline buttons beneath lesson
videos in view mode. Show transcript opens the existing transcript side
panel when an uploaded video has a transcript. Summarize lesson appears
on every lesson and sends a contextual prompt to the AI assistant.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): localize summarize lesson AI prompts

Move hardcoded summarize prompt strings into translation keys so the
prompt sent to the AI assistant follows the user's app language.

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-13 11:18:28 +01:00
3fbfa4b09f feat(website): update AI builds section headline to company knowledge positioning (#750)
* feat(website): update AI builds section headline and description

Replace 'Your AI Sidekick In Every Course' with positioning focused on
turning company knowledge into courses, matching marketing copy patterns
used elsewhere on the site.

Co-authored-by: Rotimi Best <[email protected]>

* fix(website): remove em dash from AI builds section description

Co-authored-by: Rotimi Best <[email protected]>

* fix(website): remove em dash from chat with builder lane copy

Co-authored-by: Rotimi Best <[email protected]>

* feat(website): update AI builds headline to business outcome positioning

Co-authored-by: Rotimi Best <[email protected]>

* fix(website): accent business outcome in AI builds headline

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-13 10:53:21 +01:00
Rotimi BestandRotimi Best fa060f5f6d feat(notifications): email notification preferences (#747)
* 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 <[email protected]>

* 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 <[email protected]>

* fix(notifications): use Exercise instead of Quiz in toggle copy

Co-authored-by: Rotimi Best <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------
2026-07-13 10:33:19 +01:00
Rotimi Best 14db5363e7 docs: fix railway.mdx steps failing 2026-07-13 02:18:54 +01:00
Rotimi Best 5e8188fbd3 fix: replace stale program references (#749) 2026-07-13 02:09:57 +01:00
Rotimi Best 484e4fb674 docs: replace Railway and Coolify screenshots with compressed WebP
Converted 19 screenshots from PNG to WebP with quality 80, width capped at 1280px.
File sizes reduced from 1.3-1.5 MB each down to 12-25 KB.
Updated all image references in railway.mdx and coolify.mdx accordingly.
2026-07-13 01:40:27 +01:00
Rotimi Best faa8eae011 docs: rename programs references to cohorts 2026-07-13 01:09:01 +01:00
Rotimi Best 54fa0875c8 docs: prd and pp updates 2026-07-13 01:02:15 +01:00
d1deb1be22 fix(dashboard): improve AI chat word wrapping readability (#746)
Replace break-all on chat message prose with break-words so normal text
wraps at word boundaries. Apply overflow-wrap only to inline code and
links, and use horizontal scroll for fenced code blocks.

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-13 00:24:23 +01:00
Rotimi Best 2815a61437 fix(dashboard): fix instructor avatar upload on course landing page (#745)
* fix(dashboard): fix instructor avatar upload on course landing page

The crop dialog was hidden behind the full-screen (z-250) landing-page
editor because bits-ui portals it to <body> at z-modal (200), and every
crop triggered an unbounded loop of POST /media/image requests.

- Add opt-in `elevatedDialog` prop to UploadImage that raises the crop
  dialog above the editor takeover; enable it in the instructor,
  certificate, and review landing-page editor forms.
- Break the upload effect loop in instructor-form by reading `course.id`
  through `untrack`, so the effect no longer re-runs when the setter
  reassigns `course` after each unique upload URL.
- Handle upload failures in instructor-form with a snackbar and always
  reset `isUploading`, so a failed upload no longer leaves the widget
  stuck and silent.

* docs(email): add email notification preferences PRD

Specify a Settings feature letting users enable/disable notification emails at two scopes: personal per-user preferences and org-wide admin defaults. Both gates must allow a send; transactional emails stay mandatory. Grounds storage in organization.settings and a new profile.settings jsonb, with a single preference guard at the email enqueue chokepoint.
2026-07-13 00:01:11 +01:00
Rotimi Best 7439f1f510 fix(dashboard): keep enroll join button visible with loading state (#744)
Logged-in users no longer lose the Join Course button while session
checks, app init, or auto-enrollment run. The button stays visible and
shows a loading spinner instead of being hidden during those flows.
2026-07-12 23:21:23 +01:00
8bd07a524c fix(org): auto-enroll idempotency, signup enrollment, and pending invite gate (#729)
* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

* 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 <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-12 22:46:44 +01:00
Rotimi BestandRotimi Best 7eaff0040c fix(dashboard): make course list rows real links like cards (#743)
Wrap list-view course rows in anchor tags so users can open courses in
new tabs via right-click or modifier-click, matching the grid card layout.
Falls back to button behavior when a custom explore click handler is set.

Co-authored-by: Rotimi Best <[email protected]>
2026-07-12 22:46:25 +01:00
Rotimi BestandRotimi Best 7605395f44 fix(dashboard): respect defaultMode light and track=false for color mode (#742)
* fix(dashboard): respect defaultMode light and track=false for color mode

ModeWatcher defaults userPrefersMode to system and enables OS tracking
before components mount, so prefers-color-scheme won on first paint even
with defaultMode="light" and track={false}.

- Add a blocking init script in app.html that defaults empty storage to
  light before any JS bundle runs
- Disable ModeWatcher head injection to avoid duplicate init scripts
- Keep track={false} on ModeWatcher and sync system tracking only when
  userPrefersMode is explicitly system
- Update the sidebar theme toggle to enable OS tracking only when the
  user selects System

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): ignore stale system color mode until user opts in

mode-watcher leaves mode-watcher-mode as system for anyone who never
picked a theme. ModeWatcher onMount then calls setMode('system'), which
overrides defaultMode="light" and track={false}.

Track an explicit sidebar choice separately and normalize stale system
values to light during the app.html init script and on mount.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): bootstrap color mode before mode-watcher module init

Even with cleared localStorage, mode-watcher's module-level derivedMode
could apply the OS color-scheme before ModeWatcher mounted. Stop relying
on ModeWatcher for document classes and bootstrap light mode from
hooks.client.ts immediately after the app.html script runs.

Co-authored-by: Rotimi Best <[email protected]>

* refactor(dashboard): collapse color-mode workaround onto mode-watcher

The scaffolding (color-mode.svelte.ts state module, color-mode-document.ts,
init-color-mode.client.ts) existed only to work around disableHeadScriptInjection,
which had turned off mode-watcher's own setInitialMode script — the very script
that seeds localStorage before the module's eager "system" write and prevents FOUC.

Let ModeWatcher inject that script again (defaultMode="light") and drive theme
application, tracking, and Sonner theming. Keep only the app.html bootstrap for
the one thing mode-watcher can't do: treat a returning user's stale "system" as
light unless they explicitly chose it. The theme toggle now calls mode-watcher's
setMode directly and marks the choice explicit.

* fix(dashboard): run color-mode bootstrap as external script to satisfy CSP

The inline app.html bootstrap and mode-watcher's own setInitialMode script were
both blocked by the nonce-based CSP (script-src has no 'unsafe-inline'), so
localStorage was never seeded before mode-watcher's eager "system" write — every
fresh load followed the OS and came up dark.

Move the seed + FOUC logic to a same-origin static file (static/theme-init.js),
which 'self' allows without a nonce, and load it as a parser-blocking script in
app.html so it still runs before the app bundle. Disable ModeWatcher's inline
head-script injection since it is CSP-blocked and now redundant, and paint the
root element with var(--background) so the pre-hydration shell never flashes
white in dark mode.

Verified with Playwright across fresh/explicit/stale-system cases in light and
dark OS settings; no theme-related CSP violations remain.

---------

Co-authored-by: Rotimi Best <[email protected]>
2026-07-12 22:07:36 +01:00
Rotimi BestandRotimi Best bce0f582ce fix(dashboard): redirect to course start after auto-enroll (#741)
* fix(dashboard): redirect to course start after auto-enroll

After successful enrollment on the org-site enroll page, verified users
were always sent to /lms because resolvePostEnrollRedirect was hardcoded.
Restore the API redirectTo target (/courses/:id/lessons?next=true) so
students land on their first lesson, with /course/:slug as fallback.

Co-authored-by: Rotimi Best <[email protected]>

* fix(dashboard): match LMS continue path after auto-enroll

Use the same student course continue URL as the LMS home page
(/courses/:id/lessons?next=true) via a shared helper, and navigate with
goto instead of a hard reload or org-site /course/:slug fallback.

Co-authored-by: Rotimi Best <[email protected]>

---------
Co-authored-by: Rotimi Best <[email protected]>
2026-07-12 22:06:40 +01:00
Rotimi Best 8bed9f6b4e refactor(billing): system email + single-query backfill for the student cap (#735)
* refactor(billing): make student-limit email a system email; tighten backfill query

- Fetch free-plan orgs over the student limit in a single query
  (getFreePlanOrganizationsOverStudentLimit) instead of loading all orgs and
  counting each in memory; the backfill script now just fans out admin emails.
- Report a dry-run summary: how many free orgs are over the limit, how many
  with admins, total admin emails, and how many were skipped for having none.
- Reclassify the student-limit email as a system (ClassroomIO-branded) email
  rather than org-branded — it's the platform telling an admin about their
  plan. Drop the `branding` field and org masthead/theme; both callers updated.
- Reword the email to explain why it's sent (new students are being turned
  away) and that it's throttled to once a day, so it doesn't read as spam.
- Document the system-vs-org-branded email distinction in AGENTS.md.

* feat(billing): send student-limit emails on milestone crossings, once each

Stop emailing admins on every blocked attempt. Instead notify once when the
org crosses a student-count milestone:
- 50% of the limit → new studentLimitApproaching email (heads-up).
- 100% (limit reached) → studentLimitReached email.

Crossings are detected in assertStudentCapacityOrThrow on the allowed path
(blocked attempts no longer email). "Already notified" is persisted on
organization.settings.studentLimitNotified so each milestone fires at most
once, ever — surviving restarts, no per-day approximation. The backfill script
uses the same flag, making reruns idempotent (skips already-notified orgs and
marks them after sending). Email copy updated to explain the trigger and that
it's a one-time-per-milestone message.

* fix(email): use z.url() and drop the frequency footnotes on student-limit emails

Address PR review: `upgradeUrl` uses `z.url()`; remove the "once a day / once
per milestone" footnote lines from both the reached and approaching emails.

* set track={false}
2026-07-12 20:16:12 +01:00
7392be04d7 fix(dashboard): stop rendering mode-watcher init script as visible page text (#740)
createInitialModeExpression returns bare JS, not a script tag. Injecting it
via {@html} in svelte:head printed setInitialMode source on customer org
sites. Let ModeWatcher inject the script correctly instead.

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-12 18:28:35 +01:00
8288f157b3 fix(email): include course name in student welcome subject (#739)
* fix(email): include course name in student welcome subject

The studentCourseWelcome template used a static subject, so bulk course
assignments produced identical inbox titles. Resolve the subject from
courseName at send time and sanitize it like other email headers.

Co-authored-by: Rotimi Best <[email protected]>

* fix(email): use course-only subject for student welcome email

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-12 18:04:07 +01:00
51f90bf450 fix(dashboard): make upgrade plan modal mobile responsive (#736)
* fix(dashboard): make upgrade plan modal mobile responsive

- Constrain modal height and enable scrolling on small screens
- Anchor modal near top on mobile instead of vertical centering
- Stack success-state buttons and use full-width cards in the grid
- Reduce pricing card padding and adjust toggle badge on narrow viewports

Co-authored-by: Rotimi Best <[email protected]>

* chore(ui): regenerate output.css for pricing responsive classes

Co-authored-by: Rotimi Best <[email protected]>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-12 17:54:11 +01:00