main
143
Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
4348e89860 |
chore: prepare for deployment
Publish Docker Images / Smoke test (api) (push) Canceled after 0s
Publish Docker Images / Smoke test (dashboard) (push) Canceled after 0s
Publish Docker Images / Smoke test (jobs) (push) Canceled after 0s
Publish Docker Images / build-and-push (./docker/Dockerfile.api, api, api) (push) Canceled after 0s
Publish Docker Images / build-and-push (./docker/Dockerfile.dashboard, dashboard, dashboard) (push) Canceled after 0s
Publish Docker Images / build-and-push (./docker/Dockerfile.jobs, jobs, jobs) (push) Canceled after 0s
MCP package build check / build (push) Canceled after 0s
Package build check / build (push) Canceled after 0s
Upload OpenAPI Spec to R2 / generate-and-upload-openapi (push) Canceled after 0s
|
||
|
|
fcbf38f6a8 | fix: prevent self-hosted auto-enroll from downgrading admins/tutors to students | ||
|
|
2ebb544a81 |
fix(dashboard): cache org stats in Redis with versioned invalidation (#883)
* fix(dashboard): cache org stats in Redis with versioned invalidation Replace the never-expiring process-local cache in the org dashboard page load with API-side Redis caching keyed per org. A version stamp prevents slow reads from writing pre-purge data back into the cache. Invalidate on course create/update/delete, student enroll/remove, certificate claim, and progress reset. Remove the dashboard SSR module cache so every page load reads through the API cache layer. Fixes #852 Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): address PR review on stats cache invalidation - Validate bust via ZDashStats instead of raw query access - Use import type for OrganisationAnalytics - Resolve orgId before course delete so invalidation is reliable - Invalidate immediately after persisted writes, before fallible side effects - Invalidate on member role changes and program course access enrollment Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): complete stats cache invalidation coverage Wire org stats cache purge on lesson completion (manual toggle and video-watch auto-complete) so topCourses completion percentages stay fresh. Harden bulk member adds with sequential inserts and a finally invalidation boundary, and only purge org invite enrollments when new group memberships were actually inserted. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(core): make lesson completion stats purge best-effort Wrap org stats cache invalidation in try/catch so a lookup or Redis failure cannot turn a successful lesson completion into a 500. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): keep org stats cache through lesson completion Lesson complete is a high-frequency event and usually does not change the rounded dashboard percentages. Rely on a 10 minute TTL instead of purging the snapshot on every completion. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * docs: explain how admin dashboard numbers are calculated Add a user-facing guide covering headline counts, top courses, certificates, login activity, and when the 10 minute snapshot refreshes. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> --------- |
||
|
|
9fb75e11d2 |
fix: improve MCP page setup UX and remove automation credit quotas (#821)
* Remove MCP credit quotas and improve MCP setup page - Always show client setup snippets with placeholder keys - Add View Docs link to MCP page header - Remove monthly automation credit enforcement and UI - Update plan feature text to reflect free MCP access Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix: address PR review feedback for MCP page changes - Clear generatedSecret when revoking MCP keys - Use Button link mode for View Docs with rel noopener noreferrer - Update all dashboard locale files for MCP translations - Make MCP rate-limit admission atomic with transactional reservations - Wrap MCP route handlers with withMcpAutomationUsage helper Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * revert: remove atomic MCP rate-limit reservations The transactional advisory-lock reservation was unnecessary complexity for our MCP usage model. Restore the simpler check-then-record flow. Also only clear generatedSecret when revoking the key it belongs to. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix: address remaining MCP page bot review feedback - Add blank lines after confirm guards in key handlers - Skip showing rotated secret if key was revoked during rotation - Fix vi keys_title and de rate_limits_title translations Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix: prevent late key rotation from un-revoking or exposing secrets - Rotate only when revoked_at IS NULL at update time (DB) - Reject rotate requests for already-revoked keys (API) - Ignore stale rotate responses in automation client state - Guard MCP UI against showing secrets for revoked keys Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> --------- |
||
|
|
ad5c8db5a9 | feat(dashboard): moved course completion deadline to course settings page (#842) | ||
|
|
d48d85df2f |
fix(api): allow inviting students to compliance courses without a certificate deadline (#840)
* Allow teachers invite students * Greptile comment fix * coderabbit comment fix |
||
|
|
e67c314c46 | fix(api): return standard error shape for unhandled public API 500s#809 | ||
|
|
a83e172e6d |
fix: show clear error when creating or updating organization site name to an existing one (#793)
* fixed error of not showing the org name API response error * Fixed the error also for organization settings page * modified * Keep field-specific mapping in OrgApi --------- Co-authored-by: Rotimi Best <best@classroomio.com> |
||
|
|
e79d920aeb |
fix(newsfeed): tighten card spacing and reposition pinned icon (#818)
* fix(newsfeed): tighten card spacing and reposition pinned icon - Move pinned icon to top-left corner with a tilted rotation - Remove the extra pin row that added excess top padding - Reduce spacing between post content, reactions, and comments - Remove card box shadow Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(newsfeed): place pinned icon on card corner instead of avatar Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(newsfeed): stop dropdown menus locking body scroll bits-ui defaults DropdownMenu.Content's preventScroll to true, so every open ran a body scroll lock: a forced full-document style recalc and reflow, a horizontal shift by the scrollbar width, and pointer-events: none on the body. On the newsfeed page — a long list of cards each with a scrollable comment thread — that made the comment ellipsis menu feel sluggish to open. Menus don't need scroll locking; only modal layers do. Also tighten card spacing, use the primary color for the pinned icon, give the card menu trigger a pointer cursor, and switch the comment save and submit buttons to the loading and secondary variants. * refactor(newsfeed): store reply targets as data, not markup Replies used to prepend a presentational <blockquote> — Tailwind classes, hardcoded English and a copied snippet of the parent — onto the comment body, and that whole string was persisted to course_newsfeed_comment.content. Editing a reply therefore showed raw markup in the textarea, the quoted snippet froze at write time, the label could never be localized, and two of its ui: utilities never compiled because they appeared only in dashboard code. Add a replyToCommentId column, distinct from parentId (the thread root), so a reply can name the comment it answers. The service verifies the target belongs to the same feed and the same thread; the paginated query joins it to return replyToAuthorFullname, so the label is rendered from live data by the new CommentTree.ReplyingTo component using the existing replying_to key. Rows written the old way still display their stored quote, and stripLegacyReplyQuote keeps that markup out of the edit textarea. * refactor(newsfeed): drop redundant comments on reply target plumbing * fix(newsfeed): address review feedback on pinned icon and quote stripping - The pin icon carried an unprefixed `text-muted-foreground` plus `ui:stroke-primary`, which is emitted nowhere because it appears only in dashboard code, so the outline fell back to the muted colour. Use `ui:text-primary` and let the stroke inherit currentColor. - Give the pinned state an accessible name: the icon is `aria-hidden`, and header.svelte no longer carries an indicator, so screen readers had nothing. - Filter feeds on `isPinned` alone. `is_pinned` is only the database column name; drizzle maps it to `isPinned` and the API never returns the snake_case form, so the `as any` fallback was dead and let the page and card disagree. - `ui:transition-color` is not a Tailwind utility and emitted nothing; the plural `ui:transition-colors` is what applies the transition. - Strip a legacy reply quote only when the blockquote carries the `reply-quote` class the old code always wrote, so a blockquote the author typed is kept. * docs(agents): forbid presentational markup in persisted content columns --------- |
||
|
|
a1c4ee720f |
feat(newsfeed): Improved UI flow for the course newsfeed (#791)
* 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> |
||
|
|
2a53d675f6 |
fix(jobs): survive Redis Cloud maintenance reconnects without crashing API (#775)
* fix(jobs): survive Redis Cloud maintenance reconnects BullMQ duplicates ioredis connections without inheriting error listeners, so Redis Cloud maintenance (ECONNABORTED) crashed the API process. Attach error handlers on duplicates, harden reconnect options for failover, and add API process-level safety nets. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * refactor(jobs): address review feedback on Redis resilience fix - reconnectOnError returns 2 for READONLY/LOADING so the failed command is resent against the newly promoted primary after failover - export prepareRedisConnection and test the real implementation instead of a reimplemented copy; add a no-double-listener regression case - API uncaughtException now reports to Sentry and fails fast (flush + exit 1) so the orchestrator restarts cleanly instead of running in an undefined state Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(api): don't fail-fast on transient Redis errors in process guard End-to-end testing (restarting Redis to simulate Cloud maintenance) revealed node-redis surfaces reconnect failures as an uncaught AggregateError [ECONNREFUSED], which the fail-fast uncaughtException guard turned into a process exit. Classify transient Redis/socket errors (incl. AggregateError sub-errors) and log-and-continue for those; still fail fast on unknown errors. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(api): address PR review on Redis process guards and logging - Fail fast on non-transient unhandledRejection (Greptile) - Filter Sentry OnUncaughtException/OnUnhandledRejection integrations to avoid double-capture; keep custom guards with transient Redis filtering - Make wrapDuplicate idempotent; throttle reconnect/error logs during maintenance - Drop READONLY/LOADING from node-redis transient classifier (command-level only) Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(api): rate-limit transient Redis warnings in process guards Reuse logTransientRedisEvent from core so maintenance bursts do not flood logs from uncaughtException/unhandledRejection handlers. Co-authored-by: Rotimi Best <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> |
||
|
|
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 <rotimi-best@users.noreply.github.com> * 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 <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> |
||
|
|
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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> * fix(email): shorten auto-enrol button label per review Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(email): use American spelling Enroll in auto-enroll CTA Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(i18n): localize Danish grant-access send invite label Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> --------- Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> |
||
|
|
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 <rotimi-best@users.noreply.github.com> * 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 <rotimi-best@users.noreply.github.com> |
||
|
|
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 <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> |
||
|
|
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 <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> |
||
|
|
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 <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> --------- |
||
|
|
5e8188fbd3 | fix: replace stale program references (#749) | ||
|
|
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 <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> |
||
|
|
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}
|
||
|
|
fd8bec3e70 |
feat(billing): enforce free-plan student cap across the app (#733)
* feat(billing): enforce free-plan student cap across the app
Free (BASIC) plan orgs were able to exceed the advertised 20-student
limit because the cap existed only as dashboard cosmetic copy with no
server-side enforcement.
Add a single source of truth (getStudentLimit / STUDENT_LIMITS) and a
central guard (assertStudentCapacityOrThrow) wired into every path that
creates a new student-role org member: self-enrolment, course-invite
acceptance, bulk audience import, and org-invite acceptance. The guard
throws UPGRADE_REQUIRED (403) and notifies org admins by email (throttled
to once per org per day) when the limit is hit.
Surface the limit in the UI: disable the audience import button and the
course new-student invite section with upgrade banners for admins, and
show a "contact your administrator" message on the public enrol page for
students (who cannot upgrade). Expose a studentLimitReached flag from the
public course endpoint so the enrol button can be disabled proactively.
Add a one-time (manual, non-cron) db script,
db:notify-students-over-limit, to email admins of orgs already over the
limit at rollout.
* feat(billing): guard remaining student-creating paths against the cap
The initial cap enforcement missed several service entry points that also
create a new student-role org member. Add the capacity guard to:
- autoEnrollStudent (public org self-join on a tenant site)
- cohort enrollment (bulk group enrollment and single member accept)
- ensureProgramCourseAccess (program/cohort-granted course access)
For the cohort and program paths, only genuinely new members are counted
(existing org members are skipped) so re-enrolling current students is
never falsely blocked. The core path enforces via the shared limit check;
it throws UPGRADE_REQUIRED without the admin email since the queue lives
in the api layer.
* fix(billing): don't show cap block to existing org members on enroll page
The public course-by-slug endpoint is unauthenticated, so the enroll
page's studentLimitReached flag is org-wide and can't tell whether the
current viewer is already an org member. An existing student of a full
BASIC org would see Join disabled even though the backend correctly lets
existing members enroll without consuming a new seat.
Only pre-block anonymous visitors (would-be new joiners). Logged-in users
are governed by the membership-aware backend, which allows existing
members and returns UPGRADE_REQUIRED (surfaced via the enroll onError
mapping) for genuinely new students.
* feat(billing): deliver org plan-limits via /account with an extensible registry
Replace the ad-hoc, per-surface audience fetches (each calling getOrgAudience
just to read a count) with a single, future-proof delivery of org usage +
limits on the /account payload, gated to admin/tutor members (students never
receive limit data; a plain COUNT, no student rows).
Model it generically so future org limits (courses, etc.) drop in without
reshaping the payload or re-plumbing the frontend:
- New packages/utils plan-limits registry: PLAN_LIMIT_RESOURCES, PLAN_LIMITS
(reuses STUDENT_LIMITS), getPlanLimit, and a JSON-safe ResourceUsage /
OrgUsageLimits shape with isResourceLimitReached / resourceLimitRemaining.
- getAccountData attaches a per-org `limits` map for admin/tutor orgs.
- Store selectors currentOrgLimits / studentUsage / isStudentLimitReached;
sidebars, upgrade card, People pages, and invite modals read these instead
of fetching audience rows.
Bring cohort surfaces to parity with course: cohort People page banner and
cohort sidebar People upgrade icon, matching the course equivalents.
Also: close the upgrade modal that opened over the invite modal (drop the
`add` param via openUpgradeModalOver), redesign the sidebar upgrade card
(progress toward the student limit), add a Progress `variant`, and document
the no-complex-expressions-in-object-literals rule in AGENTS.md.
* refactor(billing): address PR review on the limits work
- Dedupe new student profile IDs before the cohort seat check so duplicate
batch entries can't overcount against the cap (Greptile P1).
- Drive the org-sidebar upgrade indicator from a dynamic nav-item `upgrade`
flag (audience config declares `upgradeResource: 'students'`; groups set it
from a `limitsReached` map) instead of a hardcoded path check.
- Collapse the audience Import button into a single button with ternary
`disabled`/`href`.
- Simplify the notify-students-over-limit upgrade link to app.classroomio.com.
- Drop the comment on the org `limits` field.
- Add .greptile/rules.md documenting the manual one-time db/src/scripts
convention (operator-controlled reruns) so idempotency isn't reflagged.
* fix(billing): name the org in the enroll capacity message
Change the enroll-page student-limit message from "This course has reached
its student capacity" to "{orgName} has reached its student capacity", since
the cap is org-wide, not course-specific. Pass orgName from the loaded org on
the page and from the currentOrg store in the enroll onError snackbar.
* style(billing): use PremiumIcon for the Authentication upgrade indicator
Replace the RocketIcon on paid settings sub-items (e.g. Authentication) with
the PremiumIcon used on the Audience item, right-aligned (ml-auto) to match.
|
||
|
|
0b5247bed3 |
fix(org): route admin and tutor invites to app domain (#731)
Team member invite emails and link-invite copy URLs were built with the org tenant host (custom domain or siteName.myclassroomio.com). Admin and tutor invites should always point at the admin dashboard origin (app.classroomio.com in cloud). Student org invites are unchanged. |
||
|
|
c03c08fc84 |
fix(email): make team org invites more transactional (#732)
* fix(email): make team org invites more transactional Use org-branded sender and subject instead of the default "Best from ClassroomIO" marketing identity. Include the inviter name in the body, remove promotional emojis, and thread optional subject overrides through the email job pipeline. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(email): escape invite fields and sanitize email headers Address Greptile review: escape inviter/org/role values in the team invite template, sanitize display names in buildEmailFromName, and strip control characters from dynamic subjects. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(email): use RFC 5322 backslash escaping in display names Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * test(email): add vitest coverage for email header helpers Cover escapeHtml, sanitizeEmailDisplayName, sanitizeEmailSubject, and buildEmailFromName. Collapse repeated whitespace when stripping control characters from header values. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(email): strip all ASCII control chars from email headers 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> |
||
|
|
c0f82d9573 |
fix(auth): sync profile email verification with auth user (#726)
* fix(auth): sync profile email verification with auth user Repair stale profile rows where user.email_verified is true but profile.is_email_verified is false, which blocked the LMS with the verify-email modal while send-verification-email returned EMAIL_IS_ALREADY_VERIFIED. Sync on session login, account fetch, and user update; update the modal to respect session emailVerified and self-heal on that error. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * chore(db): add backfill script for stale profile email verification Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * docs(db): document profile email verification backfill command Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(auth): never demote profile email verification on sync Greptile: self-hosted first signup can verify profile before user.email_verified is set. Sync now only promotes verification. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(auth): promote user.email_verified when profile is verified Self-hosted bootstrap and other flows can set profile.is_email_verified before user.email_verified. Sync and createProfileHook now update both tables when promoting verification. 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> |
||
|
|
2fb6b1c477 |
fix(auth): strip internal port from OAuth redirects on BYOD domains (#728)
* fix(auth): strip internal port from OAuth redirects on BYOD domains When BYOD custom domains proxy auth requests through the dashboard to PRIVATE_SERVER_URL (e.g. http://cio-api-vymh:10000), Node's URL host setter preserves the upstream port. Better Auth then redirects browsers to https://customer.example:10000/... which fails. - Add oauth-proxy-redirect helpers to clear backend ports explicitly - Rewrite inbound auth request URLs and outbound oauth-proxy-callback Location headers - Send hostname-only X-Forwarded-Host from dashboard proxy when port matches backend Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(auth): address Greptile review on oauth port stripping - Only treat PRIVATE_SERVER_URL as an internal backend port - Add test for rewriteAuthRequestUrl when forwarded host still has :10000 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> |
||
|
|
35f5216839 |
fix(org): favicon fallback, OG logo styling, and Discord embed CDN URLs (#714)
* 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> |
||
|
|
22a3e6484b |
fix(org): show org logo for favicon and OG image (#713)
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> |
||
|
|
3d15bc0a01 |
fix(org): OG redesign, public meta URLs, and org-site meta copy (#712)
* 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> * fix(dashboard): allow File favicon in org update before upload Skip favicon from pre-upload Zod validation when a File is provided, matching how avatar uploads bypass ZUpdateOrganization until resolved. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): use public API URL for org OG meta tags Never emit PRIVATE_SERVER_URL in og:image URLs. Prefer PUBLIC_SERVER_URL for crawlers; fall back to same-origin /proxy when unset. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(dashboard): use org landing copy and public OG URLs for org sites Populate og:title, description, and site_name from org landing page hero copy on cloud tenant sites. Build og:image from the tenant origin /proxy path so crawlers never receive internal PRIVATE_SERVER_URL values. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * feat(api): redesign org OG image with logo top-left Replace the saturated gradient layout with a light theme-tinted card: Academy label, org name, and tagline bottom-left; logo top-left; decorative circle top-right; watermark on free plans. 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> |
||
|
|
b8fca607aa |
feat(org): dynamic open graph images for school sites (#709)
* 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> |
||
|
|
0ed0e3864d |
fix(api): return Zod validation errors from public API routes (#708)
* 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> |
||
|
|
efdcfba68d |
feat(api): purge Cloudflare CDN before and after OpenAPI spec upload (#704)
* feat(api): purge Cloudflare CDN before and after OpenAPI spec upload Purge api.cdn.clsrio.com edge cache for openapi-latest and the dated spec URL so deploys are not blocked by the 1h Cache-Control TTL on R2 objects. Uses the same CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID pattern as embed uploads; skips when credentials are unset. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(api): purge OpenAPI CDN cache only after upload Match the embeds upload pattern: update R2 first, then invalidate edge cache so clients refetch the new spec. Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com> * fix(api): treat Cloudflare purge 2xx as success when body is unparseable Avoid false upload failures when the purge API returns 200 with a non-JSON body. Addresses Greptile review on #704. 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> |
||
|
|
ec61bfe7d9 |
feat(cohorts): rename Programs to Cohorts with data migration (#694)
* 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> |
||
|
|
fe9f8c441f |
feat(api): public API plan gating, docs auth, and dashboard snippets (#701)
* 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> |
||
|
|
78a2b85ad3 |
feat(courses): add type, search, and pricing filters to org landing page
- 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 |
||
|
|
a61942feb9 |
feat: configurable upload file size limits via .env (#695)
* feat: make upload file size limits configurable via .env Add UPLOAD_MAX_*_MB environment variables for documents, images, videos, exercise assignment files, AI assistant documents, landing page images, and media thumbnails. API presign routes validate file size server-side; dashboard UI reads the same limits from layout data. * chore: wire upload limit env vars through Docker compose and docs Pass UPLOAD_MAX_*_MB from the root .env into api and dashboard containers in both compose files, document them in .env.example and self-hosting guides. Co-authored-by: Rotimi Best <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 --------- |
||
|
|
65420320e7 |
feat(ai-assistant): odt support, document-to-lesson attach, upload UX fixes
- Add .odt (application/vnd.oasis.opendocument.text) as a supported upload format; extract text via JSZip + content.xml parsing - Add attach_document_to_lesson agent tool so the AI can attach an uploaded document to a lesson as a downloadable material - Instruct the agent to create a "Course Materials" lesson (first lesson of the first section) and attach the uploaded document to it on implementation - Suppress hallucinated references: uploaded documents do not count as fetched URLs — omit the References section when no fetch_documentation_url calls were made - Surface specific upload error snackbars (file_too_large, unsupported_file_type) instead of silently logging; fix generic fallback message - Allow typing in the chat textarea while a document is uploading; only block Enter/send, not the whole input - Fix retry endpoint to return the current run instead of erroring when the run is already active (queued/running/waiting_for_input/paused) - Connect @cio/core Redis singleton in the jobs bootstrap so course-generation workers no longer fail with "The client is closed" - Expose documentAssets (assetId + fileName) alongside documentText in AgentContext and pass it through both the chat route and course runner |
||
|
|
513ab5e81f |
fix(hls): scope cookie name per-asset to prevent multi-video 403s
When a lesson has two or more uploaded videos each calls mintHlsCookie,
but all wrote to the same `cio_hls` cookie name. The last write won,
leaving every other video with a mismatched token and a 403 from the
CF Worker.
Cookie name is now `cio_hls_${assetId}` so each video owns its own
cookie. The tenant-router Worker, the Hono HLS fallback route, and the
transcripts route all read the per-asset name.
|
||
|
|
47b85a9697 | Merge branch 'main' of https://github.com/classroomio/classroomio | ||
|
|
a70f8c5287 | fix: hls session ending and user can't watch video | ||
|
|
e168525ad1 |
fix(api): update openapi upload script env import path (#693)
The env module was moved to @cio/core/config/env but the upload-openapi-spec script still imported from the removed apps/api/src/config/env path, causing CI upload:openapi to fail with ERR_MODULE_NOT_FOUND. Co-authored-by: Rotimi Best <best@classroomio.com> |
||
|
|
a695c97eda |
feat(course): reset student course progress (#671)
* feat(course): add reset student course progress action Give course admins and tutors a destructive reset-progress flow on the student people detail page. The backend clears learner progress for one (courseId, memberId) pair in a single transaction while keeping the groupmember enrolled, and logs an audit summary. The dashboard shows a confirmation dialog with impact counts from analytics before deleting. Co-authored-by: Rotimi Best <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> |
||
|
|
4fe45ac642 |
fix(dashboard): course menu icons, delete scroll, and settings tags auth (#691)
* fix(dashboard): course menu icons, delete scroll, and settings tags auth Remove inconsistent dropdown icons, scroll to settings sections for delete and related actions when already on the settings page, and skip org tag group fetches for non-admin users to avoid ORG_TEAM_NOT_AUTHORIZED errors. * fix(api): allow org members to fetch organization tags Use orgMemberMiddleware for GET /organization/tags instead of restricting reads to admins. Revert the dashboard settings workaround that skipped tag group fetches for non-admin users. --------- |
||
|
|
23dba7392e | Merge branch 'main' of https://github.com/classroomio/classroomio | ||
|
|
50a4f24b7c | fix: encode body of translation updating, reduce p margin in prose | ||
|
|
6d594b4296 |
feat: harden self-hosting + add pull-based deploy, Coolify/Railway (#666)
* feat: enhance self-hosting support and CI/CD workflows - Updated .env.example to clarify auth secret generation and self-hosting mode. - Added CI workflow for build and lint checks on PRs and main branch pushes. - Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support. - Enhanced upload-openapi-spec workflow with caching for dependencies. - Removed obsolete Dockerfile for API service and added jobs worker to Docker setup. - Updated self-host documentation to reflect new jobs worker requirement. - Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation. - Enhanced docker-compose.yaml with health checks and jobs worker configuration. - Updated docker-push.sh to support multi-architecture builds and improved logging. - Revised documentation for publishing images and self-hosting to include new features and requirements. * feat(self-host): safe versioned releases + pull-based image pipeline Builds on the pull-based images compose (docker-compose.images.yaml) to make pulling pre-built images safe for self-hosters: - docker-publish: stable-releases + edge tag policy. Git tag vX.Y.Z publishes X.Y.Z/X.Y/X/latest; a push to main publishes only :edge (latest never moves on a routine merge). - Pre-publish smoke-test gate: each image is booted against ephemeral postgres+redis and healthchecked before any push (docker/smoke-test.sh). - CIO_VERSION pinning in .env.example; run-docker-full-stack.sh --images flag to pull instead of build. - Docs: SELF_HOST pull-from-images + versioning/upgrade/rollback sections, PUBLISHING_IMAGES tag policy + pnpm release flow, self-hosting.mdx quickstart. * feat: enhance self-hosting support and CI/CD workflows - Updated .env.example to clarify auth secret generation and self-hosting mode. - Added CI workflow for build and lint checks on PRs and main branch pushes. - Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support. - Enhanced upload-openapi-spec workflow with caching for dependencies. - Removed obsolete Dockerfile for API service and added jobs worker to Docker setup. - Updated self-host documentation to reflect new jobs worker requirement. - Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation. - Enhanced docker-compose.yaml with health checks and jobs worker configuration. - Updated docker-push.sh to support multi-architecture builds and improved logging. - Revised documentation for publishing images and self-hosting to include new features and requirements. * ci: run build-only on PRs, defer lint until repo-wide format * docs: point Railway template link at the new template URL * fix: address Greptile review + dashboard CI build env - docker-push.sh: only also-tag :latest for real semver releases, so VERSION=edge/main/dev can't clobber the stable latest pointer (P1). - run-docker-full-stack.sh: stop deriving OBJECT_STORAGE_PUBLIC_ENDPOINT from the dashboard origin (broke presigned URLs for videos/documents buckets); derive only the public media base URL and document the S3-API route. - ci.yml: provide PUBLIC_IS_SELFHOSTED at build time (dashboard reads it from SvelteKit's strict $env/static/public); bump cache actions v3 -> v4. - upload-openapi-spec.yml: bump cache actions v3 -> v4. - smoke-test.sh: dashboard gate now fails on 5xx instead of accepting any response. * ci: scope PR build gate to the api + dashboard closures Building every package surfaced pre-existing rot in apps that aren't part of this PR (storybook's missing @lucide/svelte import, course-app prettier debt). The gate's purpose is to verify the two Docker-image apps; the rest are built on PRs by their own Cloudflare Workers builds. Filter to @cio/api... and @cio/dashboard... so the gate covers exactly what it should. * fix(docker-push): make latest_arg always return 0 Use an `if` instead of `is_release_version && printf` so the helper returns 0 even when it emits no tag. A non-zero return inside the $(latest_arg ...) build argument is fragile under set -e (and would abort with inherit_errexit), which would break the edge/main builds the policy explicitly supports. * fix(docker): copy patches/ into jobs image so pnpm install resolves patchedDependencies The jobs Dockerfile ran `pnpm install` without copying patches/, so pnpm failed hashing package.json#pnpm.patchedDependencies (layerchart patch), breaking the self-host full-stack build. api/dashboard already copy patches/. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> * fix(docker): build jobs worker deps in topological order The jobs Dockerfile built workspace packages with a hand-written list that omitted transitive deps (e.g. @cio/question-types, consumed by @cio/utils), so `tsc` failed with TS2307 during the self-host build. Use the recursive `pnpm -r --filter @cio/jobs-worker... build` like the api/dashboard images, which builds the full dependency closure in the correct order. * refactor(docker): move jobs Dockerfile to docker/Dockerfile.jobs and align dep install Move apps/jobs/Dockerfile -> docker/Dockerfile.jobs so all three image recipes live together in docker/, and update every reference (docker-compose.yaml, docker-push.sh, docker-publish.yml, railway.jobs.json, self-hosting.mdx). Also align its dependency install with the api/dashboard images: replace the hand-maintained list of workspace package.json copies (which silently missed transitive deps like @cio/core / @cio/ai-assistant, leaving their node_modules uninstalled and breaking the build with TS2307) with the proven pnpm-fetch pattern: fetch the full lockfile into a cached store, COPY . ., then an offline filtered install of @cio/jobs-worker... This installs the complete dependency closure regardless of how the workspace graph grows. * refactor(docker): move docker-compose.yaml to repo root for Coolify compatibility Coolify's Docker Compose build pack forces Base Directory to the repo root and resolves build contexts from there, so the compose's `context: ..` (needed when the file lived in docker/) climbed above the repo and failed with `lstat /artifacts/docker: no such file or directory`. Move docker/docker-compose.yaml -> ./docker-compose.yaml and change the build contexts from `..` to `.` so the context is the repo root with no upward path. This works identically for run-docker-full-stack.sh, manual `docker compose up`, and Coolify. Dockerfiles stay referenced as docker/Dockerfile.*. Update the references in run-docker-full-stack.sh and the docs accordingly. The images compose (docker-compose.images.yaml) is unaffected (no build context) and stays in docker/. * Add self-hosting documentation for Coolify and Railway - Created new documentation for deploying ClassroomIO on Coolify using a pull-based Docker Compose stack. - Added comprehensive guide for self-hosting ClassroomIO with Docker, covering both pre-built images and building from source. - Introduced a new meta.json file to organize self-hosting documentation pages. - Developed a Railway deployment guide with environment variable configurations for API, dashboard, and jobs worker. - Updated self-hosting blog post to link directly to the new Railway guide. - Removed obsolete Railway configuration files (railway.api.json, railway.dashboard.json, railway.jobs.json). - Enhanced run-docker-full-stack.sh script to improve MinIO credential generation and service readiness checks. - Updated docker-compose files to enforce strong secret requirements for authentication. - Adjusted self-hosting references in the Docker documentation to include new guides. * fix(docker): point references at root-level docker-compose.images.yaml The pull-based compose file moved to the repo root (alongside docker-compose.yaml). Update IMAGES_COMPOSE_FILE in run-docker-full-stack.sh, the file's own header example, and the docs (docker.mdx curl URL, coolify.mdx path + blob link, SELF_HOST.md relative link + curl URL). * fix(self-host): address review round 2 — drop unused AUTH_BEARER_TOKEN, pull-by-default, doc fixes - Remove AUTH_BEARER_TOKEN everywhere (unused in app code; PRIVATE_SERVER_KEY is the real dashboard<->api key). Simplifies run-docker-full-stack.sh secret generation. - run-docker-full-stack.sh now defaults to PULLING pre-built images; add --build for source. - docs: recommend MinIO, 'a way' (not 'recommended way') to self-host, remove Direct API Access section, trim Common Commands, SMTP vars per-line, fix PRIVATE_SERVER_KEY wording. * feat(db): versioned migrations for self-host with auto-baseline - Regenerate a single clean baseline migration; db:setup now runs drizzle-kit migrate (not push). - Add db:generate / db:migrate / db:baseline scripts. - baselineMigrationsIfNeeded(): adopt an existing (push/supabase) schema into drizzle's journal so migrate won't try to recreate existing objects ("type/table already exists"). No-ops on fresh and already-tracked databases; runs automatically in db:setup before migrate. * ci(openapi): drop stale ci.yml reference in turbo-cache comment ci.yml was removed earlier in this review; the openapi workflow's turbo cache still warms from its own prior runs via restore-keys, so this is a comment-only fix. * fix(db): make baseline squash-safe for migrate-tracked databases baselineMigrationsIfNeeded returned early whenever drizzle.__drizzle_migrations had any rows. After a migration squash, an instance tracked under the old journal (pre-squash hashes) would then let `drizzle-kit migrate` re-run the consolidated baseline — whose journal `when` is newer than every old created_at — and crash on `CREATE TYPE/TABLE "..." already exists` at startup. Now, when history exists but the consolidated baseline hash is absent, insert that hash (marking it already applied) so migrate skips it. Only the baseline entry (idx 0) is reconciled, so genuinely pending future migrations still run. Verified end-to-end against Postgres: reproduces the original crash, confirms the fix no-ops migrate on a pre-squash DB, and confirms a later 0001 migration still applies. Fresh-DB and already-adopted paths unchanged. * fix(docker): add explicit id to pnpm-store cache mounts Railway's Metal builder rejects BuildKit cache mounts that omit an explicit id (it does not default id to target like standard Docker/ BuildKit do), failing the build at parse time with: dockerfile invalid: flag '--mount=type=cache,target=/pnpm/store' is missing an id argument Add id=pnpm-store to all six cache mounts across the api, dashboard, and jobs images. Harmless on Docker Desktop / CI / GitHub Actions, and required for the Railway self-host build path. * fix(docker): drop BuildKit cache mounts for Railway compatibility Railway's builder rejects cache-mount ids not prefixed with its per-service cacheKey, which can't be hardcoded in a Dockerfile shared across CI, local Docker, and three Railway services. Remove the --mount=type=cache flag (keep --store-dir /pnpm/store); the store is baked into the fetch layer and reused by the offline install, so behaviour is unchanged. Supersedes 9c4633f2c. * fix(docker): force LF for shell scripts and Dockerfiles via .gitattributes cio-api crash-looped on Railway with 'exec /app/entrypoint.sh: No such file or directory' because the entrypoint shipped with CRLF endings (Windows checkout, core.autocrlf=true) — the '#!/bin/sh\r' shebang is unresolvable in the container. Add .gitattributes pinning *.sh and Dockerfile* to LF so container-executed files are never CRLF regardless of contributor OS. * fix(db): serialize db-setup with an advisory lock to prevent baseline double-insert Concurrent db:setup runs (e.g. multiple API replicas booting together) could both pass the baseline 'already present?' check and insert duplicate rows into drizzle.__drizzle_migrations, which has no unique constraint on hash. Wrap baseline + migrate in a pg_advisory_lock held on a dedicated connection so concurrent startups serialize: the loser blocks, then sees the baseline applied and no-ops. * docs(railway): update template deploy URL to classroomio-1 --------- Co-authored-by: Rotimi Best <best@classroomio.com> |
||
|
|
81294a47f9 |
feat: live class sessions, CE workflow, and docs (#683)
Adds live-class session scheduling on lessons (join link, date/time, timezone) with automatic 24h and 1h reminder emails plus a calendar invite (.ics), a time-change notification with an updated invite, a teacher-authored welcome email and session invite on enrollment, and a "Notify students" quiz nudge. LMS gets an upcoming live-sessions card and Join buttons, plus a reusable live-session card component (3 states) in @cio/ui with a Storybook story. The sidebar content tree now reflects sequential progression locking like the content list, and the course-completion confetti regression is fixed. Org-based emails no longer reference ClassroomIO by name. Docs add live-class, welcome-email, and course-progression guides, expand the certificates guide (design, signatures, corrected labels), move plan-required callouts to the top, flatten how-to-guides to the docs root with redirects, and add screenshots. Includes an add-docs-image skill. |
||
|
|
5e2dcf71f4 |
docs: add dev setup guide and make package scripts cross-platform (Windows) (#664)
* chore: update README with environment setup instructions and demo account credentials fix: replace rm -rf with rimraf in build scripts for cross-platform compatibility fix: add shell option to pnpm command in db-setup script for Windows compatibility chore: update package.json files to use rimraf instead of rm -rf for consistency chore: add rimraf dependency to pnpm-lock.yaml feat: add postinstall script to install lefthook git hooks in a cross-platform manner * docs(qa): add QA test plan, flow checklists, and bug-report workflow Add a repo-native QA execution layer on top of FEATURE_AUDIT.md (the feature map). Test cases live in the repo; bugs live in GitHub Issues, so there is no duplicate tracker to keep in sync. QA system: - FEATURE_AUDIT.md: the feature map - every feature, its user flow, and edge cases with file:line citations. - qa/README.md: the workflow, severity/difficulty rubrics, label setup, and a static feature -> flow coverage map proving all 42 audited features have a test plan. - qa/flows/01..13: per-flow happy-path checklists covering signup & onboarding, course authoring, people/invites/roles, student -> certificate, AI assistant & tutor, programs/attendance, community, org public site, media & import, org settings, public API/MCP, enterprise SSO/licensing, and billing & AI credits. - .github/ISSUE_TEMPLATE/qa-bug.yml: a QA Bug template requiring severity, difficulty, area, repro steps, and a screen recording. Dev tooling: - package.json: add db:studio and docker:build scripts. - packages/db: add db:studio (drizzle-kit studio); fix db:reset on Windows by spawning the pnpm.cmd shim with shell:true (Node's CVE-2024-27980 fix blocks spawning .cmd/.bat without a shell). * chore: update QA bug template and README for clarity and improved workflow * docs: enhance DEV_SETUP_NOTES and README for clarity on prerequisites and email setup chore: update package.json scripts for cross-platform compatibility feat: add cross-platform Prettier check script for pre-commit hook * Remove QA documentation and flow files for ClassroomIO - Deleted the entire QA README.md file which contained the QA process, bug filing guidelines, and feature coverage map. - Removed all flow files (01 to 13) that detailed the QA testing processes for various features including admin signup, course authoring, people management, student learning, AI assistant, programs, community, org public site, media management, org settings, public API, enterprise SSO, billing, and AI credits. * fix: update postinstall script |
||
|
|
c97284f870 |
feat(domains): support apex domains and non-.com TLDs
Loosen the custom-domain regex to accept apex domains (1+ labels) in addition to subdomains; the TLD already allowed any letters. Suppress the CNAME record for apex hostnames since a CNAME is invalid at the zone apex, and surface only the A record there. Drop the frontend apex rejection and the misleading ".com" input suffix, and render the domain-input prefix/suffix addons only when set. |
||
|
|
ee4d4922d6 | fix: return clear error when social auth provider is not configured | ||
|
|
0975f82c8b | fix(widget): powered toggle persistence, copy buttons, version dates, showLessonsCount, color pickers, and loading spinner |