Files
Abdulmuizz Hamzat 5e2dcf71f4 docs: add dev setup guide and make package scripts cross-platform (Windows) (#664)
* chore: update README with environment setup instructions and demo account credentials

fix: replace rm -rf with rimraf in build scripts for cross-platform compatibility

fix: add shell option to pnpm command in db-setup script for Windows compatibility

chore: update package.json files to use rimraf instead of rm -rf for consistency

chore: add rimraf dependency to pnpm-lock.yaml

feat: add postinstall script to install lefthook git hooks in a cross-platform manner

* docs(qa): add QA test plan, flow checklists, and bug-report workflow

Add a repo-native QA execution layer on top of FEATURE_AUDIT.md (the
feature map). Test cases live in the repo; bugs live in GitHub Issues, so
there is no duplicate tracker to keep in sync.

QA system:
- FEATURE_AUDIT.md: the feature map - every feature, its user flow, and
  edge cases with file:line citations.
- qa/README.md: the workflow, severity/difficulty rubrics, label setup,
  and a static feature -> flow coverage map proving all 42 audited
  features have a test plan.
- qa/flows/01..13: per-flow happy-path checklists covering signup &
  onboarding, course authoring, people/invites/roles, student ->
  certificate, AI assistant & tutor, programs/attendance, community,
  org public site, media & import, org settings, public API/MCP,
  enterprise SSO/licensing, and billing & AI credits.
- .github/ISSUE_TEMPLATE/qa-bug.yml: a QA Bug template requiring
  severity, difficulty, area, repro steps, and a screen recording.

Dev tooling:
- package.json: add db:studio and docker:build scripts.
- packages/db: add db:studio (drizzle-kit studio); fix db:reset on
  Windows by spawning the pnpm.cmd shim with shell:true (Node's
  CVE-2024-27980 fix blocks spawning .cmd/.bat without a shell).

* chore: update QA bug template and README for clarity and improved workflow

* docs: enhance DEV_SETUP_NOTES and README for clarity on prerequisites and email setup
chore: update package.json scripts for cross-platform compatibility
feat: add cross-platform Prettier check script for pre-commit hook

* Remove QA documentation and flow files for ClassroomIO

- Deleted the entire QA README.md file which contained the QA process, bug filing guidelines, and feature coverage map.
- Removed all flow files (01 to 13) that detailed the QA testing processes for various features including admin signup, course authoring, people management, student learning, AI assistant, programs, community, org public site, media management, org settings, public API, enterprise SSO, billing, and AI credits.

* fix: update postinstall script
2026-06-15 17:40:06 +01:00
..
2026-04-27 11:40:38 +01:00

@cio/embeds

Standalone ES module bundles for third-party sites (no dependency on apps/website). The question-type-picker entry shows a small loading spinner (same style as @cio/ui Spinner) as soon as the bootstrap script runs, then loads the main widget chunk—so slow connections see feedback while the larger bundle downloads.

Question type picker

Build outputs dist/question-type-picker.js (bootstrap) plus additional question-type-picker-*.js chunks. Upload every .js file from dist/ to the same directory on your CDN so dynamic import() resolves correctly.

CSS for the widget is injected when the main chunk runs.

Develop

pnpm --filter @cio/embeds dev

Open the dev harness at http://localhost:5180 (see root index.html).

Build

pnpm --filter @cio/embeds build

prebuild runs @cio/ui Tailwind generation so @cio/ui/output.css exists for src/embed.css.

Deploy to Cloudflare R2

Built files are uploaded with the same S3-compatible credentials and assets bucket used for Storybook. Objects live under a per-widget prefix so future embeds do not clash:

Widget R2 key prefix
Question type picker embeds/question-type-picker/

After upload, every file from dist/ appears at embeds/question-type-picker/<filename>. The bootstrap script is always question-type-picker.js; lazy-loaded chunks use content hashes in the filename (for example question-type-picker-embed-mount-7a3b2c1d.js) so they can be cached for a long time. The upload script sets a short TTL + must-revalidate on the bootstrap only, so a normal refresh picks up new chunk URLs after you deploy. If you still see an old UI, hard-refresh or purge your CDN cache for question-type-picker.js.

Map your public CDN or custom domain to this bucket (or path) so those URLs are reachable over HTTPS.

Environment variables

Set these locally (e.g. .env in apps/embeds, loaded by the upload script) or in CI:

  • CLOUDFLARE_ACCESS_KEY
  • CLOUDFLARE_SECRET_ACCESS_KEY
  • CLOUDFLARE_ACCOUNT_ID

CDN cache purge (optional): R2 is only storage; if assets.cdn.clsrio.com (or your public host) is proxied through Cloudflare, edge cache can still serve old bytes after upload. The upload script can call Cloudflare’s Purge Cache API for every file URL it just published.

  • CLOUDFLARE_API_TOKEN — API token with Zone → Cache Purge → Purge on the zone that serves the asset hostname (not the R2 S3 access key).
  • CLOUDFLARE_ZONE_ID — that zone’s ID (dashboard → site → Overview → API).
  • EMBED_CDN_BASE_URL — public origin for URLs to purge, default https://assets.cdn.clsrio.com (no trailing slash). Override if your embeds are served under another host.
  • EMBED_SKIP_CDN_PURGE=1 — skip purge (e.g. local testing).

If CLOUDFLARE_API_TOKEN or CLOUDFLARE_ZONE_ID is missing, upload still succeeds and purge is skipped.

Manual publish (build + upload)

From the repo root:

pnpm --filter @cio/embeds run embeds:publish

Or from apps/embeds after a build:

pnpm upload:embeds

pnpm upload:embeds only uploads dist/; run pnpm build first if needed.

GitHub Actions

Workflow: .github/workflows/deploy-embeds.yml.

  • When it runs: push to main that touches packages/ui/**, packages/question-types/**, or apps/embeds/**, or workflow_dispatch.
  • What it does: pnpm i, then pnpm --filter @cio/embeds run embeds:publish.
  • Secrets: the job uses the production environment; configure the R2 CLOUDFLARE_ACCESS_KEY, CLOUDFLARE_SECRET_ACCESS_KEY, and CLOUDFLARE_ACCOUNT_ID there. Optionally add CLOUDFLARE_API_TOKEN and CLOUDFLARE_ZONE_ID so each deploy purges the Cloudflare CDN cache for the published embed URLs. Optional repo variable EMBED_CDN_BASE_URL overrides the default public base URL used when building purge URLs.

Embed on a site

The bootstrap file question-type-picker.js loads additional chunks via dynamic import(). Browsers resolve those URLs relative to the bootstrap script’s URL, so every file from dist/ must be served from the same directory (same path prefix on your CDN). Do not host only the bootstrap file.

Production (CDN / R2 public URL)

Use your public base URL for the picker folder—for example:

https://<your-public-host>/embeds/question-type-picker/

Then reference the bootstrap script under that path.

Auto-mount (looks for [data-cio-widget="question-type-picker"]):

<div data-cio-widget="question-type-picker"></div>
<script type="module" src="https://<your-public-host>/embeds/question-type-picker/question-type-picker.js"></script>

Manual mount (no data-cio-widget on that node—avoid double-mounting):

<div id="qtp"></div>
<script type="module">
  await import('https://<your-public-host>/embeds/question-type-picker/question-type-picker.js');
  const el = document.getElementById('qtp');
  if (el) window.CIO?.questionTypePicker?.mount(el);
</script>

Local or any static host

If you copy all of dist/ to a single URL prefix, set src / import() to that prefix + question-type-picker.js (same pattern as above, with your host and path).

Use a type="module" script in all cases; the bundle is ES modules with code-splitting.

Local smoke test

From apps/embeds, after pnpm build, serve the folder and open example-embed.html:

pnpm build && npx serve . -p 5190
# open http://localhost:5190/example-embed.html

Adding another widget

  1. Add src/widgets/<name>/widget.svelte and a bootstrap entry (same pattern as question-type-picker).
  2. Add a dedicated build.lib.entry or Vite config if you need a separate publishable script.

Course widget host split

The course widget uses three different URL concerns:

  1. Embed script host
  • this is where the browser downloads course-widget.js
  • production script URL: https://embed.classroomio.com/course-widget
  • production hosted iframe: https://embed.classroomio.com/course-widget?key=<public-key>
  • configured on the API side with PUBLIC_EMBED_BASE_URL
  1. Payload API host
  • this is where the widget fetches GET /widgets/:publicKey/payload
  • production: tenant-router injects window.CIO.apiBaseUrl from its API_URL env when serving embed assets
  • local dev: @cio/embeds Vite injects the same global from API_URL (defaults to http://localhost:3002)
  1. Course destination host
  • this is where end users are sent when they click a course
  • it comes from the organization public site or custom domain, not the API host

These are intentionally separate. The widget fetches data from the API host, but links users to the organization’s public course pages.

Course widget local development

Local development should not depend on deployed CDN assets.

Expected local hosts

  • embed script host: PUBLIC_EMBED_BASE_URL on the API (falls back to http://localhost:5180 in dev)
  • payload API host: API_URL when running embeds dev or tenant-router (not included in customer copy-paste code)

Generated snippet:

<div data-cio-widget="course-widget" data-widget-key="wgt_xxx"></div>
<script async type="module" src="https://embed.classroomio.com/course-widget"></script>

Dev server path shape

The embeds Vite dev server rewrites these stable production-style paths:

  • /embeds/question-type-picker/question-type-picker.js
  • /embeds/course-widget/course-widget.js

to the underlying source entry files during development.

That means dashboard-generated widget embed snippets can use the same path shape in local development and production without requiring the CDN to exist first.