* 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]>
* 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]>
* 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 <[email protected]>
* fix(db): restore migrate:answer-data script alongside cohort migration
Co-authored-by: Rotimi Best <[email protected]>
* fix(db): cast program enums when migrating data to cohort tables
Co-authored-by: Rotimi Best <[email protected]>
* fix(db): truncate cohort tables before program-to-cohort copy
Co-authored-by: Rotimi Best <[email protected]>
* fix(db): report accurate row counts in programs-to-cohorts migration
Co-authored-by: Rotimi Best <[email protected]>
---------
Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
* feat(lms): add student certificates page and sidebar nav
Add a Certificates item to the student LMS sidebar that lists courses
where the student has earned a certificate. Each course card includes a
View Certificate action linking to the existing per-course certificate
page.
* refactor: reuse isOrgOnFreePlan helper from org store
* chore(i18n): add certificates translations for all locales
---------