Commit Graph
1671 Commits
Author SHA1 Message Date
be0085f090 fix(dashboard): save validation feedback, public site light mode, global verify modal (#716)
* fix(dashboard): surface save errors, force public site light mode, global verify modal

- Show snackbar with first validation error when course update fails client-side validation
- Force light mode on public org-site pages; lesson player and dashboard keep theme selector
- Mount VerifyEmailModal in root layout for all authenticated routes; keep trigger=app email guard

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

* fix: instructor editor sync, auto-enroll after auth, email verification API

- Initialize instructor form from org fallbacks and batch metadata updates
- Auto-complete course enrollment after login/signup return from enroll page
- Block new org auto-enroll and course enroll until email is verified

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

* fix: address PR review on instructor setter, color scheme, auto-enroll

- Use existing setter with batched metadata.instructor updates in InstructorForm
- Allowlist public org-site paths so /lms and /courses/:id keep theme selector
- Auto-enroll logged-in users via Better Auth session; remove sessionStorage flow
- Retry enrollment after email verification without duplicate Join Course clicks

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

* fix: allow enrollment without verification; gate dashboard only

- Remove API email verification checks from enroll and auto-enroll flows
- Move VerifyEmailModal to (app) layout so public org-site pages stay usable
- Unverified users can enroll but hit the verification modal in LMS/dashboard

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

* fix: address PR feedback on verify modal, routes, instructor form

- Restore currentOrg.id guard on VerifyEmailModal open condition
- InstructorForm imports currentOrg directly instead of prop drilling
- Move public org-site light-mode route patterns to routes constants
- Reset enrollment attempt key on enroll failure to allow retry

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-10 01:55:13 +01:00
35f5216839 fix(org): favicon fallback, OG logo styling, and Discord embed CDN URLs (#714)
* fix(org): serve OG images from CDN for Discord embeds

Discord times out on slow dynamic OG generation (~3-5s). Persist generated
images to R2, point meta tags at the media CDN, and warm the API endpoint
on org-site page loads so crawlers get a fast static PNG URL.

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

* docs: add org-site Open Graph and favicon guides

Document user-facing sharing behavior and contributor architecture
for dynamic OG images, favicon fallback, R2/CDN caching, and embed
cache invalidation.

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

* fix(org-site): address PR review on OG CDN fallback and docs

- Fall back to API OG URL when CDN object is not yet available (HEAD check)
- Cancel warm-fetch response body to avoid connection pool exhaustion
- Align tenant-router OG edge cache TTL with one-hour API revalidation
- Fix favicon docs navigation and add Custom Domain tab screenshot
- Replace Chiberry Artistry example with AI Academy

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

* fix(org-site): accept docker hostnames for OG warm URL

Use isValidHttpUrl for PRIVATE_SERVER_URL so internal values like
http://api:3081 work for SSR warm fetches in Docker Compose.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-09 13:38:49 +01:00
Abdulmuizz Hamzat bd2585f8fd docs(self-hosted): rework Railway + Coolify self-hosting guides with walkthroughs and explicit env vars (#700) 2026-07-09 10:01:24 +01:00
22a3e6484b fix(org): show org logo for favicon and OG image (#713)
When no custom favicon is set, fall back to the organization logo on
public org-site pages instead of the default ClassroomIO icon.

For OG images, only apply the theme-color background behind the logo
when rendering initials; real logo images now render without the tint.

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-08 03:28:38 +01:00
3d15bc0a01 fix(org): OG redesign, public meta URLs, and org-site meta copy (#712)
* feat(org): add dynamic open graph images for school sites

Generate branded 1200x630 OG images for org public sites via GET /org/og/:siteName.png.
Paying cloud orgs include their landing-page tagline; free plans show a Powered by
ClassroomIO watermark. Dashboard org-site meta tags now point at the dynamic image.

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

* feat(org): enable custom favicon upload and org-site rendering

Allow org admins to upload a favicon from domain settings, persist it via
PUT /organization, and render it on public org sites with logo fallback.

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

* fix(org): scope custom favicon to public org-site routes only

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

* refactor(api): inline org-site OG rendering, remove @cio/og-images package

Move template and render helpers into apps/api/src/utils/org-site-og.ts.
Certificates stay in a shared package because they support multiple templates;
org OG is a single API-only concern.

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

* chore: remove packages/og-images directory

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

* refactor(api): mount org OG image under /org-site/og

Align with existing org-site public routes (/org-site/course) instead of a
top-level /org prefix.

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

* refactor(plans): consolidate isOrgOnFreePlan into options-based utility

Move orgId guard and self-hosted/plan checks into @cio/utils/plans.
Dashboard and API callers pass plans, isSelfHosted, and optional orgId
directly; remove the dashboard store wrapper.

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

* fix(org): require orgId for free-plan check and translate favicon strings

Treat missing orgId as not on free plan so nav gating matches prior
behavior when currentOrg is unset. Pass organization.id from OG service.

Add favicon settings translations for all non-English locales.

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

* refactor(dashboard): move meta tag helpers to metaTags.server.ts

Keep PRIVATE_SERVER_URL resolution in a server-only module so dynamic
private env imports cannot leak into universal or client bundles.

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

* fix(dashboard): allow File favicon in org update before upload

Skip favicon from pre-upload Zod validation when a File is provided,
matching how avatar uploads bypass ZUpdateOrganization until resolved.

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

* fix(dashboard): use public API URL for org OG meta tags

Never emit PRIVATE_SERVER_URL in og:image URLs. Prefer PUBLIC_SERVER_URL
for crawlers; fall back to same-origin /proxy when unset.

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

* fix(dashboard): use org landing copy and public OG URLs for org sites

Populate og:title, description, and site_name from org landing page hero
copy on cloud tenant sites. Build og:image from the tenant origin /proxy
path so crawlers never receive internal PRIVATE_SERVER_URL values.

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

* feat(api): redesign org OG image with logo top-left

Replace the saturated gradient layout with a light theme-tinted card:
Academy label, org name, and tagline bottom-left; logo top-left;
decorative circle top-right; watermark on free plans.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-07 23:33:48 +01:00
b8fca607aa feat(org): dynamic open graph images for school sites (#709)
* feat(org): add dynamic open graph images for school sites

Generate branded 1200x630 OG images for org public sites via GET /org/og/:siteName.png.
Paying cloud orgs include their landing-page tagline; free plans show a Powered by
ClassroomIO watermark. Dashboard org-site meta tags now point at the dynamic image.

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

* feat(org): enable custom favicon upload and org-site rendering

Allow org admins to upload a favicon from domain settings, persist it via
PUT /organization, and render it on public org sites with logo fallback.

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

* fix(org): scope custom favicon to public org-site routes only

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

* refactor(api): inline org-site OG rendering, remove @cio/og-images package

Move template and render helpers into apps/api/src/utils/org-site-og.ts.
Certificates stay in a shared package because they support multiple templates;
org OG is a single API-only concern.

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

* chore: remove packages/og-images directory

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

* refactor(api): mount org OG image under /org-site/og

Align with existing org-site public routes (/org-site/course) instead of a
top-level /org prefix.

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

* refactor(plans): consolidate isOrgOnFreePlan into options-based utility

Move orgId guard and self-hosted/plan checks into @cio/utils/plans.
Dashboard and API callers pass plans, isSelfHosted, and optional orgId
directly; remove the dashboard store wrapper.

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

* fix(org): require orgId for free-plan check and translate favicon strings

Treat missing orgId as not on free plan so nav gating matches prior
behavior when currentOrg is unset. Pass organization.id from OG service.

Add favicon settings translations for all non-English locales.

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

* refactor(dashboard): move meta tag helpers to metaTags.server.ts

Keep PRIVATE_SERVER_URL resolution in a server-only module so dynamic
private env imports cannot leak into universal or client bundles.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-07 22:15:11 +01:00
Rotimi Best af95c6280c fix(dashboard): prevent CDN caching of HTML pages to fix stale builds after deploy 2026-07-07 21:14:10 +01:00
Abdulmuizz Hamzat 2e402efa78 chore(ci): publish latest from main and retire the edge tag (#711)
* fix(docs): update versioning strategy and tag policies for Docker images

* fix(docs): clarify versioning strategy in environment configuration
fix(docker): update docker-push script to remove local latest tag handling

* fix(docs): clarify VERSION variable usage in Docker push script
2026-07-07 12:59:33 +01:00
Rotimi Best e8181b296b chore(dashboard): add SVG logo variants alongside existing PNGs 2026-07-07 07:17:11 +01: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
8a22ee60bc fix(website): restore dotted hero backgrounds on PageHeader (#705)
* fix(website): restore radial hero backgrounds on page headers

PageHeader lost its decorative background when hero-bg was replaced by
DotField. Add a bottom-centered radial glow via a CSS class so it renders
reliably without Tailwind arbitrary-value parsing issues.

Also move the signup CTA gradient from an inline arbitrary Tailwind class
to the same CSS-class pattern for consistency.

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

* fix(website): restore dotted DotPattern hero background on PageHeader

Revert the radial glow added to page headers and bring back the static
DotPattern grid that was replaced by DotField in c31ad6395.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-02 13:20:14 +01:00
0ed0e3864d fix(api): return Zod validation errors from public API routes (#708)
* fix(api): return Zod validation errors from public API routes

Public API handlers now surface ZodError details as 400 VALIDATION_ERROR
responses instead of generic 500 INTERNAL_ERROR payloads. Course import
services also rethrow ZodError so validation failures are not wrapped as
server errors.

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

* refactor(utils): centralize course type values without db dependency

Define COURSE_TYPE_VALUES once in @cio/utils/constants and use it for
ZCourseType validation and the Drizzle COURSE_TYPE pgEnum. Removes the
circular @cio/utils -> @cio/db import while keeping course-import schemas
aligned with all four course types including PUBLIC.

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

* fix(validation): default missing course metadata allowNewStudent to true

Courses stored without allowNewStudent in metadata failed structure export
validation. Align Zod preprocessing with runtime behavior where absent
values mean enrollments are open.

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

* chore(db): drop unused COURSE_TYPE_VALUES re-export from schema

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-02 12:56:58 +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
1ec0c34ba7 fix(dashboard): use default highlighting for API quick-start snippets (#707)
* fix(dashboard): API page snippet highlighting and breadcrumb i18n

Stop double-translating nav titles in breadcrumbs (t was called on
already-localized strings like Audience and API).

Use Code.Root default typescript highlighting for all API quick-start
snippets instead of unsupported bash/python/go langs.

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

* revert(dashboard): drop breadcrumb i18n change from API page PR

Translation warnings were unrelated; keep only the Shiki lang fix.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-02 12:21:22 +01:00
efdcfba68d feat(api): purge Cloudflare CDN before and after OpenAPI spec upload (#704)
* feat(api): purge Cloudflare CDN before and after OpenAPI spec upload

Purge api.cdn.clsrio.com edge cache for openapi-latest and the dated
spec URL so deploys are not blocked by the 1h Cache-Control TTL on R2
objects. Uses the same CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID
pattern as embed uploads; skips when credentials are unset.

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

* fix(api): purge OpenAPI CDN cache only after upload

Match the embeds upload pattern: update R2 first, then invalidate
edge cache so clients refetch the new spec.

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

* fix(api): treat Cloudflare purge 2xx as success when body is unparseable

Avoid false upload failures when the purge API returns 200 with a
non-JSON body. Addresses Greptile review on #704.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-02 12:11:00 +01:00
ec61bfe7d9 feat(cohorts): rename Programs to Cohorts with data migration (#694)
* feat(cohorts): rename Programs to Cohorts with data migration

- Add cohort database tables and Drizzle migration alongside legacy program tables
- Introduce migrate-programs-to-cohorts script to copy existing program data safely
- Rename API routes (/cohort), services, queries, validation, and error codes
- Rename dashboard feature module, routes (/cohorts), navigation, and translations
- Update docs, emails, and internal compliance cron endpoints for cohort terminology
- Keep legacy program tables in place for rollback until explicitly removed

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

* fix(db): restore migrate:answer-data script alongside cohort migration

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

* fix(db): cast program enums when migrating data to cohort tables

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

* fix(db): truncate cohort tables before program-to-cohort copy

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

* fix(db): report accurate row counts in programs-to-cohorts migration

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-02 12:10:31 +01:00
fe9f8c441f feat(api): public API plan gating, docs auth, and dashboard snippets (#701)
* fix(automation): align public API plan gating across cloud and self-hosted

Add shared canUsePublicApi helper so cloud allows Early Adopter and
Enterprise while self-hosted requires Enterprise. Update the API key
dashboard page and backend key creation checks to use the same rules.

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

* docs(api): document bearer auth on public API reference

Add shared OpenAPI enrichment with bearerAuth security scheme and
authentication instructions. The interactive reference now loads an
enriched spec with Authorize support, and the API guide explains how
to create and use org API keys.

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

* feat(dashboard): add API quick-start code snippets on automation page

Mirror the MCP client setup section with cURL, JavaScript, and Python
examples that show how to authenticate public API requests with org keys.

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

* ci(openapi): republish spec when public API OpenAPI helpers change

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

* feat(dashboard): add Go snippet to API quick-start examples

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

* docs(api): use /org/*/api and clarify dashboard key generation path

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

* fix(api): escape backticks in OpenAPI description template literal

Greptile flagged that unescaped backticks around * in
PUBLIC_API_OPENAPI_DESCRIPTION evaluated to NaN and dropped auth docs
from the Scalar spec. Also use plan-specific snackbar message on upgrade.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-07-02 11:24:33 +01:00
Rotimi Best 447e54b1f4 chore(db): add script to fix landing page #courses links and Programs labels 2026-07-01 10:38:37 +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 c72558f8f1 fix(course): treat null isUnlocked as unlocked in progression logic
Items with isUnlocked = null were evaluated as teacher-locked due to
`?? false` defaulting to false before negation. This caused content to
appear locked immediately after a student marked a lesson complete, even
in free-mode courses.

Changed to `?? true` in both client-side (progression-utils.ts) and
server-side (core/progression.ts) so null/undefined isUnlocked defaults
to unlocked — consistent with the sidebar's own `?? true` guard.
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 65420320e7 feat(ai-assistant): odt support, document-to-lesson attach, upload UX fixes
- Add .odt (application/vnd.oasis.opendocument.text) as a supported upload
  format; extract text via JSZip + content.xml parsing
- Add attach_document_to_lesson agent tool so the AI can attach an uploaded
  document to a lesson as a downloadable material
- Instruct the agent to create a "Course Materials" lesson (first lesson of
  the first section) and attach the uploaded document to it on implementation
- Suppress hallucinated references: uploaded documents do not count as fetched
  URLs — omit the References section when no fetch_documentation_url calls
  were made
- Surface specific upload error snackbars (file_too_large, unsupported_file_type)
  instead of silently logging; fix generic fallback message
- Allow typing in the chat textarea while a document is uploading; only block
  Enter/send, not the whole input
- Fix retry endpoint to return the current run instead of erroring when the run
  is already active (queued/running/waiting_for_input/paused)
- Connect @cio/core Redis singleton in the jobs bootstrap so course-generation
  workers no longer fail with "The client is closed"
- Expose documentAssets (assetId + fileName) alongside documentText in
  AgentContext and pass it through both the chat route and course runner
2026-06-29 12:29:47 +01:00
Rotimi Best 773fc873c2 feat(lms): add student certificates page and sidebar nav (#696)
* feat(lms): add student certificates page and sidebar nav

Add a Certificates item to the student LMS sidebar that lists courses
where the student has earned a certificate. Each course card includes a
View Certificate action linking to the existing per-course certificate
page.

* refactor: reuse isOrgOnFreePlan helper from org store

* chore(i18n): add certificates translations for all locales

---------
2026-06-29 11:49:27 +01:00
Rotimi Best d242215277 docs: prd for public course comments 2026-06-29 07:22:45 +01:00
Rotimi Best 8c14bfad45 fix(exercise): show correct submitted state and real questions for students
exercise.svelte was auto-adding a QUESTION_TEMPLATE to the store whenever
questions were empty, which raced with hydrateExercisePageData.
applyMySubmission fired with the blank template in place, set
isFinished=true, and the guard prevented re-application once real
questions loaded.

Two fixes: guard the auto-add effect with isOrgStudent (students never
need to add questions), and gate applyMySubmission on isExerciseLoaded
so it always waits for hydrateExercisePageData to populate real questions
first.

Also fix off-by-1 in applyMySubmission (questions.length → questions.length + 1)
so currentQuestion is undefined at end-of-exercise, satisfying the
isFinished derived. Clear questionnaire.questions in onDestroy to prevent
stale questions flashing on the next exercise.

Fix lesson delete not updating sidebar/content list by filtering the
removed lesson from sections on success.
2026-06-29 07:18:58 +01:00
Rotimi Best 513ab5e81f fix(hls): scope cookie name per-asset to prevent multi-video 403s
When a lesson has two or more uploaded videos each calls mintHlsCookie,
but all wrote to the same `cio_hls` cookie name. The last write won,
leaving every other video with a mismatched token and a 403 from the
CF Worker.

Cookie name is now `cio_hls_${assetId}` so each video owns its own
cookie. The tenant-router Worker, the Hono HLS fallback route, and the
transcripts route all read the per-asset name.
2026-06-29 02:09:50 +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 55abf982ed fix(question-types): realign registry ids with the question_type table
The registry hardcoded numeric ids that drifted from the database
`question_type` table: MATCHING and HOTSPOT were inserted mid-list
(ids 9 and 11) but never seeded, shifting ORDERING/LINK/WORD_BANK/
STAR/VIDEO_RECORDING one or two ids past their real rows. Every id
emitter inherited the wrong values — the exercise editor, the AI
question generator, and the grading/scoring gate — so e.g. an ORDERING
question was stored as id 10 (LINK in the DB), came back as a manual
type, and never auto-graded.

- Move MATCHING/HOTSPOT to the end (ids 14/15) and flag them
  `disabled`, restoring the append-only id convention and realigning
  ORDERING=9, LINK=10, WORD_BANK=11, STAR=12, VIDEO_RECORDING=13 with
  the DB.
- Add a registry test asserting every enabled type's id matches the
  canonical `question_type` rows and disabled types stay past the live
  range, so ids can't silently drift again.
- Constrain the agent tool schema to enabled, DB-backed ids so the
  model can't emit a disabled id with no row.
- Drop disabled types from the teacher prompt's type list and guidance.
- Centralize the dashboard question-type picker on the `disabled` flag.

This also fixes video-recording detection, which compared against the
stale id 15 instead of the real 13.
2026-06-29 01:07:02 +01:00
Rotimi Best eb4cd22ac5 chore(db): add migration dropping position from asset_usages unique key
Generated migration for the narrowed
asset_usages_asset_target_slot_unique constraint (asset_id, target_type,
target_id, slot_type, slot_key). Run the duplicate cleanup before
applying — existing rows that differ only by position will otherwise
violate the new constraint.
2026-06-28 23:08:17 +01:00
Rotimi Best 07491ae609 feat(media): paginate library video picker with total count
Load the library 20 videos per page with Previous/Next controls, and
show the total match count beside the "Search library" label.
2026-06-28 23:06:08 +01:00
Rotimi Best f18e5600f6 fix(media): prevent attaching an asset to a target twice
Drop `position` from the `asset_usages` unique constraint so the same
asset can't be attached to one target/slot more than once, and update
the insert's onConflict target + lookup to match the narrower key.

Guard it at the service layer too: attachAssetService now rejects an
attach when the asset already has a usage for that target with a new
ASSET_ALREADY_ATTACHED (409) error, backed by an
assetUsageExistsForTarget query.
2026-06-28 23:05:57 +01:00
Rotimi Best 42231b3ee1 fix: filter library videos in lesson with videos in the current lesson 2026-06-28 22:21:05 +01:00
Rotimi Best 202fd30445 fix: enable adding video from library to another lesson 2026-06-28 21:41:53 +01:00
Rotimi Best 47b85a9697 Merge branch 'main' of https://github.com/classroomio/classroomio 2026-06-28 13:19:37 +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 9222c8947d docs: update website wording 2026-06-28 12:36:27 +01:00
Rotimi Best 08ea608f94 feat(media): delete videos from the media library
Add an admin flow to permanently delete a media asset and clean up
everything it leaves behind.

- Delete dialog with a destructive, irreversible confirmation; deletion
  is blocked while the video is still attached and shows the in-use
  lessons so the admin can detach first.
- Usage list resolves lesson/course context for each attachment, links
  to the lesson editor via an anchor (right-click "open in new tab"),
  and exposes a refresh button to re-check usage after detaching
  elsewhere.
- Storage cleanup runs as a background maintenance job: the producer
  captures the asset's object keys/prefixes up front and an idempotent
  worker sweeps HLS segments, thumbnails, audio, transcript, and the
  raw upload from object storage.

Also harden usage accounting so stale attachments cannot trap an asset:

- asset_usages has no FK to its polymorphic target, so prune a lesson's
  usage rows whenever the lesson is hard-deleted (single + bulk paths).
- Usage resolution drops orphaned lesson usages (lesson gone or course
  soft-deleted) so they neither appear in the list nor block deletion.

Document the icon-only button convention (variant="secondary") in
AGENTS.md.
2026-06-28 12:36:13 +01:00
Rotimi Best e168525ad1 fix(api): update openapi upload script env import path (#693)
The env module was moved to @cio/core/config/env but the upload-openapi-spec
script still imported from the removed apps/api/src/config/env path, causing
CI upload:openapi to fail with ERR_MODULE_NOT_FOUND.

Co-authored-by: Rotimi Best <[email protected]>
2026-06-27 10:26:53 +01:00
Rotimi Best 2ccbf30414 Merge branch 'main' of https://github.com/classroomio/classroomio 2026-06-27 10:19:46 +01:00
a695c97eda feat(course): reset student course progress (#671)
* feat(course): add reset student course progress action

Give course admins and tutors a destructive reset-progress flow on the
student people detail page. The backend clears learner progress for one
(courseId, memberId) pair in a single transaction while keeping the
groupmember enrolled, and logs an audit summary. The dashboard shows a
confirmation dialog with impact counts from analytics before deleting.

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

* fix(i18n): add localized reset progress strings for all locales

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

* fix(course): scope reset progress to course data only

Stop deleting org-level community questions and answers during course
progress reset. Fix course newsfeed deletion order so comments on a
student's posts are removed before the posts themselves. Restrict reset
to student members, move analytics loading to the person detail layout,
and rename impact copy to course newsfeed only.

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

* fix(course): address Greptile review on reset progress

Make progressImpact opt-in via includeProgressImpact query param so
analytics requests avoid extra DB work unless the reset dialog needs it.
Count only student-authored course newsfeed posts and comments in the
impact summary to avoid double-counting replies on the student's posts.

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

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
2026-06-27 10:14:52 +01:00
Rotimi Best e43d8a2444 fix(sidebar): restore menu button styles on disabled nav items (#692)
Disabled items were overriding props.class with a custom class string,
stripping the Sidebar.MenuButton's height, padding, and color styles.
Drop the inline class and rely on the existing aria-disabled CSS rules.
2026-06-25 23:54:47 +01:00
Rotimi Best e6899c27c3 Merge branch 'main' of https://github.com/classroomio/classroomio 2026-06-25 23:53:22 +01:00
Rotimi Best 4fe45ac642 fix(dashboard): course menu icons, delete scroll, and settings tags auth (#691)
* fix(dashboard): course menu icons, delete scroll, and settings tags auth

Remove inconsistent dropdown icons, scroll to settings sections for delete
and related actions when already on the settings page, and skip org tag
group fetches for non-admin users to avoid ORG_TEAM_NOT_AUTHORIZED errors.

* fix(api): allow org members to fetch organization tags

Use orgMemberMiddleware for GET /organization/tags instead of restricting
reads to admins. Revert the dashboard settings workaround that skipped tag
group fetches for non-admin users.
---------
2026-06-25 23:53:16 +01:00
Rotimi Best 87272812f9 fix(sidebar): restore menu button styles on disabled nav items
Disabled items were overriding props.class with a custom class string,
stripping the Sidebar.MenuButton's height, padding, and color styles.
Drop the inline class and rely on the existing aria-disabled CSS rules.
2026-06-25 23:13:08 +01:00
Rotimi BestRotimi Bestgreptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>Cursor Agent
bd13d0724d docs(agents): add Cursor Cloud dev environment setup notes (#676)
* docs(agents): add Cursor Cloud dev environment setup notes

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

* docs(agents): document self-hosted vs cloud deployment modes

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

* docs(agents): document seeded tenants and cloud-mode multi-tenant testing

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

* Update AGENTS.md

Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>

---------

Co-authored-by: Cursor Agent <[email protected]>
Co-authored-by: Rotimi Best <[email protected]>
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com>
2026-06-25 22:49:27 +01:00
Rotimi Best 23dba7392e Merge branch 'main' of https://github.com/classroomio/classroomio 2026-06-25 22:47:30 +01:00
Rotimi Best 1305e2e454 feat(dashboard): add view course site actions and publish flow (#690)
Add View course site and Publish course options to org course context menus.
Replace the course header View as student button with View Course Site and
a companion overflow menu that includes View as student plus list actions.
Show a publish prompt modal for unpublished courses and support #publish
deep links in course settings.
2026-06-25 22:39:30 +01:00
Rotimi Best 50a4f24b7c fix: encode body of translation updating, reduce p margin in prose 2026-06-25 22:12:38 +01:00
Abdulmuizz HamzatandRotimi Best 6d594b4296 feat: harden self-hosting + add pull-based deploy, Coolify/Railway (#666)
* feat: enhance self-hosting support and CI/CD workflows

- Updated .env.example to clarify auth secret generation and self-hosting mode.
- Added CI workflow for build and lint checks on PRs and main branch pushes.
- Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support.
- Enhanced upload-openapi-spec workflow with caching for dependencies.
- Removed obsolete Dockerfile for API service and added jobs worker to Docker setup.
- Updated self-host documentation to reflect new jobs worker requirement.
- Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation.
- Enhanced docker-compose.yaml with health checks and jobs worker configuration.
- Updated docker-push.sh to support multi-architecture builds and improved logging.
- Revised documentation for publishing images and self-hosting to include new features and requirements.

* feat(self-host): safe versioned releases + pull-based image pipeline

Builds on the pull-based images compose (docker-compose.images.yaml) to make
pulling pre-built images safe for self-hosters:

- docker-publish: stable-releases + edge tag policy. Git tag vX.Y.Z publishes
  X.Y.Z/X.Y/X/latest; a push to main publishes only :edge (latest never moves
  on a routine merge).
- Pre-publish smoke-test gate: each image is booted against ephemeral
  postgres+redis and healthchecked before any push (docker/smoke-test.sh).
- CIO_VERSION pinning in .env.example; run-docker-full-stack.sh --images flag
  to pull instead of build.
- Docs: SELF_HOST pull-from-images + versioning/upgrade/rollback sections,
  PUBLISHING_IMAGES tag policy + pnpm release flow, self-hosting.mdx quickstart.

* feat: enhance self-hosting support and CI/CD workflows

- Updated .env.example to clarify auth secret generation and self-hosting mode.
- Added CI workflow for build and lint checks on PRs and main branch pushes.
- Improved docker-publish workflow to trigger on relevant file changes and added multi-arch support.
- Enhanced upload-openapi-spec workflow with caching for dependencies.
- Removed obsolete Dockerfile for API service and added jobs worker to Docker setup.
- Updated self-host documentation to reflect new jobs worker requirement.
- Modified Dockerfiles for API and Dashboard to optimize dependency fetching and installation.
- Enhanced docker-compose.yaml with health checks and jobs worker configuration.
- Updated docker-push.sh to support multi-architecture builds and improved logging.
- Revised documentation for publishing images and self-hosting to include new features and requirements.

* ci: run build-only on PRs, defer lint until repo-wide format

* docs: point Railway template link at the new template URL

* fix: address Greptile review + dashboard CI build env

- docker-push.sh: only also-tag :latest for real semver releases, so
  VERSION=edge/main/dev can't clobber the stable latest pointer (P1).
- run-docker-full-stack.sh: stop deriving OBJECT_STORAGE_PUBLIC_ENDPOINT from
  the dashboard origin (broke presigned URLs for videos/documents buckets);
  derive only the public media base URL and document the S3-API route.
- ci.yml: provide PUBLIC_IS_SELFHOSTED at build time (dashboard reads it from
  SvelteKit's strict $env/static/public); bump cache actions v3 -> v4.
- upload-openapi-spec.yml: bump cache actions v3 -> v4.
- smoke-test.sh: dashboard gate now fails on 5xx instead of accepting any response.

* ci: scope PR build gate to the api + dashboard closures

Building every package surfaced pre-existing rot in apps that aren't part of
this PR (storybook's missing @lucide/svelte import, course-app prettier debt).
The gate's purpose is to verify the two Docker-image apps; the rest are built on
PRs by their own Cloudflare Workers builds. Filter to @cio/api... and
@cio/dashboard... so the gate covers exactly what it should.

* fix(docker-push): make latest_arg always return 0

Use an `if` instead of `is_release_version && printf` so the helper returns 0
even when it emits no tag. A non-zero return inside the $(latest_arg ...) build
argument is fragile under set -e (and would abort with inherit_errexit), which
would break the edge/main builds the policy explicitly supports.

* fix(docker): copy patches/ into jobs image so pnpm install resolves patchedDependencies

The jobs Dockerfile ran `pnpm install` without copying patches/, so pnpm
failed hashing package.json#pnpm.patchedDependencies (layerchart patch),
breaking the self-host full-stack build. api/dashboard already copy patches/.

Co-Authored-By: Claude Opus 4.8 <[email protected]>

* fix(docker): build jobs worker deps in topological order

The jobs Dockerfile built workspace packages with a hand-written list that
omitted transitive deps (e.g. @cio/question-types, consumed by @cio/utils),
so `tsc` failed with TS2307 during the self-host build. Use the recursive
`pnpm -r --filter @cio/jobs-worker... build` like the api/dashboard images,
which builds the full dependency closure in the correct order.

* refactor(docker): move jobs Dockerfile to docker/Dockerfile.jobs and align dep install

Move apps/jobs/Dockerfile -> docker/Dockerfile.jobs so all three image recipes
live together in docker/, and update every reference (docker-compose.yaml,
docker-push.sh, docker-publish.yml, railway.jobs.json, self-hosting.mdx).

Also align its dependency install with the api/dashboard images: replace the
hand-maintained list of workspace package.json copies (which silently missed
transitive deps like @cio/core / @cio/ai-assistant, leaving their node_modules
uninstalled and breaking the build with TS2307) with the proven pnpm-fetch
pattern: fetch the full lockfile into a cached store, COPY . ., then an offline
filtered install of @cio/jobs-worker... This installs the complete dependency
closure regardless of how the workspace graph grows.

* refactor(docker): move docker-compose.yaml to repo root for Coolify compatibility

Coolify's Docker Compose build pack forces Base Directory to the repo root and
resolves build contexts from there, so the compose's `context: ..` (needed when
the file lived in docker/) climbed above the repo and failed with
`lstat /artifacts/docker: no such file or directory`.

Move docker/docker-compose.yaml -> ./docker-compose.yaml and change the build
contexts from `..` to `.` so the context is the repo root with no upward path.
This works identically for run-docker-full-stack.sh, manual `docker compose up`,
and Coolify. Dockerfiles stay referenced as docker/Dockerfile.*. Update the
references in run-docker-full-stack.sh and the docs accordingly. The images
compose (docker-compose.images.yaml) is unaffected (no build context) and stays
in docker/.

* Add self-hosting documentation for Coolify and Railway

- Created new documentation for deploying ClassroomIO on Coolify using a pull-based Docker Compose stack.
- Added comprehensive guide for self-hosting ClassroomIO with Docker, covering both pre-built images and building from source.
- Introduced a new meta.json file to organize self-hosting documentation pages.
- Developed a Railway deployment guide with environment variable configurations for API, dashboard, and jobs worker.
- Updated self-hosting blog post to link directly to the new Railway guide.
- Removed obsolete Railway configuration files (railway.api.json, railway.dashboard.json, railway.jobs.json).
- Enhanced run-docker-full-stack.sh script to improve MinIO credential generation and service readiness checks.
- Updated docker-compose files to enforce strong secret requirements for authentication.
- Adjusted self-hosting references in the Docker documentation to include new guides.

* fix(docker): point references at root-level docker-compose.images.yaml

The pull-based compose file moved to the repo root (alongside docker-compose.yaml).
Update IMAGES_COMPOSE_FILE in run-docker-full-stack.sh, the file's own header
example, and the docs (docker.mdx curl URL, coolify.mdx path + blob link,
SELF_HOST.md relative link + curl URL).

* fix(self-host): address review round 2 — drop unused AUTH_BEARER_TOKEN, pull-by-default, doc fixes

- Remove AUTH_BEARER_TOKEN everywhere (unused in app code; PRIVATE_SERVER_KEY is the real
  dashboard<->api key). Simplifies run-docker-full-stack.sh secret generation.
- run-docker-full-stack.sh now defaults to PULLING pre-built images; add --build for source.
- docs: recommend MinIO, 'a way' (not 'recommended way') to self-host, remove Direct API Access
  section, trim Common Commands, SMTP vars per-line, fix PRIVATE_SERVER_KEY wording.

* feat(db): versioned migrations for self-host with auto-baseline

- Regenerate a single clean baseline migration; db:setup now runs drizzle-kit migrate (not push).
- Add db:generate / db:migrate / db:baseline scripts.
- baselineMigrationsIfNeeded(): adopt an existing (push/supabase) schema into drizzle's journal so
  migrate won't try to recreate existing objects ("type/table already exists"). No-ops on fresh
  and already-tracked databases; runs automatically in db:setup before migrate.

* ci(openapi): drop stale ci.yml reference in turbo-cache comment

ci.yml was removed earlier in this review; the openapi workflow's turbo cache still
warms from its own prior runs via restore-keys, so this is a comment-only fix.

* fix(db): make baseline squash-safe for migrate-tracked databases

baselineMigrationsIfNeeded returned early whenever drizzle.__drizzle_migrations
had any rows. After a migration squash, an instance tracked under the old
journal (pre-squash hashes) would then let `drizzle-kit migrate` re-run the
consolidated baseline — whose journal `when` is newer than every old created_at
— and crash on `CREATE TYPE/TABLE "..." already exists` at startup.

Now, when history exists but the consolidated baseline hash is absent, insert
that hash (marking it already applied) so migrate skips it. Only the baseline
entry (idx 0) is reconciled, so genuinely pending future migrations still run.

Verified end-to-end against Postgres: reproduces the original crash, confirms
the fix no-ops migrate on a pre-squash DB, and confirms a later 0001 migration
still applies. Fresh-DB and already-adopted paths unchanged.

* fix(docker): add explicit id to pnpm-store cache mounts

Railway's Metal builder rejects BuildKit cache mounts that omit an
explicit id (it does not default id to target like standard Docker/
BuildKit do), failing the build at parse time with:

  dockerfile invalid: flag '--mount=type=cache,target=/pnpm/store'
  is missing an id argument

Add id=pnpm-store to all six cache mounts across the api, dashboard,
and jobs images. Harmless on Docker Desktop / CI / GitHub Actions, and
required for the Railway self-host build path.

* fix(docker): drop BuildKit cache mounts for Railway compatibility

Railway's builder rejects cache-mount ids not prefixed with its
per-service cacheKey, which can't be hardcoded in a Dockerfile shared
across CI, local Docker, and three Railway services. Remove the
--mount=type=cache flag (keep --store-dir /pnpm/store); the store is
baked into the fetch layer and reused by the offline install, so
behaviour is unchanged. Supersedes 9c4633f2c.

* fix(docker): force LF for shell scripts and Dockerfiles via .gitattributes

cio-api crash-looped on Railway with 'exec /app/entrypoint.sh: No such
file or directory' because the entrypoint shipped with CRLF endings
(Windows checkout, core.autocrlf=true) — the '#!/bin/sh\r' shebang is
unresolvable in the container. Add .gitattributes pinning *.sh and
Dockerfile* to LF so container-executed files are never CRLF regardless
of contributor OS.

* fix(db): serialize db-setup with an advisory lock to prevent baseline double-insert

Concurrent db:setup runs (e.g. multiple API replicas booting together) could both
pass the baseline 'already present?' check and insert duplicate rows into
drizzle.__drizzle_migrations, which has no unique constraint on hash. Wrap baseline
+ migrate in a pg_advisory_lock held on a dedicated connection so concurrent
startups serialize: the loser blocks, then sees the baseline applied and no-ops.

* docs(railway): update template deploy URL to classroomio-1

---------
Co-authored-by: Rotimi Best <[email protected]>
2026-06-23 10:17:09 +01:00