Commit Graph
86 Commits
Author SHA1 Message Date
Rotimi BestandRotimi Best 3980aa0a9b feat(dashboard): restyle lesson transcript and summarize actions (#758)
* feat(dashboard): restyle lesson transcript and summarize actions

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

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

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

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

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

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

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

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

* add script update

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

Co-authored-by: Rotimi Best <[email protected]>
2026-07-15 22:28:40 +01:00
Rotimi BestandRotimi Best bfafc685e0 fix(exercise): persist completion policy and improve dev build workflow (#754)
* fix(exercise): persist completion policy and pass threshold on save

buildExerciseUpdateFields accepted completionPolicy and passThreshold in
the validation schema but never mapped them to the database update, so
exercise settings appeared to save while reverting on refresh.

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

* fix(db): build workspace deps before compiling

@cio/db imports @cio/utils/notifications, which only exists after
@cio/utils is built. Building db in isolation failed when utils dist
was missing or stale.

- Add a db prebuild step for question-types, utils, and email
- Clean dist and tsbuildinfo before utils and email builds so
  incremental tsc cannot skip emission after dist was removed

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

* chore(dev): build api workspace deps before api:dev

Run turbo build for @cio/api and @cio/jobs-worker dependencies before
starting the dev servers, so missing dist/ folders do not break startup.

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

* feat(course): improve exercise progression grading UX

---------

Co-authored-by: Rotimi Best <[email protected]>
2026-07-15 03:26:56 +01:00
51f90bf450 fix(dashboard): make upgrade plan modal mobile responsive (#736)
* fix(dashboard): make upgrade plan modal mobile responsive

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

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

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

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-12 17:54:11 +01:00
46feccd0fd fix(dashboard): restore paid course payment modal on landing page (#737)
* fix(dashboard): restore paid course payment modal on landing page

Wire the existing PaymentModal back into the theme-based course landing
page. Paid course CTAs now open the name/email modal and redirect to the
configured payment link instead of navigating to the blocked enroll route.

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

* fix(dashboard): reset payment modal on close without effect loop

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

* docs: add Svelte 5 rerender and effect loop guidance to AGENTS.md

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

* fix(dashboard): avoid double-applying discount on course landing pricing

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-12 12:22:53 +01:00
Rotimi Best 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.
2026-07-12 11:33:04 +01:00
0836d9b5bb refactor(z-index): introduce a shared named z-index scale (#723)
* refactor(z-index): introduce a shared named z-index scale

Replace scattered magic z-index numbers (100, 150, 200, 201, 250) with
named layers (z-app-bar, z-app-bar-elevated, z-modal, z-menu-elevated)
so intent is documented at each call site and future additions insert
predictably instead of colliding.

Defined via Tailwind's @utility directive in two places that mirror
each other: packages/ui/src/index.css (ui:-prefixed, for components
that live in the @cio/ui package) and apps/dashboard/src/app.css
(unprefixed, for dashboard-only files) — a ui:-prefixed custom utility
only compiles if it is also used somewhere under packages/ui/src, so
dashboard-only usages need the unprefixed twin.

Also fixes two pre-existing dropdown/select menus (tags-admin.svelte,
mark-exercise-modal.svelte) that used ui:z-[250], which never actually
compiled to any CSS rule since nothing under packages/ui/src used that
exact class.

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

* fix(ui): safelist the packaged z-index scale so it always compiles

@utility only emits a class when Tailwind's scanner sees that exact
candidate under packages/ui/src, so ui:z-app-bar-elevated and
ui:z-menu-elevated were documented but silently missing from the
compiled @cio/ui CSS since nothing inside the package itself used
them. Safelist all four layers via @source inline() so the shipped
scale always matches what's documented, regardless of which layers
this package happens to reference internally.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-11 08:25:11 +01:00
Cursor AgentandRotimi Best b888725125 fix(sidebar): scope elevated mobile z-index to course sidebar
Remove the global z-150 mobile sheet stacking from Sidebar.Root so LMS
and org sidebars keep the default sheet layer. Course sidebar opts in via
mobileOverlayClass and its existing z-150 class.

Co-authored-by: Rotimi Best <[email protected]>
2026-07-10 07:36:06 +00:00
Cursor AgentandRotimi Best 553c126e3c fix(sidebar): raise mobile sidebar above sticky lesson headers
Restore lesson/exercise page header z-100 stacking and fix the overlap
by lifting the mobile sidebar sheet and overlay to z-150, matching the
desktop course sidebar.

Co-authored-by: Rotimi Best <[email protected]>
2026-07-10 05:15:39 +00:00
Rotimi BestandRotimi Best bce38aa530 fix(dashboard): show course cards in landing page settings preview (#697)
* fix(dashboard): show course cards in landing page settings preview

Fetch public courses for the org and pass them to the theme preview
on the landing page settings screen, matching the editor preview.

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

* refactor(dashboard): skip redundant public course fetches

Add loadPublicCoursesIfNeeded so settings, editor, and live landing
page reuse cached courses for the same org site instead of clearing
and refetching on every mount.

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

* fix(dashboard): always refetch courses on live org landing page

Keep loadPublicCoursesIfNeeded for settings preview and editor only.
The public-facing landing page should fetch fresh on each visit.

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

* feat(ui): add shared empty state for org landing page catalogs

Introduce OrgLandingPageCoursesEmpty using the dashboard Empty pattern
with landing-theme colors. Show it across all ten org landing templates
when no courses are published, and wire translated labels through
buildOrgLandingPageProps.

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

* fix(dashboard): address Greptile review on public courses cache

- Add coursesLoaded prop so empty catalog state waits for fetch
- Deduplicate concurrent public course requests via in-flight promise
- Refresh courses on settings mount; reuse cache in editor only
- Expose invalidatePublicCourses for explicit cache clearing

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

* fix(dashboard): load landing preview courses when org store hydrates

Replace onMount course fetch with a reactive effect keyed on the route
slug and currentOrg.siteName so public courses load after app init.
Mark the fetch as complete in finally so failed requests show the empty
state instead of a permanently blank catalog section.

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

---------
2026-07-06 20:51:17 +01:00
a337363965 fix(ui): respect iframe width and height in org landing embed section (#706)
Embed iframes were placed inside flex column layouts without shrink
protection, so authored width/height attributes could be ignored when the
preview panel was narrower than the iframe. Parse dimensions from pasted
embed code and size a dedicated host wrapper so the iframe renders at the
intended dimensions while still scaling down with max-width: 100%.

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-02 12:39:20 +01:00
Rotimi Best 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
2026-07-01 10:36:53 +01:00
Rotimi Best 8d2f2a6d2d fix(landing-page): render iframe embed codes instead of stripping them
The embed section passed code through SafeHtmlContent which ran DOMPurify
with FORBID_TAGS including 'iframe', silently removing the element.

Replaced SafeHtmlContent with a new sanitizeEmbedHtml() that uses a
whitelist of ALLOWED_TAGS: ['iframe'] and restricts src to https: only,
so Google Forms, YouTube, and other https embeds render while javascript:
sources and non-iframe tags are still blocked.
2026-06-30 09:25:40 +01:00
Rotimi Best 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 <[email protected]>

* docs: cross-link upload limits in README, Railway, Coolify, and Cloudflare guides

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

* 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

---------
2026-06-29 12:48:51 +01:00
Rotimi Best a41cc38b7b fix(ui): deduplicate seek-blocked notification in video player
Replace the per-handler boolean guard with a shared 300ms cooldown
timestamp so all three Plyr seek-enforcement paths (progress-bar
listener, seeking, seeked) produce at most one snackbar per skip
attempt.
2026-06-29 01:36:34 +01:00
Rotimi Best a70f8c5287 fix: hls session ending and user can't watch video 2026-06-28 13:15:37 +01:00
Rotimi Best dff0476ac2 fix(ui): block forward seeks via Plyr seek listener, not after the fact
Clamping currentTime in the seeking/seeked handlers is ignored on progressive
MP4 (the forward seek takes effect immediately), so the must-watch lock leaked
even though the snackbar showed.

Block at the source: a Plyr `seek` listener returns false for targets past the
watched point, which skips Plyr's default seek so the media never moves.
Rewinding and re-seeking up to the watched point stay allowed; the snackbar
fires when a forward skip is blocked. furthestSeconds is now shared between the
listener and the playback tracker. The seeking/seeked clamp is kept as a
backstop for keyboard/programmatic seeks that bypass the progress bar.
2026-06-23 10:12:32 +01:00
Rotimi Best bbdb2e9419 fix(ui): hold must-watch seek lock on progressive (non-HLS) video
The lock clamped currentTime only during the seeking event. HLS defers the
forward seek until the fragment loads, so that clamp caught it; but for
progressive MP4 the forward seek takes effect immediately and the mid-seek
write is ignored, so playback leaked past the watched point (snackbar showed,
no snap-back).

Keep the immediate seeking-time clamp (HLS relies on it to avoid buffering
ahead) and add an authoritative re-clamp in seeked: if the playhead still
landed past the limit, snap back via a backward seek into buffered content,
which holds. Snackbar now fires once per blocked gesture and only when a
clamp actually happens.
2026-06-22 10:52:36 +01:00
Rotimi Best 4e2e02e7e7 Revert "fix(ui): enforce must-watch seek lock after the seek settles"
This reverts commit 9de0615e0f.
2026-06-22 10:42:30 +01:00
Rotimi Best 9de0615e0f fix(ui): enforce must-watch seek lock after the seek settles
Resetting currentTime inside the seeking handler doesn't reliably stick
across sources (HLS re-buffering, emulated YouTube events, coalesced native
seeks), so the snackbar showed but playback continued ahead. The timeupdate
handler then ignored the large forward jump via its delta < 2 guard, making
the skip permanent.

Re-clamp in seeked once the position settles, and add a timeupdate backstop
that snaps back forward jumps (delta >= 2) landing past furthestSeconds.
2026-06-22 10:00:56 +01:00
Rotimi Best 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.
2026-06-22 05:43:48 +01:00
Rotimi Best 219ed2eb92 feat(course): crop cover image to 280x200 on upload
The course cover Upload tab stored the raw file and relied on CSS
object-cover to fake the 280x200 frame at display time. Route uploads
through the existing ImageCropper at a 280/200 aspect ratio so the saved
asset matches the frame. Also teach the cropper UploadTrigger to accept
drag-and-drop so the prior drop-to-upload UX is preserved.
2026-06-16 23:50:55 +01:00
Rotimi Best a3a3c5bf48 fix(landing): render footer grid content for all template variants
The footer grid (brand, socials, columns, CTAs) was only rendered for
the 'classic' variant. For 7 other variants (minimal, bold, saas, tech,
studio, corporate, terminal) the grid content was defined as a snippet
but never invoked, silently dropping social links and other footer
content.
2026-06-16 23:49:33 +01:00
Rotimi Best f35fba36f6 fix(landing): remove excess opacity from studio course card preview image 2026-06-16 23:47:19 +01:00
Rotimi Best 0225614356 fix(ui): keep hovered active underline tab text legible in dark mode
The active tab's data-[state=active]:text-foreground rule outranked the
plain text-primary hover class, so hovering the active tab left white
text on the white hover pill (invisible in dark mode). Mark the hover
text-primary as important so the hovered active tab matches the hover
color of inactive tabs.
2026-06-16 23:39:23 +01:00
Rotimi Best 3f4e00bde8 fix(landing): rename catalog 'Programs' labels to 'Courses'
The org landing templates used 'Program(s)' in catalog headings, the
'view all' link, and the studio/terminal hero counters. That collided
with the app's actual Programs feature and confused users into thinking
a landing-page Program meant an app Program. Default copy now says
'Courses'; renamed the programsCount var to coursesCount to match.
2026-06-16 23:25:11 +01:00
Rotimi Best 2350315321 fix(course): make coursesNo and reward description optional; render landing HTML safely
- Allow coursesNo to be omitted in instructor metadata (not required)
- Allow reward description to be omitted when reward object is present
- Replace raw {@html} with SafeHtmlContent in course-pricing,
  course-info-blocks, and embed landing-page components
2026-06-16 02:57:23 +01:00
Rotimi Best 38d912a0b1 docs: add early adopters blog post 2026-06-15 12:03:09 +01:00
Rotimi Best 3e9d8bf3f5 ui: email preview 2026-06-15 08:31:16 +01:00
Rotimi Best 8503e8595e fix(org-site): size section-cap icons in the landing-page editor
The moving-icons family sizes its SVG via a numeric `size` prop and only
forwards `class` to the wrapper div, so the cap's `size-3.5` class never
reached the SVG and icons rendered at the default 24px, overflowing the
cap. Pass size={14} to the cap icon.
2026-06-14 19:07:19 +01:00
Rotimi Best 0c395e9f1b fix(org-site): stabilize landing-page nav auth button
The auth button derived from stores that hydrate in stages, so it
flashed through Login -> Dashboard -> Go to LMS as isLoggedIn and the
role/basePath settled at different times. Gate the label on
appInitApi.isInitializedAndReady so it only renders once the account
(and thus role) has loaded, and show a spinner in the button while a
logged-in user's experience resolves. Add separate loading and disabled
flags to authAction and wire them through all theme navs.
2026-06-14 10:10:14 +01:00
Rotimi Best a411c50526 fix(ui): correct underline tab hover text color
The hovered tab used text-primary-foreground for inactive tabs, which is
near-white — the same token as the hover pill background — so the label
disappeared. Drop the override and keep the hover label on text-primary.
2026-06-14 10:09:55 +01:00
Rotimi Best 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.
2026-06-14 10:09:40 +01:00
Rotimi Best 0975f82c8b fix(widget): powered toggle persistence, copy buttons, version dates, showLessonsCount, color pickers, and loading spinner 2026-06-13 11:24:20 +01:00
Rotimi Best 367ddcddd7 feat(embeds): add embed.classroomio.com public widget host
Serve widgets from short public URLs via tenant-router with R2-backed
assets and runtime API_URL injection, so copy-paste snippets hide infra
paths. Update widget payloads, CSP, upload purge URLs, and dev-only API
inject; document format/stage workflow in AGENTS.md and fix ui-prefix-check
YAML syntax.
2026-06-12 02:06:37 +01:00
Rotimi Best 8fd80e9dc5 fix: ui prefix breaking sidebar 2026-06-12 01:03:17 +01:00
Rotimi Best 899a9f2b51 fix: apply ui: prefix to all packages/ui components and automate it 2026-06-11 23:18:40 +01:00
Rotimi Best bbd243d214 fix: render newsfeed content as html 2026-06-11 16:01:10 +01:00
Rotimi Best 52d9bf18c3 feat: add sequential progression and video watch lesson completion
Gate student content by completion order, track per-video watch progress
with seek enforcement, and auto-complete lessons at the configured
threshold. Show ring progress in the lesson header, keep watch progress
on rewatch, and sync progression locks client-side so the next item
unlocks after completion.
2026-06-11 14:48:34 +01:00
Rotimi Best 8961912530 fix: improve plyr render for youtube video 2026-06-09 22:42:26 +01:00
Rotimi Best cf79b24dc3 fix: restore list markers in lesson editor to match saved prose output
Tailwind's list-style reset stripped bullets and numbers while editing. Mirror dashboard prose list styles in the editor CSS instead.
2026-06-09 22:28:28 +01:00
Rotimi Best d5b07d356e feat: HLS streaming, transcripts, agent course generation & landing-page course previews (#663)
New features:
- HLS video encoding/streaming (hls routes, hls-encoder) and a media
  transcripts pipeline (transcripts route + media-transcript queries)
- AI assistant chat vs. run model: agent-course-generation worker/queue,
  run-monitor pane, chat-changes card, content ask-ai bar
- Per-theme course-page previews across org landing-page themes (bold,
  classic, corporate, editorial, etc.) with a shared course token/component set
- View-as-student mode and login-link / link-invite invite flows

Refactor:
- Extract packages/core consolidating config (env, storage, dashboard-url),
  services and utils previously duplicated under apps/api/src/config
- Remove legacy agent service modules (chat-tools, chat-context,
  context-window, document, fetch-url, lesson-content)
- Relax Cross-Origin-Resource-Policy to allow cross-origin media embedding
- Add format script to tenant-router so turbo run format covers it
2026-06-07 18:43:05 +01:00
Rotimi Best 78ab13f54b feat: preview templates, thumbnail features, 2 new org templates 2026-05-23 12:24:57 +01:00
Rotimi Best 4fdd4115b9 chore: marketing updates and video player improvement 2026-05-22 01:28:18 +01:00
Rotimi Best fbbce6b7c0 fix: ci fixes, tenant router debug, select width improvement 2026-05-21 17:52:28 +01:00
Rotimi Best 6f84002fa5 chore: community page fixes. 2026-05-21 16:38:35 +01:00
Rotimi Best c875f7bf86 chore: use primary color for sidebar active state 2026-05-21 10:42:56 +01:00
Rotimi Best 71fbb2101c feat: auto enroll into org as student and oauth improvements 2026-05-20 08:32:56 +01:00
Rotimi Best 0048f267e2 fix: deployment errors 2026-05-19 04:32:32 +01:00
Rotimi Best ac3a2c14a7 feat: certificates, landing pages, ai tutor, website update, compliance 2026-05-18 03:47:22 +01:00
Rotimi Best c31ad63955 chore: ai, public course and other ui improvements 2026-05-09 06:05:03 +01:00