Commit Graph
705 Commits
Author SHA1 Message Date
Rotimi Best 020f96f2a5 feat(onboarding): remove sell and teach goal options (#908)
Drop sell-online and teach-online from the "What brings you here" list so onboarding only offers employee training, customer courses, and expanding from another platform.
2026-08-15 13:33:19 +01:00
5b63c784b4 fix(course): use distinct shield-check icon for Compliance nav (#906)
* fix(course): use shield-check icon for compliance nav

Certificates and Compliance both used CertificateIcon in the course
sidebar; point Compliance at Lucide's shield-check instead.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): add ShieldCheckIcon moving icon for compliance nav

Replace the Lucide shield-check import with a moving-icons component
matching sibling hover-animation patterns, and use it for Compliance.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(course): use moving BotIcon for AI tutor nav

Switch the course sidebar AI Assistant item from Lucide bot to the
shared moving-icons BotIcon so hover animation matches other nav items.

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>
2026-08-15 12:12:19 +01:00
Victor Jack d48d85df2f fix(api): allow inviting students to compliance courses without a certificate deadline (#840)
* Allow teachers invite students

* Greptile comment fix

* coderabbit comment fix
2026-08-15 10:30:45 +01:00
Richard Kenneth 96bade402a fix: sequential progression bug in course with ordered contents (#889)
* fix: sequential progression bug in course with ordered contents

* test(utils): add course progression tests and unify navigation ordering

Reuse flattenNavigableItems in getOrderedNavigableContent so next/previous
navigation follows the same section-aware order as progression locks.
Add vitest coverage for grouped multi-section flattening and sequential
access rules.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix: address PR review comments for course progression

Simplify grouped flatten ordering, only block progression for lessons and
exercises explicitly, integrate flattenNavigableItems in the sequential access
test, and include @cio/core watch in api:dev:full.

---------
2026-08-14 13:50:59 +01:00
Victor Jack 0be63fe558 changed the checked value from status to isPublished (#882) 2026-08-14 11:02:16 +01:00
Richard Kennethandrichardkenneth 7ba551aa43 fix(dashboard): hide admin callout banner and actions on cohort courses page for students (#895)
* fix(dashboard): hide admin callout banner and actions on cohort courses page for students

* fix(dashboard): update empty state description for student users

---------

Co-authored-by: richardkenneth <richard-kenneth@users.noreply.github.com>
2026-08-14 10:52:13 +01:00
Victor JackandRotimi Best 176949d8c8 feat: add back button on login page to return to landing page (#808)
* Back button in Login page

Modified the back button to have two options, href option and onlick

The onClick bceomes usable when there's no href prop

and the onClick takes you directly to the previous page

While the href takes you to the url you indicated

* added a showBack condition to show the button

* modified the onClick function

* Removed the back button, made the Image act as the back button

* Changed the OAuth place holder

* Update back-button.svelte

* remove f

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
2026-08-12 07:55:19 +01:00
Richard Kennethandrichardkenneth f0695e9d18 fix(dashboard): Certain menus inside of modals are hidden (#875)
* fix: update z-index utility class for Select component across tag category and tutor selection views

* refactor: standardize z-index management using ui:z-menu-elevated, ui:z-app-bar, ui:z-app-bar-elevated and ui:z-modal classes across UI components

---------

Co-authored-by: richardkenneth <richard-kenneth@users.noreply.github.com>
2026-08-12 07:09:54 +01:00
Victor JackandRotimi Best 6537613000 fix: preserve landing page section content when toggled off (#838)
* preserve section content when toggled off

* Update types.ts

* Remove comment on LandingPageLinks enabled property

Removed comment about the default visibility of the section.

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
2026-08-11 14:31:58 +01:00
Victor JackandRotimi Best 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>
2026-08-06 06:24:07 +01:00
Victor JackandRotimi Best 60056a6236 fix: validation of allowing teachers create compliance courses without setting deadline (#778)
* Fixed Validation of allowing teachers create compliance courses without setting deadline

* greptile highlightCertificateSettings decraration suggestion

* greptile multiple error bar showing fix suggestion

* organization race condition

* Delete screenshots/Screenshot 2026-07-22 at 11.52.00 AM.png

* deleted unwanted files

* code review fixes

* Changed AttentionHightlight component to work with just one parameter

created fixed Route Enums, Paths and Sections for

* Kebab-case naming fix

* Changes for code comments

---------

Co-authored-by: Rotimi Best <best@classroomio.com>
2026-08-05 13:28:32 +01:00
6ba746c80b fix(dashboard): ensure @cio/ui output.css exists before build (#835)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-08-05 07:07:19 +01:00
9b9dccc07a feat(dashboard): add last updated at course sort option (#823)
Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-08-04 18:20:25 +01:00
ee4d4e031d fix(course): coerce metadata.discount to number before save (#830)
* fix(course): coerce metadata.discount to number before save

Normalize string discount values from the landing page pricing form and
coerce legacy string metadata on ZCourseUpdate validation so course
settings saves do not fail with a Zod type error.

Fixes classroomio/classroomio#828

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* refactor(utils): add shared toFiniteNumber for metadata discount coercion

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(utils): only coerce string discount values in metadata preprocess

Reject broad Number() coercion on booleans and arrays. String and finite
number discounts are normalized; invalid types are left for Zod to reject.

Addresses Greptile review on #830

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* refactor(dashboard): use writable $derived in pricing form again

Keep toFiniteNumber coercion in setter effect; derived fields stay in
sync when course loads or updates (Svelte 5 writable derived).

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(utils): reject invalid metadata.discount instead of clearing it

Only coerce numeric strings in preprocess; leave invalid strings,
non-finite numbers, and other types for Zod to reject.

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>
2026-08-04 15:24:24 +01:00
Rotimi BestandRotimi Best dc55f936a1 fix(dashboard): persist notification preferences after save (#824)
* fix(dashboard): persist notification preferences after save

Personal notification toggles were reset when a stale GET response
arrived after save. Org toggles also triggered a spurious org save
because change detection compared against stale currentOrg settings,
and the UI reset because currentOrg was not updated after org save.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* refactor(dashboard): send only changed settings key and tidy load guard

The org update endpoint deep-merges settings server-side, so spreading
local settings into the payload risked writing stale values over
unrelated keys. Also drop a redundant load-version bump and let only
the latest in-flight preferences load clear the loading flag.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* refactor(dashboard): hydrate org notification defaults on org id transition

The sync effect read orgHasChanges and savedOrgToggles while writing them
through applyOrgToggleState, causing an extra effect pass after hydration.
/account returns settings together with the org id and role, so gating
hydration on org id transitions is sufficient and also discards unsaved
edits when switching orgs instead of saving them against the wrong org.

Addresses CodeRabbit review on PR #824.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------
2026-08-03 02:18:26 +01:00
Rotimi Best 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

---------
2026-08-02 23:23:18 +01:00
0482790126 feat(ui): add AttachmentList component for lesson file attachments (#822)
* feat(ui): add AttachmentList component for lesson file attachments

Introduce a reusable AttachmentList molecule with view and edit modes.
View mode shows a header with file count and view/download actions; edit
mode supports drag-to-reorder, view, and delete. Includes Storybook
stories and package exports.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): address AttachmentList PR review feedback

- Sync edit-mode file state using full metadata signature, not IDs only
- Clamp formatFileSize unit index and guard non-finite inputs
- Recognize common MIME types for file-type icon styling
- Scope role=list to row container; use dragHandleZone for reordering
- Add reorder label for drag handle accessibility

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): guard AttachmentList against undefined files prop

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): render AttachmentList stories and guard missing props

Use Storybook template snippets and default args so stories mount
correctly. Default mode and labels in the component for autodocs, and
align dragHandleZone items with rendered rows.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* feat(dashboard): use AttachmentList on lesson documents tab

Replace the horizontal DocumentCard grid with AttachmentList in view and
edit modes, including drag-to-reorder in edit. Tighten the list header to
p-2 with xs text, add document mapping helpers, and translate new labels.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(dashboard): reset document upload modal state on close

Clear selected file, success message, and upload progress when the add
document modal closes so reopening starts fresh.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(dashboard): raise PDF viewer above sidebar and AI chat

Use z-modal (200) instead of z-100 so the full-screen PDF overlay stacks
above z-app-bar-elevated (150) course sidebar and z-app-bar (100) AI panel.
Add the missing z-modal utility to dashboard app.css to mirror the UI scale.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* chore: minor changes

* feat(dashboard): show spinner and snackbar on document download

AttachmentList download button now shows the Button loading spinner
immediately while the async download runs. On success, a snackbar confirms
the download; on failure, an error snackbar is shown instead of opening
a new tab.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): address PR review feedback on AttachmentList

- Reject negative byte counts in formatFileSize instead of using Math.abs
- Make AttachmentList mode/files/labels props optional; disable cross-list drops
- Include file name in drag-handle aria-label for screen readers
- Prevent upload modal from closing while an upload is in progress
- Use ICU plural rules for attachment file_count across all locales

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>
2026-08-02 21:34:06 +01:00
Rotimi Best 06c3a7feba feat(ui): Page.SettingsActions for settings save bar (#819)
* feat(ui): add Page.SettingsActions for settings save bar

Introduce a sticky bottom save/discard bar on Page and migrate settings
pages to use it instead of header or inline save buttons. Includes
profile, org, customize-lms, notifications, auth, AI tutor, course
settings, certificates, and cohort settings.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): always show Page.SettingsActions, disable when clean

Keep the settings save bar visible at all times and disable Save and
Discard when hasChanges is false instead of hiding the bar entirely.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): add bottom spacing and shadow to Page.SettingsActions

Wrap the actions card in a sticky container with bottom padding so it
does not sit flush against the viewport edge, and use shadow-md on the
card for clearer elevation.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): stop viewport-sticking Page.SettingsActions on scroll

Remove sticky bottom positioning so the save bar stays at the end of the
page content and scrolls away when viewing upper sections.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): use sticky bottom for Page.SettingsActions dock behavior

Replace the IntersectionObserver fixed/dock toggle that caused layout
thrashing and flipping with native position: sticky; bottom: 0. Move
overflow-x-hidden from Page.Root to Page.Body so sticky is not broken.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): keep Page.SettingsActions sticky and above editor content

Page.Root carried md:overflow-x-auto, which makes overflow-y compute to
auto, turning the root into the sticky bar's nearest scrolling ancestor.
Since the root sizes to its content it never scrolls internally, so the
bar rendered at its natural flow position instead of pinning to the
viewport. Drop the overflow so sticky resolves against the viewport;
horizontal containment stays on Page.Body.

Raise the bar to z-50 so it is not painted over by the editor root,
which is relative z-50. Matching rather than exceeding 50 keeps
body-portaled dialogs and sheets above the bar.

Show the shadow only while the bar is pinned away from its flow
position, tracked with an IntersectionObserver on a 1px sentinel
sibling. Unlike the previous observer this toggles a box-shadow only, so
it cannot feed back into layout.

* fix(ui): tighten settings actions spacing and tab visibility

- Remove mt-6 and lower z-index so popovers sit above the bar
- Wrap sentinel in a single root to avoid extra Page.Root flex gap
- Remove Page.Body mb-4 redundant with settings actions spacing
- Show certificate save bar only on the settings tab
- Raise theme color picker z-index via --picker-z-index

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(dashboard): address PR review feedback for settings actions

- Capture profile/org saved baselines only on initial load, not on edits
- Reset course settings errors with explicit field shape in handleDiscard
- Restore VisitOrgSiteButton on org profile settings header
- Derive AI tutor dirty state from stores directly instead of mirrored state
- Use discard-specific French label and document SettingsActions props

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* docs(ui): clarify Page.Root ownership for Page.SettingsActions

"Last child of Page.Root" is a runtime relationship. Several settings
pages get their Page.Root from a +layout.svelte, so the page file holds
only Page.Header, Page.Body, and Page.SettingsActions. Document that this
is correct and list the pages, since adding a nested Page.Root stacks two
min-height containers and shortens the sticky bar's containing block.

Also document that Page.SettingsActions must never be rendered from
inside Page.Body: overflow-x-hidden makes Page.Body a scroll container
that never scrolls, so a sticky bar in it has no travel.

---------
2026-08-02 09:11:09 +01:00
c01b8dea3c fix(dashboard): use secondary variant for customize design button (#816)
Change the Customize design button on the certificate design page from
primary to secondary styling for better visual hierarchy.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-08-01 19:55:06 +01:00
20e34e29dd feat(exercise): add open-ended thumbs up/down question type (#780)
* feat(ui): add THUMBS question type renderer

- Add seven new renderer files under renderers/thumbs/:
  thumbs-correct.ts, take.svelte, edit.svelte, preview.svelte,
  thumbs-review-toggle.svelte, review.svelte, submission.svelte
- Register THUMBS in renderer-contract.ts with edit/take/preview/submission/review
- Export ThumbsEdit, ThumbsTake, ThumbsPreview, ThumbsSubmission,
  ThumbsReview from exercise-question/index.ts
- Add THUMBS_FIXTURE, four thumbs label keys, and Thumbs story
  in storybook exercise-question files
- Add THUMBS case to question-type-modes.svelte submission builder

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* feat(question-types): add THUMBS contracts, scoring, and seed

Introduce the THUMBS question type (id 14) with boolean answer data,
codec, auto-grading, Zod validation, DB seed/schema docs, and binary
normalize support shared with TRUE_FALSE. Dashboard wiring is deferred.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* feat(dashboard): wire THUMBS question type into exercise UI

Connect THUMBS option sync in edit/view modes, labels, picker icon,
and locale strings so the existing contracts surface in the dashboard.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): polish thumbs question UI and move premium upgrade icon

Widen thumbs Yes/No toggles, inherit selected icon color via custom class,
replace badge-based review correctness with solid fill states and bare
Check/X icons, add a success pill in preview, and place the premium
upgrade icon at the end of the question type select row.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): align thumbs review colors with prototypes

Tune correct/incorrect selected states so review buttons use dark
emerald/red content colors on solid fills, matching the design comps.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): address remaining PR review comments

Use a shared timestamp local in thumbs edit fallback options.
Move border-beam animation-delay after the animation shorthand.
Alias --spacing to --ui-spacing for tw-animate compatibility.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(db): sync question_type sequence before seeding new types

Avoid duplicate key errors when question_type_id_seq lags behind rows
inserted via restore or explicit ids (e.g. THUMBS on restored databases).

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(exercise): resolve thumbs review label ICU crash

Remove {value} ICU placeholder from thumbs.review.correct_answer_label
since labels are resolved at map-build time without context. Append the
dynamic yes/no label in thumbs-review-toggle instead.

Also bind question points through the questionnaire store index to avoid
the Svelte non-reactive property binding warning in edit-mode.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* docs(agent): add THUMBS guidance and sync question type docs

Add shared agent question-type guide helpers in @cio/question-types and
use them across the teacher prompt, add/update_questions tool schemas,
legacy ai-assistant schemas, and MCP exercise tools.

- Document when to use THUMBS (Yes/No sentiment) vs TRUE_FALSE (factual)
- Fix stale MCP question type ids and add THUMBS=14
- Exclude disabled MATCHING/HOTSPOT from enabled type lists

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(thumbs): clarify auto-grading and add editable option labels

THUMBS is auto-graded like TRUE_FALSE; the difference is thumbs UI with
customizable Yes/No-style labels and the teacher selecting the correct
option. Render take/preview/review from question.options labels and add
label inputs in the thumbs edit renderer. Update agent and MCP docs to
stop implying THUMBS is manual or non-gradable.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* chore(mcp): bump version to 0.0.10

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(question-types): make THUMBS open-ended with no correct answer

THUMBS is a sentiment/poll question type, not an auto-graded binary like
TRUE_FALSE. Remove correct-answer UI, scoring, and validation; update agent
and MCP docs accordingly.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(dashboard): stop treating THUMBS as auto-graded in editor validation

Normalize thumbs questions on load/save, show an Open-ended badge instead
of Auto, and only enforce points/correct-answer rules on auto-gradable types.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* revert(dashboard): remove unsolicited open-ended grading badge

THUMBS uses the existing Manual badge like other non-auto-gradable types.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(exercise): show question title validation errors in editor

Display mapped title errors beneath the question title input instead of
only highlighting the question card border. Pass the exercise entity to
mapZodErrorsToTranslations so messages resolve to validations.exercise.*
keys, and use a translated snackbar key for save validation failures.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(mcp): build workspace deps before MCP publish bundle

The MCP esbuild step imports @cio/question-types and @cio/utils dist
outputs. CI failed on a clean checkout because mcp:build only built the
MCP package itself. Build those dependencies first so 0.0.10 can publish.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-07-31 14:59:38 +01:00
0abdfe2102 fix(dashboard): align hero enroll CTA with pricing section (#810)
The hero Enroll button was disabled unless metadata.allowNewStudent was
explicitly true, while the pricing CTA treated unset values as open
(matching the enroll page and API). This left the hero button inert on
courses where enrollment was allowed but the flag was not set.

Also sync pricing CTA disabled state, use activeOrg for paid enroll on
public org-site pages, and keep hero/pricing disabled flags aligned via
alignHeroCtaWithPricing.

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-07-31 14:53:01 +01:00
Nwosu EmmanuelandRotimi Best 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>
2026-07-31 00:26:59 +01:00
Victor JackandRotimi Best 88c4cf03b3 Fixed the embeded code responsiveness (#799)
Co-authored-by: Rotimi Best <best@classroomio.com>
2026-07-31 00:11:25 +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 <best@classroomio.com>
2026-07-24 14:18:07 +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 <rotimi-best@users.noreply.github.com>

* 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 <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>
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 <best@classroomio.com>
2026-07-22 22:30:10 +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 <rotimi-best@users.noreply.github.com>

* 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 <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>
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 <rotimi-best@users.noreply.github.com>

* fix(ui): format course landing page pricing with Intl currency

Use getCourseCurrencyFormatter (matching dashboard getCurrencyFormatter)
so NGN and USD prices render as proper locale currency strings instead of
raw numbers with a currency code suffix.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

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

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* fix(ui): remove unused formatCourseCurrency helper

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* refactor: move currency formatter to @cio/utils

Shared course price formatting belongs in @cio/utils, not the UI
package. Dashboard re-exports the helper to keep existing imports.

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

---------
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 <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>
2026-07-19 12:02:11 +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 <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>
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 <rotimi-best@users.noreply.github.com>

* 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 <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>
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 <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-07-16 11:29:59 +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 <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>
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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

---------

]Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

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

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

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

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

---------

Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
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 <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>
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 <rotimi-best@users.noreply.github.com>

* 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 <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>
2026-07-13 11:18:28 +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 <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>

---------
2026-07-13 10:33:19 +01:00
Rotimi Best 5e8188fbd3 fix: replace stale program references (#749) 2026-07-13 02:09:57 +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 <cursoragent@cursor.com>
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
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 <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>
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 <rotimi-best@users.noreply.github.com>
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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>
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 <rotimi-best@users.noreply.github.com>

* 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 <rotimi-best@users.noreply.github.com>

---------
Co-authored-by: Rotimi Best <rotimi-best@users.noreply.github.com>
2026-07-12 22:06:40 +01:00