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
This commit is contained in:
@@ -1,48 +1,111 @@
|
||||
name: 'Bug Report'
|
||||
description: Report something that is broken or behaving unexpectedly.
|
||||
title: '[area] '
|
||||
labels: ['bug']
|
||||
description: Create a bug report to help us improve
|
||||
body:
|
||||
- type: markdown
|
||||
attributes:
|
||||
value: Thank you for reporting an issue.
|
||||
Please fill in as much of the form below as you're able to.
|
||||
- type: textarea
|
||||
value: |
|
||||
Thanks for taking the time to report a bug — anyone is welcome to file one.
|
||||
Please fill in as much as you can; a short **screen recording** is the single
|
||||
most helpful thing you can add.
|
||||
- type: dropdown
|
||||
id: severity
|
||||
attributes:
|
||||
label: Issue Description
|
||||
description: Please provide a clear and concise description of the problem.
|
||||
- type: textarea
|
||||
label: Severity
|
||||
description: How bad is it? (drives fix order — S0 first)
|
||||
options:
|
||||
- S0 — Blocker (flow impossible / data loss / security / crash / payments broken)
|
||||
- S1 — Major (core feature broken, no reasonable workaround)
|
||||
- S2 — Minor (works but wrong/confusing; workaround exists)
|
||||
- S3 — Polish (cosmetic / copy / layout / edge-only)
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: area
|
||||
attributes:
|
||||
label: Steps to Reproduce
|
||||
description: Please provide step-by-step instructions to reproduce the issue. Include code snippets, error messages, and any other relevant information.
|
||||
- type: textarea
|
||||
label: Area
|
||||
options:
|
||||
- backend / API (Api not behaving as expected, e.g. wrong response, error when it should succeed, etc.)
|
||||
- auth
|
||||
- signup / onboarding
|
||||
- organizations / settings
|
||||
- people / invites / roles
|
||||
- courses
|
||||
- lessons
|
||||
- exercises / grading
|
||||
- compliance / certificates
|
||||
- programs / attendance
|
||||
- community / newsfeed
|
||||
- media / import
|
||||
- org public site / website
|
||||
- tags / domains / widgets
|
||||
- public API / MCP / automation
|
||||
- SSO / token auth / licensing
|
||||
- billing / AI credits
|
||||
- AI assistant / tutor
|
||||
- email / jobs / analytics
|
||||
- other
|
||||
validations:
|
||||
required: true
|
||||
- type: dropdown
|
||||
id: environment
|
||||
attributes:
|
||||
label: Expected Behavior
|
||||
description: Describe what you expected to happen.
|
||||
label: Environment
|
||||
options:
|
||||
- Local (pnpm dev)
|
||||
- Staging (Coolify)
|
||||
- Production (cloud)
|
||||
- Self-hosted
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: steps
|
||||
attributes:
|
||||
label: Current Behavior
|
||||
description: Describe what is currently happening.
|
||||
label: Steps to reproduce
|
||||
description: Numbered, from a known starting state.
|
||||
placeholder: |
|
||||
1. Log in as admin ([email protected])
|
||||
2. Courses → New course
|
||||
3. ...
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: expected
|
||||
attributes:
|
||||
label: Screenshots (optional)
|
||||
description: If applicable, add screenshots to help explain the issue.
|
||||
label: Expected behavior
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: actual
|
||||
attributes:
|
||||
label: Actual behavior
|
||||
description: Include console/network errors if any.
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
id: video
|
||||
attributes:
|
||||
label: Screen recording (recommended)
|
||||
description: If you can, drag-and-drop a short video reproducing the bug — it speeds up triage a lot.
|
||||
- type: input
|
||||
id: sentry
|
||||
attributes:
|
||||
label: Operating System [e.g., Windows 10]
|
||||
- type: input
|
||||
label: Sentry link (if applicable)
|
||||
description: Paste the Sentry issue URL if this maps to a captured error.
|
||||
- type: dropdown
|
||||
id: mode-specific
|
||||
attributes:
|
||||
label: Browser [e.g., Chrome, Firefox]
|
||||
- type: input
|
||||
attributes:
|
||||
label: Environment (locally, production, selfhosted)
|
||||
description: If applicable, describe the environment in which the issue occurs.
|
||||
label: Mode-specific?
|
||||
description: Does this only happen in one deployment mode? (PUBLIC_IS_SELFHOSTED divergence)
|
||||
options:
|
||||
- 'No — happens in both / not mode-related'
|
||||
- 'Cloud only'
|
||||
- 'Self-hosted only'
|
||||
- 'Not yet verified in the other mode'
|
||||
- type: checkboxes
|
||||
attributes:
|
||||
label: Please check the boxes that apply to this issue report.
|
||||
label: Before filing
|
||||
options:
|
||||
- label: I have searched the existing issues to make sure this is not a duplicate.
|
||||
- label: I have provided steps to reproduce the issue.
|
||||
- label: I have included relevant environment information.
|
||||
- label: I have included any relevant screenshots.
|
||||
- label: I understand that this is a voluntary contribution and that there is no guarantee of resolution.
|
||||
- label: I want to work on creating a PR for this issue if approved
|
||||
- label: I searched existing issues and this is not a duplicate.
|
||||
- label: I attached a screen recording (if I was able to).
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
# ClassroomIO — Local Dev Setup Guide
|
||||
|
||||
A practical guide to running classroomio locally for development, plus a
|
||||
troubleshooting reference for the errors you're likely to hit. Use it to set up a
|
||||
fresh machine or to onboard a new contributor.
|
||||
|
||||
> Works on macOS, Linux, and Windows. **Windows users:** run the interactive
|
||||
> commands in **Git Bash** (Unix commands like `cp` work there). The repo's package
|
||||
> scripts are already cross-platform.
|
||||
|
||||
---
|
||||
|
||||
## 1. Prerequisites
|
||||
|
||||
| Tool | Version | Required? | Notes |
|
||||
|------|---------|-----------|-------|
|
||||
| Node.js | 20.x (`.nvmrc` = v20.19.3) | **Required** | `nvm install && nvm use` |
|
||||
| pnpm | 10.x | **Required** | The package scripts call `pnpm` directly (e.g. `build:clean` runs `pnpm run build`) — npm/yarn are not supported substitutes. `corepack enable` then `corepack prepare pnpm@latest --activate`, or `npm i -g pnpm` |
|
||||
| Docker (Desktop) | latest | **Required** | Runs Postgres + Redis (and optionally MinIO). No local install of those needed. |
|
||||
| Git | latest | **Required** | Windows: includes Git Bash |
|
||||
| ffmpeg / ffprobe | latest | Optional | Only for media/thumbnail background jobs |
|
||||
|
||||
---
|
||||
|
||||
## 2. Setup steps
|
||||
|
||||
### Step 1 — Install dependencies
|
||||
```bash
|
||||
pnpm i
|
||||
```
|
||||
|
||||
### Step 2 — Create the `.env` files
|
||||
Each app/package reads its **own** `.env`. Copy each `.env.example` to `.env` and fill
|
||||
the values below (host ports). The secret values are placeholders — **generate each one**
|
||||
with `openssl rand -hex 32` rather than copying the example text. The two
|
||||
`PRIVATE_SERVER_KEY` values **must match**, so generate it once and paste the same value
|
||||
into both files.
|
||||
|
||||
**`apps/api/.env`**
|
||||
```bash
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/classroomio"
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
PUBLIC_SERVER_URL="http://localhost:3002"
|
||||
TRUSTED_ORIGINS="http://localhost:5173" # the dashboard dev origin
|
||||
BETTER_AUTH_SECRET="<generate-with-openssl-rand-hex-32>"
|
||||
AUTH_BEARER_TOKEN="<generate-with-openssl-rand-hex-32>"
|
||||
PRIVATE_SERVER_KEY="<generate-with-openssl-rand-hex-32>" # generate once; use the SAME value in the dashboard
|
||||
```
|
||||
|
||||
**`apps/dashboard/.env`**
|
||||
```bash
|
||||
PUBLIC_SERVER_URL="http://localhost:3002"
|
||||
PRIVATE_SERVER_URL="http://localhost:3002" # used by the SSR auth proxy — required for login
|
||||
PRIVATE_SERVER_KEY="<generate-with-openssl-rand-hex-32>" # must match the API (paste the value generated above)
|
||||
PUBLIC_IS_SELFHOSTED=true
|
||||
```
|
||||
|
||||
**`apps/jobs/.env`** — only if you'll run the background workers (`pnpm api:dev` starts
|
||||
them). Simplest: copy the API env, which already has `REDIS_URL` + `DATABASE_URL`.
|
||||
```bash
|
||||
cp apps/api/.env apps/jobs/.env
|
||||
```
|
||||
|
||||
**`packages/db/.env`** — used by the DB setup scripts (Step 4). Created there.
|
||||
|
||||
> Why so many? The dashboard calls the API through a **server-side proxy**
|
||||
> (`/api/auth/*`), so it needs `PRIVATE_SERVER_URL`. The API's better-auth needs
|
||||
> `BETTER_AUTH_SECRET` (to sign sessions) and `TRUSTED_ORIGINS` (to trust the
|
||||
> dashboard origin). Server-to-server data calls use the shared `PRIVATE_SERVER_KEY`.
|
||||
|
||||
### Step 3 — Start infrastructure (Postgres + Redis)
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml up -d postgres redis
|
||||
```
|
||||
(Optional — object storage for uploads:
|
||||
`docker compose -f docker/docker-compose.yaml --profile minio up -d minio minio-init`.)
|
||||
|
||||
### Step 4 — Set up + seed the database
|
||||
```bash
|
||||
cp packages/db/.env.example packages/db/.env # contains DATABASE_URL for the db scripts
|
||||
pnpm --filter @cio/db db:setup:seed # creates schema + demo data
|
||||
```
|
||||
|
||||
### Step 5 — Build the shared packages once
|
||||
```bash
|
||||
pnpm build
|
||||
```
|
||||
The dashboard/api import workspace libs (`@cio/ui`, `@cio/api`, `@cio/db`, …) from
|
||||
their `dist/`, which only exists after a build. (If `@cio/jobs-worker` fails here on
|
||||
`ffmpegProbeLuma`, that's a known branch bug — see Known issues; it doesn't block
|
||||
dashboard/api.)
|
||||
|
||||
### Step 6 — Run the app (two terminals)
|
||||
```bash
|
||||
pnpm api:dev # API (port 3002) + background workers
|
||||
pnpm dashboard:dev # dashboard UI (port 5173)
|
||||
```
|
||||
> Don't use `pnpm dev` — it tries to start the whole monorepo (~20 services) and
|
||||
> currently trips a turbo concurrency limit. The two scoped commands above are the
|
||||
> day-to-day workflow.
|
||||
|
||||
### Step 7 — Log in
|
||||
Open http://localhost:5173/login → `[email protected]` / `123456`.
|
||||
|
||||
### Email (optional)
|
||||
Email is **off by default** and the app runs fine without it. You only need it to
|
||||
test auth emails (verify/reset) and notifications.
|
||||
|
||||
To enable, set the SMTP vars in **`apps/api/.env`** (auth-flow emails) and
|
||||
**`apps/jobs/.env`** (all other emails) — both can point at the same server:
|
||||
```bash
|
||||
SMTP_HOST= SMTP_PORT= SMTP_USER= SMTP_SENDER= SMTP_PASSWORD=
|
||||
```
|
||||
Any SMTP provider works (Zoho ZeptoMail, Resend, Gmail SMTP, or Mailtrap for
|
||||
throwaway testing). See the inline notes in [`apps/api/.env.example`](apps/api/.env.example)
|
||||
for which emails each process sends.
|
||||
|
||||
**Cost:** most providers have a free tier for low volume; sustained/high volume is
|
||||
paid. For local dev a free sandbox (e.g. Mailtrap) or a provider's free tier is
|
||||
enough — developing costs nothing.
|
||||
|
||||
---
|
||||
|
||||
## 3. Ports
|
||||
|
||||
| Service | URL / Port | Notes |
|
||||
|-------------|-----------------------|----------------------------------|
|
||||
| API | http://localhost:3002 | backend (`API_PORT` default) |
|
||||
| Dashboard | http://localhost:5173 | Vite dev server |
|
||||
| Postgres | localhost:5432 | Docker container `cio-postgres` |
|
||||
| Redis | localhost:6379 | Docker container `cio-redis` |
|
||||
| MinIO | 9000 / console 9001 | only if started (optional) |
|
||||
| jobs-worker | (none) | background worker, no HTTP port |
|
||||
|
||||
## 4. Demo accounts
|
||||
|
||||
Seeded accounts use password **`123456`** (verified against the seed's bcrypt hashes):
|
||||
|
||||
- `[email protected]` / `123456` (admin — "Elon Gates")
|
||||
- `[email protected]`, `[email protected]`, and the `*[email protected]`
|
||||
accounts — also `123456`
|
||||
|
||||
> `[email protected]`'s seed hash doesn't match common passwords — use `[email protected]`.
|
||||
|
||||
---
|
||||
|
||||
## 5. Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| `no such service: db-init` | `db-init` was removed; the `api` container now runs DB setup | Don't reference `db-init`. For host dev use `db:setup:seed` (Step 4). |
|
||||
| `DATABASE_URL or PRIVATE_DATABASE_URL ... required` | `packages/db/.env` missing (db scripts read their own dir) | `cp packages/db/.env.example packages/db/.env` |
|
||||
| `password authentication failed for user "postgres"` | `localhost:5432` is another project's Postgres, not classroomio's | Stop the other container / free port 5432, then recreate: `docker compose -f docker/docker-compose.yaml up -d postgres` |
|
||||
| `Failed to resolve entry for package "@cio/*"` | shared packages not built (`dist/` missing) | `pnpm build`, then re-run the dev commands |
|
||||
| `'rm'/'cp' is not recognized` or `spawn EINVAL` (Windows) | Unix command / `.cmd` spawn in a script | Already fixed in-repo (rimraf, Node copy, `shell:true`). Run `pnpm i` to get latest. |
|
||||
| `invalid task configuration … N persistent tasks … concurrency` | `pnpm dev` starts the whole monorepo past turbo's cap | Use scoped `pnpm api:dev` + `pnpm dashboard:dev` instead |
|
||||
| `ZodError … REDIS_URL ... expected string, received undefined` | `apps/jobs/.env` missing `REDIS_URL` | `cp apps/api/.env apps/jobs/.env` and set `REDIS_URL=redis://localhost:6379` |
|
||||
| `API upstream not configured` (502) on login | dashboard SSR proxy has no upstream | Set `PRIVATE_SERVER_URL=http://localhost:3002` in `apps/dashboard/.env`, restart dashboard |
|
||||
| Login `Invalid email or password` | wrong demo password | Use `[email protected]` / **`123456`** |
|
||||
| Login still fails after upstream fix | API auth env empty | Fill `BETTER_AUTH_SECRET`, `TRUSTED_ORIGINS=http://localhost:5173`, `PUBLIC_SERVER_URL` in `apps/api/.env`, restart API |
|
||||
| Changed a `.env` but nothing changed | env is read at process start | Restart the affected `*:dev` server |
|
||||
|
||||
> After editing any `.env`, **restart** the relevant dev server.
|
||||
|
||||
---
|
||||
|
||||
## 6. Known issues (not setup problems)
|
||||
|
||||
- **`ffmpegProbeLuma` not exported** — branch bug (commit `78ab13f54`):
|
||||
`apps/jobs/src/processors/media/generate-thumbnail.ts` imports `ffmpegProbeLuma`
|
||||
but `apps/jobs/src/utils/ffmpeg.ts` doesn't export it. Crashes the two **media**
|
||||
workers only; API / dashboard / emails / maintenance are fine. For the
|
||||
thumbnail-feature owner.
|
||||
- **ffmpeg/ffprobe not installed** — media jobs need these binaries on `PATH` (or set
|
||||
`FFMPEG_PATH` / `FFPROBE_PATH`). Other workers are unaffected.
|
||||
- **`pnpm dev` concurrency** — full-stack run needs a turbo `--concurrency` bump;
|
||||
intentionally not changed. Use the scoped workflow.
|
||||
|
||||
## 7. Windows notes
|
||||
|
||||
- Use **Git Bash** for interactive commands (`cp`, `rm`, etc.).
|
||||
- The repo's package scripts were made cross-platform (`rimraf` instead of `rm -rf`,
|
||||
a Node postinstall, a Node file-copy, and `shell:true` when scripts spawn pnpm), so
|
||||
`pnpm i` / `pnpm build` work in `cmd.exe`/PowerShell too.
|
||||
|
||||
@@ -0,0 +1,636 @@
|
||||
# ClassroomIO — Feature & Flow Map (QA Audit)
|
||||
|
||||
> **Purpose of this document:** a reverse‑engineered, citation‑backed map of every
|
||||
> feature in the ClassroomIO application, produced for QA to build test cases and
|
||||
> record demo videos. **Files are the source of truth.** Every claim is backed by a
|
||||
> file reference. Claims that could not be traced to code are marked
|
||||
> `⚠️ UNVERIFIED`. Ambiguities live in **§7 Open Questions**, not in the body.
|
||||
>
|
||||
> Reference format: `` `path/to/file` `` → symbol/lines — what it proves.
|
||||
> Scope (per QA request): full depth on the user‑facing product
|
||||
> (`apps/dashboard`, `apps/api`, `apps/website`, `apps/embeds` + the packages that
|
||||
> back features); a lighter **Supporting Infrastructure** summary for the rest.
|
||||
> PRD folders (`prd/*`) are used only to cross‑check intent — code wins on conflict.
|
||||
|
||||
---
|
||||
|
||||
## 1. System Overview
|
||||
|
||||
**What it is.** ClassroomIO is an open‑source **Learning Management System (LMS) for
|
||||
companies** — compliance/employee training, customer education, and partner
|
||||
certification. It supports multi‑organization workspaces, courses/lessons/exercises,
|
||||
programs (cohorts), certificates, an AI course builder + in‑lesson tutor, a public
|
||||
REST API, an MCP server, and embeddable course widgets. It can be **self‑hosted** or
|
||||
run as the **cloud** product. (`README.md` → lines 11–50 — feature summary;
|
||||
`README.md` → 84–91 — monorepo apps.)
|
||||
|
||||
**Tech stack** (cited from manifests):
|
||||
- **Monorepo**: pnpm + Turbo. `package.json` → root scripts/workspaces;
|
||||
`pnpm-workspace.yaml` → packages globs (`apps/*`, `packages/*`,
|
||||
`packages/course-app/src/*`); `turbo.json`.
|
||||
- **Frontend**: SvelteKit + Svelte 5 + TailwindCSS. `README.md` → 52–58 ("Built With");
|
||||
`apps/dashboard/svelte.config.js`.
|
||||
- **Backend API**: Hono (TypeScript). `apps/api/src/app.ts` → `new Hono()` app;
|
||||
`ARCHITECHTURE.md` → layered architecture (Routes → Services → Queries).
|
||||
- **Database**: PostgreSQL via Drizzle ORM. `packages/db/src/schema.ts` (~3,400 lines
|
||||
of `pgTable`/`pgEnum`); `packages/db/src/drizzle.ts`.
|
||||
- **Auth**: Better Auth (sessions, OAuth, SSO). `apps/api/src/app.ts` → 12, 107–188
|
||||
(`auth.handler`, `/api/auth/*`); `packages/db/src/auth.ts`.
|
||||
- **Background jobs**: BullMQ + Redis. `apps/jobs`, `apps/api/src/routes/admin/queues.ts`
|
||||
(`mountQueueDashboard`), `mediaJob`/`jobStep`/`deadLetterJob` tables in `schema.ts`.
|
||||
- **Object storage**: S3‑compatible / MinIO. `.env.example` → 77–89 (OBJECT_STORAGE_*);
|
||||
`README.md` → 176–191.
|
||||
- **Billing**: Polar. `apps/dashboard/src/routes/api/polar/*`.
|
||||
- **AI providers**: OpenAI / Google (Gemini) / Anthropic. `.env.example` → 110–115;
|
||||
`packages/utils/src/agent-models` (per `README.md` → 236).
|
||||
|
||||
**High‑level architecture** (`ARCHITECHTURE.md` → 9–51):
|
||||
```
|
||||
SvelteKit dashboard (apps/dashboard) ──RPC (Hono client)──▶ Hono API (apps/api)
|
||||
│ server-side SSR proxy (.server.ts, PRIVATE_SERVER_KEY) │
|
||||
│ ▼
|
||||
Public org sites / embeds Services → Queries (Drizzle)
|
||||
(apps/website, apps/embeds) │
|
||||
▼
|
||||
PostgreSQL · Redis/BullMQ · S3/MinIO
|
||||
```
|
||||
- Layered API: **Routes** (HTTP/validation/auth) → **Services** (business logic/
|
||||
transactions) → **Queries** (pure Drizzle). `ARCHITECHTURE.md` → 44–50.
|
||||
- Shared **Zod validation** in `packages/utils/src/validation/*`, used by both API and
|
||||
dashboard. `ARCHITECHTURE.md` → 45; `AGENTS.md` → 17–25.
|
||||
- Dashboard talks to the API through a typed Hono RPC client and an SSR auth proxy
|
||||
authenticated with `PRIVATE_SERVER_KEY`. `README.md` → 149–156.
|
||||
|
||||
---
|
||||
|
||||
## 2. Feature Inventory
|
||||
|
||||
Status legend: **Complete** = route+service+schema present; **Partial** = present but
|
||||
gated/limited or doc claims exceed code; **Stub/Planned** = PRD only, little/no code.
|
||||
|
||||
| # | Feature | Purpose | Primary files | Status |
|
||||
|---|---------|---------|---------------|--------|
|
||||
| 1 | Authentication & sessions | Email/password, OAuth, session mgmt | `apps/api/src/app.ts:107`, `packages/db/src/auth.ts` | Complete |
|
||||
| 2 | Signup gating | Org‑scoped/invite‑only/self‑host bootstrap rules | `apps/api/src/middlewares/signup-guard.ts` | Complete |
|
||||
| 3 | Onboarding & org creation | Create profile + first organization | `apps/api/src/services/onboarding.ts`, `apps/dashboard/src/routes/(auth)/onboarding` | Complete |
|
||||
| 4 | Organizations (workspaces) | Multi‑org, settings, members, plan | `apps/api/src/routes/organization/organization.ts`, `schema.ts:1949` | Complete |
|
||||
| 5 | Team / people management | Invite teachers, assign roles | `apps/api/src/routes/course/people.ts`, `apps/dashboard/src/lib/features/people` | Complete |
|
||||
| 6 | Courses | Create/manage courses, sections, content | `apps/api/src/routes/course/course.ts`, `schema.ts:634` | Complete |
|
||||
| 7 | Lessons & lesson editor | Lesson content, multilingual, comments | `apps/api/src/routes/course/lesson.ts`, `.../lesson-language.ts`, `schema.ts:956` | Complete |
|
||||
| 8 | Exercises & questions | Quizzes, question types, sections, grading | `apps/api/src/routes/course/exercise.ts`, `.../submission.ts`, `.../mark.ts`, `packages/question-types` | Complete |
|
||||
| 9 | Course people / enrollment | Enroll students, roster | `apps/api/src/routes/course/people.ts` | Complete |
|
||||
| 10 | Course invites | Email + link invites with audit | `apps/api/src/routes/course/invite.ts`, `apps/api/src/services/course/invite.ts`, `schema.ts:1282` | Complete |
|
||||
| 11 | Org invites | Invite org members (EMAIL/LINK) | `apps/api/src/routes/invite/invite.ts`, `schema.ts:1384` | Complete |
|
||||
| 12 | Compliance training | Deadlines, renewals, grace, records | `apps/api/src/routes/course/compliance.ts`, `apps/api/src/routes/internal`, `schema.ts:768` (`courseCompletionRecord`) | Complete |
|
||||
| 13 | Certificates | Issue branded certificates | `packages/certificates/src/render.ts`, `schema.ts:850` (`courseCertificateIssue`) | Complete |
|
||||
| 14 | Programs (cohorts) | Group courses, goals, members, newsfeed | `apps/api/src/routes/program/program.ts`, `schema.ts:2646` | Complete |
|
||||
| 15 | Community Q&A | Per‑course questions/answers | `apps/api/src/routes/community/community.ts`, `schema.ts:1558` | Complete |
|
||||
| 16 | Course newsfeed | Course/program announcements + comments | `apps/api/src/routes/course/newsfeed.ts`, `schema.ts:1605` | Complete |
|
||||
| 17 | Attendance | Track group/live‑class attendance | `apps/api/src/routes/course/attendance.ts`, `schema.ts:321` (`groupAttendance`) | Complete |
|
||||
| 18 | AI Course Assistant | In‑course AI authoring/plan/edit chat | `apps/api/src/routes/agent/*`, `packages/ai-assistant`, `schema.ts:3163` (`aiAgentRun`) | Complete (key‑gated) |
|
||||
| 19 | AI Lesson Tutor | In‑lesson learner AI helper + fair‑use caps | `apps/api/src/routes/course/ai-tutor.ts`, `apps/api/src/routes/organization/ai-tutor.ts`, `schema.ts:3013` (`aiTutorMessageCount`) | Complete (key‑gated) |
|
||||
| 20 | AI credits / token billing | Token usage, balance, purchases | `schema.ts:2919` (`aiTokenUsage`), `apps/dashboard/src/lib/features/agent/api/credit-purchase.server.ts` | Complete (cloud billing) |
|
||||
| 21 | Course import | Import from prompt/PDF/course | `apps/api/src/routes/organization/course-import.ts`, `apps/api/src/services/course-import`, `schema.ts:2603` | Complete |
|
||||
| 22 | Media manager | Upload/manage video/docs/media, transcripts | `apps/api/src/routes/media/media.ts`, `.../transcripts.ts`, `.../hls.ts`, `apps/dashboard/src/lib/features/media`, `schema.ts:1028` (`asset`) | Complete |
|
||||
| 23 | Tags | Tag groups/tags/assignments for courses | `apps/api/src/routes/organization/tags.ts`, `schema.ts:2158` | Complete |
|
||||
| 24 | Org public site (org‑site) | Branded public course catalog/pages | `apps/api/src/routes/org-site`, `apps/dashboard/src/routes/(org-site)` | Complete |
|
||||
| 25 | Custom domains | Cloudflare custom domain mapping | `apps/api/src/routes/domain/domain.ts`, `apps/tenant-router` | Complete |
|
||||
| 26 | Course widget embed | Embeddable course catalog widget | `apps/api/src/routes/widgets`, `apps/dashboard/src/routes/(app)/widgets`, `apps/embeds`, `schema.ts:2255` (`widget`) | Complete |
|
||||
| 27 | Public REST API (v1) | API‑key automation: courses, audience | `apps/api/src/routes/v1/*`, `apps/api/src/services/v1`, `schema.ts:2539` (`organizationApiKey`) | Complete |
|
||||
| 28 | Automation / API keys | Issue mcp/api/zapier keys with scopes | `apps/api/src/routes/organization/automation.ts`, `apps/api/src/middlewares/automation-key*.ts` | Complete |
|
||||
| 29 | MCP server | `@classroomio/mcp` AI‑native course authoring | `packages/mcp/src/*` | Complete |
|
||||
| 30 | Enterprise SSO | OIDC SSO (Okta/Google Workspace/Auth0) | `apps/api/src/routes/organization/sso.ts`, `apps/api/src/routes/sso/discovery.ts`, `schema.ts:2479` (`organizationSsoConfig`) | Complete (licensed) |
|
||||
| 31 | Token auth | Token‑based auth policy | `apps/api/src/routes/organization/token-auth.ts`, `schema.ts:2513` (`organizationTokenAuth`) | Complete (licensed) |
|
||||
| 32 | Licensing | Self‑host license‑gated features | `apps/api/src/routes/license.ts`, `apps/api/src/middlewares/license.ts`, `packages/utils/src/license/*` | Complete |
|
||||
| 33 | Billing (Polar) | Subscriptions + AI token packs | `apps/dashboard/src/routes/api/polar/*` | Complete (cloud only) |
|
||||
| 34 | Analytics | Login/page events, org/course/country daily | `apps/api/src/routes/dash`, `apps/api/src/services/analytics`, `schema.ts:167–285` | Complete |
|
||||
| 35 | Email / notifications | Transactional emails via jobs | `apps/api/src/routes/mail`, `packages/email`, `apps/api/src/services/jobs` | Complete |
|
||||
| 36 | Background jobs | Media/email queues, BullMQ dashboard | `apps/jobs`, `apps/api/src/routes/jobs`, `apps/api/src/routes/admin/queues.ts` | Complete |
|
||||
| 37 | Unsplash image search | Image picker for content | `apps/api/src/routes/unsplash/unsplash.ts` | Complete |
|
||||
| 38 | Marketing website | classroomio.com landing | `apps/website` | Complete |
|
||||
| 39 | Documentation site | classroomio.com/docs | `apps/docs` | Complete |
|
||||
| 40 | Outbound webhooks | Emit `certificate.issued`, etc. | `prd/webhooks` (todo) | **Stub/Planned ⚠️** |
|
||||
| 41 | SCORM support | SCORM package support | `prd/scorm-support` (todo) | **Stub/Planned ⚠️** |
|
||||
| 42 | Notification system | In‑app notifications | `prd/notification-system` (todo) | **Stub/Planned ⚠️** |
|
||||
|
||||
> Items 40–42 are listed in `PRD-TRACKER.md` → 9, 16, 24 as **todo**; treat as not yet
|
||||
> shipped. See **§7**.
|
||||
|
||||
---
|
||||
|
||||
## 3. Per‑Feature Detail
|
||||
|
||||
### 3.1 Authentication & Sessions
|
||||
- **Purpose:** authenticate users (email/password + OAuth) and attach org roles to the session.
|
||||
- **How it works:** All `/api/auth/*` requests are handled by Better Auth via
|
||||
`auth.handler`. `apps/api/src/app.ts` → 107–188. A global middleware resolves the
|
||||
session on every request and sets `user`, `session`, and `orgRoles` into context.
|
||||
`apps/api/src/app.ts` → 60–97. The app rebuilds the request host from
|
||||
`X-Forwarded-Host`/`-Proto` (Cloudflare proxy) and uses a Redis "oauth handoff" to
|
||||
shrink large OAuth callback cookies into a short token. `apps/api/src/app.ts` → 110–173.
|
||||
- **User flow:** Login page → submit email/password (or OAuth) → Better Auth sets
|
||||
session cookie → dashboard reads `/session`. `apps/dashboard/src/routes/(auth)/login`;
|
||||
`apps/api/src/app.ts` → 189–199 (`/session`).
|
||||
- **Key files:** `apps/api/src/app.ts`, `packages/db/src/auth.ts`,
|
||||
`packages/db/src/auth/hooks/create-profile.ts`.
|
||||
- **Edge cases:** OAuth cookie > 500 chars routed through Redis token (`app.ts:160`);
|
||||
`getSession` failures fall through as anonymous (`app.ts:71–84`); session carries
|
||||
`orgRoles` map used by RBAC middlewares (`app.ts:88`).
|
||||
|
||||
### 3.2 Signup Gating (`signupGuard`)
|
||||
- **Purpose:** enforce org‑level and deployment‑level signup rules server‑side.
|
||||
- **How it works:** `apps/api/src/middlewares/signup-guard.ts` → 129–159. With a
|
||||
`cio-org-id` header: rejects when `org.disableSignup` (403 `SIGNUP_DISABLED`, line 148)
|
||||
or when `settings.signup.inviteOnly` and the email lacks an active invite (403
|
||||
`INVITE_REQUIRED`, lines 97–113). Without the header: cloud passes through; **self‑hosted
|
||||
requires org context** unless no org exists yet (bootstrap) or the email already belongs
|
||||
to the org. Lines 75–91 (`canProceedWithoutOrgContext`).
|
||||
- **User flow:** Signup form → POST `/api/auth/sign-up/*` (guarded, `app.ts:106`) → allowed
|
||||
/ `SIGNUP_DISABLED` / `INVITE_REQUIRED` / `ORG_CONTEXT_REQUIRED`.
|
||||
- **Edge cases (tester probes):** invite‑only org without invite; disabled signup;
|
||||
self‑hosted with no `cio-org-id` and an existing org; unparseable body → 400 `INVALID_REQUEST`.
|
||||
|
||||
### 3.3 Onboarding & Organization Creation
|
||||
- **Purpose:** create the user profile and first organization, then send a welcome email.
|
||||
- **How it works:** `apps/api/src/services/onboarding.ts`. `createOrganizationWithOwner`
|
||||
validates site‑name uniqueness (409 `SITENAME_EXISTS`, lines 36–40), creates org + admin
|
||||
member in a transaction (lines 44–61), and on **self‑hosted** assigns the org an
|
||||
`ENTERPRISE` plan with provider `selfhosted` (lines 64–77). **Self‑hosted blocks a second
|
||||
org** (403, lines 28–33). `completeOnboarding` enqueues a `welcome` email (lines 121–134).
|
||||
- **User flow:** Signup → `(auth)/onboarding` → enter name/org/site name → org+admin created → dashboard.
|
||||
- **Edge cases:** duplicate site name (409 + Postgres `23505` fallback, lines 93–95);
|
||||
second‑org attempt on self‑host; profile not found (404).
|
||||
|
||||
### 3.4 Organizations (Workspaces)
|
||||
- **Purpose:** the top‑level tenant; owns courses, members, settings, plan, branding.
|
||||
- **How it works:** `apps/api/src/routes/organization/organization.ts` (CRUD + settings);
|
||||
`organization` table `schema.ts:1949` (siteName, settings, branding, `disableSignup`).
|
||||
Members live in `organizationmember` (`schema.ts:1754`) with `roleId` FK to `role`.
|
||||
- **User flow:** Org switcher → settings → update name/site/branding/signup policy.
|
||||
`apps/dashboard/src/lib/features/ui/sidebar/org-sidebar/org-switcher.svelte`;
|
||||
`apps/dashboard/src/lib/features/settings`.
|
||||
- **Edge cases:** unique `siteName`; self‑hosted = single org (see §5); org settings drive
|
||||
signup gating (§3.2).
|
||||
|
||||
### 3.5 Courses, Sections & Content
|
||||
- **Purpose:** author and deliver courses (self‑paced, live‑class, compliance, public).
|
||||
- **How it works:** course sub‑routers under `apps/api/src/routes/course/` — `course.ts`,
|
||||
`section.ts`, `content.ts`, `lesson.ts`, `exercise.ts`, `people.ts`, `newsfeed.ts`,
|
||||
`attendance.ts`, `compliance.ts`, `mark.ts`, `submission.ts`, `invite.ts`,
|
||||
`lesson-language.ts`, `payment-request.ts`, `presign.ts`, `ai-tutor.ts`, `katex.ts`
|
||||
(`index.ts` composes them). Course type enum `SELF_PACED|LIVE_CLASS|COMPLIANCE|PUBLIC`
|
||||
(`schema.ts:26`). Course row `schema.ts:634`.
|
||||
- **User flow:** Courses → New course → add sections/lessons → add exercises → publish →
|
||||
students enroll. `apps/dashboard/src/routes/(app)/courses/[id]`.
|
||||
- **Edge cases:** course type drives compliance/public behavior; locale enum (`schema.ts:27`,
|
||||
10 languages) drives multilingual lessons.
|
||||
|
||||
### 3.6 Lessons & Multilingual Editor
|
||||
- **Purpose:** lesson content with notes/slides/video, comments, translations.
|
||||
- **How it works:** `lesson` (`schema.ts:956`), `lessonComment` (`schema.ts:516`),
|
||||
`lessonCompletion` (`schema.ts:1490`), `lessonLanguage` + `…History` (`schema.ts:1706`,
|
||||
`1732`). Routes: `apps/api/src/routes/course/lesson.ts`, `.../lesson-language.ts`.
|
||||
PRDs `prd/lesson-edit [DONE]`, `prd/lessons-list [DONE]` confirm intent.
|
||||
- **User flow:** Open lesson → edit content → add translation → student marks complete.
|
||||
- **Edge cases:** language history retained; completion tracked per student.
|
||||
|
||||
### 3.7 Exercises, Question Types & Grading
|
||||
- **Purpose:** quizzes/assessments with multiple question types and grading.
|
||||
- **How it works:** `exercise` (`schema.ts:1175`), `exerciseSection` (`schema.ts:1217`),
|
||||
`question`/`questionType`/`questionAnswer` (`schema.ts:1803–1913`), `submission`/
|
||||
`submissionstatus` (`schema.ts:571,617`), `exerciseTemplate` (`schema.ts:2401`).
|
||||
Routes `exercise.ts`, `submission.ts`, `mark.ts`. Shared types in `packages/question-types`.
|
||||
PRDs: `exercise-question-types`, `exercise-sections [DONE]`, `video-recording-question-type [DONE]`.
|
||||
- **User flow:** Author exercise → add questions (incl. video‑recording type) → student
|
||||
submits → tutor/admin marks → status recorded.
|
||||
- **Edge cases:** section‑based exercises; video‑recording answers (media pipeline);
|
||||
`prd/selective-exercise-assignment` is **todo** (assign specific exercises to specific
|
||||
students) — verify it is *not* yet available.
|
||||
|
||||
### 3.8 Enrollment, Course Invites & Secure Invites
|
||||
- **Purpose:** add students to courses by email or shareable link, with audit trail.
|
||||
- **How it works:** `apps/api/src/services/course/invite.ts`; `courseInvite`
|
||||
(`schema.ts:1282`) + `courseInviteAudit` (`schema.ts:1342`); event‑type enum
|
||||
`schema.ts:53`. Org invites: `organizationInvite` (`schema.ts:1384`, EMAIL/LINK enum
|
||||
`schema.ts:71`) + `…Audit` (`schema.ts:1448`), routes `apps/api/src/routes/invite/invite.ts`.
|
||||
PRD `secure-student-invites [DONE]`.
|
||||
- **User flow:** Course → People → Invite (email or link) → invitee accepts via
|
||||
`apps/dashboard/src/routes/invite/[hash]` or `/invite/link/[hash]` → enrolled.
|
||||
- **Edge cases:** invite expiry/custom expiry (`AGENTS.md:231` references
|
||||
`customExpiresAt`); link vs email; audit events recorded; invite‑only signup interplay (§3.2).
|
||||
|
||||
### 3.9 Compliance Training
|
||||
- **Purpose:** deadline tracking, renewals/retake intervals, grace periods, completion records.
|
||||
- **How it works:** `apps/api/src/routes/course/compliance.ts` and internal compliance
|
||||
router `apps/api/src/routes/internal` (`AGENTS.md:87–98`); `courseCompletionRecord`
|
||||
(`schema.ts:768`), `courseCompletionNotificationEvent` (`schema.ts:824`),
|
||||
`courseCertificateIssue` (`schema.ts:850`). Course type `COMPLIANCE` (`schema.ts:26`).
|
||||
PRD `compliance-training-platform [DONE]`.
|
||||
- **User flow:** Mark course as compliance → set deadlines/renewal → learners complete →
|
||||
completion record + certificate; renewal/grace events tracked.
|
||||
- **Edge cases:** grace periods, renewal intervals, waivers (per README 21), notification events.
|
||||
|
||||
### 3.10 Certificates
|
||||
- **Purpose:** issue branded completion certificates with custom IDs.
|
||||
- **How it works:** `packages/certificates/src/render.ts` + `templates/` render the
|
||||
certificate; `courseCertificateIssue` (`schema.ts:850`) records issuance.
|
||||
- **User flow:** Learner completes course → certificate issued → downloadable/branded.
|
||||
- **Edge cases:** custom certificate IDs (README 21); template selection.
|
||||
|
||||
### 3.11 Programs (Cohorts) & Goals
|
||||
- **Purpose:** group courses into cohorts with goals, members, and progress tracking.
|
||||
- **How it works:** `apps/api/src/routes/program/program.ts`; tables `program`
|
||||
(`schema.ts:2646`), `programCourse` (`2677`), `programMember` (`2704`), `programNewsfeed`
|
||||
+`Comment` (`2739`,`2767`), `programGoal`/`…Assignment` (`2829`,`2878`) with goal‑type/
|
||||
deadline/status enums (`2803–2828`), status enum `ACTIVE|INACTIVE|ARCHIVED` (`2644`).
|
||||
Middlewares `program-member.ts`, `program-team-member.ts`. PRD `programs [DONE]`.
|
||||
- **User flow:** Create program → add courses + members → set goals/deadlines → track
|
||||
progress → cohort newsfeed. `apps/dashboard/src/routes/(app)/programs/[id]`.
|
||||
- **Edge cases:** goal deadline kinds; assignment status; archived programs.
|
||||
|
||||
### 3.12 Community Q&A & Newsfeed
|
||||
- **Purpose:** per‑course discussion (questions/answers) and announcements.
|
||||
- **How it works:** `apps/api/src/routes/community/community.ts` with route‑specific
|
||||
`middlewares/question-author-or-team` (`ARCHITECHTURE.md:241–252`); tables
|
||||
`communityQuestion`/`communityAnswer` (`schema.ts:1558`,`1522`), `courseNewsfeed`/
|
||||
`…Comment` (`1605`,`1632`). Comment‑author middlewares `newsfeed-comment-author-or-team.ts`,
|
||||
`program-newsfeed-comment-author-or-team.ts`.
|
||||
- **User flow:** Course → Community → ask/answer; team can moderate. Newsfeed posts +
|
||||
comments at course and program level.
|
||||
- **Edge cases:** author‑or‑team authorization (only author or course team can edit/delete).
|
||||
|
||||
### 3.13 AI Course Assistant (authoring chat)
|
||||
- **Purpose:** in‑course AI that generates outlines, lesson content, and edits.
|
||||
- **How it works:** `apps/api/src/routes/agent/{agent,history,runs}.ts`; `packages/ai-assistant`;
|
||||
run tracking in `aiAgentRun`/`…Step`/`…Event` (`schema.ts:3163`,`3225`,`3259`),
|
||||
conversation/model context/documents (`3083`,`3106`,`3138`). **Disabled until a provider
|
||||
key is set**; `GET /agent/status` flips to `enabled:true` when any of OPENAI/GOOGLE/
|
||||
ANTHROPIC key is present. `README.md:223–238`. Selected model persisted in `localStorage`
|
||||
(`classroomio-ai-chat-model`), provider resolved server‑side; missing key → 503
|
||||
`AI_NOT_CONFIGURED` (`README.md:236`).
|
||||
- **User flow:** Course → AI button (only shown when enabled) → chat to build/edit course.
|
||||
- **Edge cases:** no key → AI button hidden / 503; per‑model provider resolution; optional
|
||||
Tinybird observability (`TINYBIRD_TOKEN`).
|
||||
|
||||
### 3.14 AI Lesson Tutor + Fair‑Use Caps
|
||||
- **Purpose:** in‑lesson AI helper for learners, with usage caps.
|
||||
- **How it works:** `apps/api/src/routes/course/ai-tutor.ts`,
|
||||
`apps/api/src/routes/organization/ai-tutor.ts`; `aiTutorMessageCount` (`schema.ts:3013`)
|
||||
+ `aiTutorCapEvent` (`3050`). Settings UI `apps/dashboard/src/lib/features/ai-tutor-settings`.
|
||||
PRD `ai-tutor-fair-use`.
|
||||
- **User flow:** Learner in lesson → ask tutor → counts toward cap → cap event when exceeded.
|
||||
- **Edge cases:** per‑org cap configuration; cap‑exceeded behavior.
|
||||
|
||||
### 3.15 AI Credits / Token Billing
|
||||
- **Purpose:** meter AI token usage and sell token packs.
|
||||
- **How it works:** `aiTokenUsage` (`schema.ts:2919`), `aiCreditBalance` (`2950`),
|
||||
`aiCreditPurchase` (`2974`); purchases recorded by Polar `order.paid` webhook →
|
||||
`CreditPurchaseApiServer.recordPurchase`. `apps/dashboard/src/routes/api/polar/webhook/+server.ts`
|
||||
→ 33–82; token‑pack constants `@cio/utils/plans` (`TOKEN_PACK.TOKENS_PER_UNIT`, line 53).
|
||||
- **User flow:** Buy tokens (`/api/polar/buy-tokens`) → Polar checkout → webhook credits org balance.
|
||||
- **Edge cases:** **cloud only** (Polar); quantity/metadata parsing; UUID validation of
|
||||
`triggeredBy` (lines 61–66).
|
||||
|
||||
### 3.16 Course Import
|
||||
- **Purpose:** create courses from a prompt, PDF, or existing course.
|
||||
- **How it works:** `apps/api/src/routes/organization/course-import.ts`,
|
||||
`apps/api/src/services/course-import`; `courseImportDraft` (`schema.ts:2603`) with source
|
||||
enum `prompt|pdf|course` (`schema.ts:29`) and draft status `DRAFT|PUBLISHED|ARCHIVED` (`30`).
|
||||
- **User flow:** Org → Import course → choose source → review draft → publish.
|
||||
- **Edge cases:** draft lifecycle; PDF parsing pipeline (jobs/media).
|
||||
|
||||
### 3.17 Media Manager (video/docs, transcripts, HLS)
|
||||
- **Purpose:** upload and manage media; transcode video to HLS; generate transcripts.
|
||||
- **How it works:** routes `media/media.ts`, `transcripts.ts`, `hls.ts`, `course/presign.ts`
|
||||
(S3 presigned uploads). Tables `asset`/`assetUsage` (`schema.ts:1028`,`1086`),
|
||||
`mediaJob`/`jobStep`/`mediaTranscript`/`deadLetterJob` (`schema.ts:3297`,`3357`,`3393`,`3447`),
|
||||
`videoTranscripts` (`916`). Jobs processed by `apps/jobs`. Dashboard
|
||||
`apps/dashboard/src/lib/features/media`. PRD `media-manager [DONE]`. Storage via MinIO/S3
|
||||
(`.env.example:77–89`).
|
||||
- **User flow:** Media → upload → background transcode/transcribe → use asset in lesson.
|
||||
- **Edge cases:** job retries / dead‑letter; HLS cross‑origin headers (`app.ts:48–54`);
|
||||
Google Drive picker optional (`.env.example:99–101`).
|
||||
|
||||
### 3.18 Tags
|
||||
- **Purpose:** categorize courses via tag groups/tags.
|
||||
- **How it works:** `apps/api/src/routes/organization/tags.ts`; `tagGroup`/`tag`/
|
||||
`tagAssignment` (`schema.ts:2158`,`2188`,`2225`). PRD `tags-feature [DONE]`.
|
||||
- **User flow:** Org → Tags → create groups/tags → assign to courses → filter catalog.
|
||||
|
||||
### 3.19 Org Public Site (org‑site) & Theming
|
||||
- **Purpose:** branded public catalog and course landing pages for an org.
|
||||
- **How it works:** API `apps/api/src/routes/org-site` (mounted `/org-site/course`,
|
||||
`app.ts:217`); dashboard public routes `apps/dashboard/src/routes/(org-site)/{course,courses}`.
|
||||
Theme picker PRD `org-landing-page-theme-picker [DONE]`.
|
||||
- **User flow:** Visitor → org public site → browse courses → view course → enroll/sign up.
|
||||
- **Edge cases:** self‑host vs cloud routing differences (see §5); org branding/theme.
|
||||
|
||||
### 3.20 Custom Domains
|
||||
- **Purpose:** map a customer's own domain to their org site.
|
||||
- **How it works:** `apps/api/src/routes/domain/domain.ts` (mounted `/domain`, `app.ts:203`);
|
||||
routing app `apps/tenant-router`. PRD `custom-domain-cloudflare [DONE]`.
|
||||
- **User flow:** Org settings → add custom domain → Cloudflare mapping → site served on domain.
|
||||
|
||||
### 3.21 Course Widget Embed
|
||||
- **Purpose:** embed an org's course catalog on any external website.
|
||||
- **How it works:** API `apps/api/src/routes/widgets` (mounted `/widgets`, `app.ts:221`);
|
||||
dashboard `apps/dashboard/src/routes/(app)/widgets/[widgetId]` + `widget-preview`; build
|
||||
artifacts `apps/embeds` (`embeds:publish`, `README.md:256–257`). Tables `widget`/
|
||||
`widgetCourse`/`widgetVersion` (`schema.ts:2255`,`2305`,`2336`) with status/layout/
|
||||
selection enums (`42–52`). PRD `course-widget-embed [DONE]`.
|
||||
- **User flow:** Widgets → create widget → choose courses/layout → publish → copy embed
|
||||
snippet (`embed-preview.html`).
|
||||
- **Edge cases:** draft vs published widget; manual vs published course selection mode.
|
||||
|
||||
### 3.22 Public REST API (v1) & Automation Keys
|
||||
- **Purpose:** programmatic access for integrations/automation.
|
||||
- **How it works:** `apps/api/src/routes/v1/{courses,audience}.ts` mounted at
|
||||
`/public-api/v1` (`app.ts:218`, `v1/index.ts`). Auth via `automationKeyMiddleware` +
|
||||
`automationKeyScopesMiddleware(['public_api:*'])` on every route; OpenAPI documented via
|
||||
`hono-openapi`. Courses endpoints: list/create/get/update/delete, list students, export,
|
||||
get/put structure (`v1/courses.ts`). Keys: `organizationApiKey` (`schema.ts:2539`, type
|
||||
`mcp|api|zapier`), usage tracked in `organizationAutomationUsage` (`2574`); managed via
|
||||
`apps/api/src/routes/organization/automation.ts`. PRD `public-api [DONE]`.
|
||||
- **User flow:** Org → API keys → create key with scopes → call `/public-api/v1/...` with key.
|
||||
- **Edge cases:** scope enforcement (403 on missing scope); usage categories `read|write|publish`
|
||||
(`schema.ts:32`); per‑org isolation via `orgId` from key context.
|
||||
|
||||
### 3.23 MCP Server
|
||||
- **Purpose:** AI‑native course authoring through the Model Context Protocol.
|
||||
- **How it works:** `packages/mcp/src/{index.ts,api-client.ts,config.ts,tools/}`; published
|
||||
as `@classroomio/mcp` (`README.md:258`). Talks to the public API using an automation key.
|
||||
PRD `mcp-course-authoring [DONE]`.
|
||||
- **User flow:** Configure MCP client with API key → AI tools create/update courses.
|
||||
|
||||
### 3.24 Enterprise SSO & Token Auth (licensed)
|
||||
- **Purpose:** OIDC SSO and token‑based auth for enterprise self‑host.
|
||||
- **How it works:** `apps/api/src/routes/organization/sso.ts`,
|
||||
`apps/api/src/routes/sso/discovery.ts` (mounted `/organization/sso`, `/sso`, `app.ts:211,213`);
|
||||
`organizationSsoConfig` (`schema.ts:2479`), provider enum `OKTA|GOOGLE_WORKSPACE|AUTH0`
|
||||
(`schema.ts:2444`), `organizationAuthPolicy` (`2447`). Token auth:
|
||||
`apps/api/src/routes/organization/token-auth.ts`, `organizationTokenAuth` (`2513`).
|
||||
**License‑gated on self‑host** via `requireLicense('sso'|'token-auth')`
|
||||
(`apps/api/src/middlewares/license.ts`). PRD `enterprise-sso [DONE]`.
|
||||
- **User flow:** Org → SSO settings → register IdP (discovery fetches
|
||||
`.well-known/openid-configuration`) → users sign in via IdP.
|
||||
- **Edge cases:** trusted OIDC origins allow‑listed (`packages/db/src/constants.ts:14–24`);
|
||||
cloud has no license restriction (see §5).
|
||||
|
||||
### 3.25 Licensing
|
||||
- **Purpose:** gate Enterprise self‑host features behind a license key.
|
||||
- **How it works:** `apps/api/src/routes/license.ts` (mounted `/license`, `app.ts:209`),
|
||||
`apps/api/src/services/license.ts` (`isFeatureLicensed`), feature ids `sso`,
|
||||
`token-auth`, `no-tracking` (`packages/utils/src/license/constants.ts:8–10`),
|
||||
plan→feature mapping `packages/utils/src/license/plan-features.ts`. Dashboard side
|
||||
`apps/dashboard/src/lib/features/license/api/license.svelte.ts` (`licenseApi.hasAccess`).
|
||||
- **User flow:** Set `LICENSE_KEY` (`.env.example:117–118`) → licensed features unlock.
|
||||
- **Edge cases:** `requireLicense` is a **no‑op on cloud** (`license.ts:15–19`).
|
||||
|
||||
### 3.26 Billing (Polar) — cloud
|
||||
- **Purpose:** paid subscriptions and AI token packs.
|
||||
- **How it works:** `apps/dashboard/src/routes/api/polar/{subscribe,buy-tokens,portal,webhook}`.
|
||||
Webhook handles `order.paid` (token packs → credit balance) and `subscription.*`
|
||||
(create/update/cancel org plan). `webhook/+server.ts` → 30–168. Plans enum
|
||||
`EARLY_ADOPTER|ENTERPRISE|BASIC` (`schema.ts:28`), org plan stored in `organizationPlan`
|
||||
(`schema.ts:1667`).
|
||||
- **User flow:** Upgrade → Polar checkout → webhook activates/updates plan; manage via portal.
|
||||
- **Edge cases:** **cloud only**; subscription cancel → `cancelOrgPlan`; webhook secret
|
||||
`POLAR_WEBHOOK_SECRET` required.
|
||||
|
||||
### 3.27 Analytics
|
||||
- **Purpose:** usage analytics for orgs/courses.
|
||||
- **How it works:** `apps/api/src/routes/dash` + `apps/api/src/services/analytics`; tables
|
||||
`analyticsLoginEvents`, `analyticsPageEvent`, `analyticsOrgDaily`, `analyticsCourseDaily`,
|
||||
`analyticsCountryDaily` (`schema.ts:167–285`). Dashboard `apps/dashboard/src/lib/features/analytics`.
|
||||
Client tracking (PostHog/Umami) gated by deployment mode / license (§5).
|
||||
- **User flow:** Org → Analytics dashboard → view login/page/course metrics.
|
||||
|
||||
### 3.28 Email / Notifications & Background Jobs
|
||||
- **Purpose:** transactional email and async processing.
|
||||
- **How it works:** `apps/api/src/routes/mail`, templates in `packages/email`,
|
||||
enqueued via `enqueueTransactionalEmail` (`apps/api/src/services/jobs`,
|
||||
`onboarding.ts:128`). Workers in `apps/jobs`; BullMQ dev dashboard at `/admin/queues`
|
||||
(`apps/api/src/routes/admin/queues.ts`, `app.ts:232–234`). Email providers: Zoho/ZeptoMail
|
||||
or SMTP (`.env.example:62–74`).
|
||||
- **User flow:** trigger (signup, invite, completion) → email queued → worker sends.
|
||||
- **Edge cases:** idempotency keys (`onboarding.ts:133`); dead‑letter jobs (`schema.ts:3447`).
|
||||
- **Note:** an in‑app **notification system** is `prd/notification-system` **todo** — not shipped.
|
||||
|
||||
---
|
||||
|
||||
## 4. Roles & Responsibilities
|
||||
|
||||
**Org roles** are integers: `ADMIN=1`, `TUTOR=2`, `STUDENT=3`
|
||||
(`packages/utils/src/constants/roles.ts:1–4`). They are stored on
|
||||
`organizationmember.roleId` (`schema.ts:1767`, FK to `role` table `schema.ts:1913`) and
|
||||
surfaced into the session as an `orgRoles` map keyed by org id (`apps/api/src/app.ts:88`).
|
||||
RBAC middlewares read `orgRoles[orgId]` (org id from the `cio-org-id` header) — **no
|
||||
per‑request DB lookup** (`apps/api/src/middlewares/org-team-member.ts:5–53`).
|
||||
|
||||
| Role | Can do | Cannot do | Enforced by |
|
||||
|------|--------|-----------|-------------|
|
||||
| **ADMIN** (1) | Full org control: settings, members, billing, courses, SSO, API keys, all team actions | — | `org-admin.ts`, `org-team-member.ts` (admin∈team) |
|
||||
| **TUTOR** (2) | Team actions: author/manage courses, grade, moderate community/newsfeed | Org admin‑only ops (org settings, billing, member roles) | `org-team-member.ts:44` (ADMIN or TUTOR), `course-team-member.ts` |
|
||||
| **STUDENT** (3) | Enroll, learn, submit exercises, post community Q&A, use tutor | Authoring/grading/team/admin actions | absence of team/admin role → 403 |
|
||||
| **Guest / anonymous** | Public org sites, public courses, embeds, public API (with key) | Any authed action | session = null path (`app.ts:77–84`); public CORS paths (`app.ts:60–67`) |
|
||||
|
||||
**Resource‑scoped middlewares** (`apps/api/src/middlewares/`):
|
||||
- `authMiddleware` — requires a session (`auth.ts`).
|
||||
- `orgMemberMiddleware` — must be org member; `orgTeamMemberMiddleware` — ADMIN|TUTOR;
|
||||
`orgAdminMiddleware` — ADMIN only (`ARCHITECHTURE.md:233–237`).
|
||||
- `courseMemberMiddleware` / `courseTeamMemberMiddleware` — course‑level membership/team.
|
||||
- `programMemberMiddleware` / `programTeamMemberMiddleware` — program‑level.
|
||||
- `apiKeyMiddleware` / `automationKeyMiddleware` (+ `automationKeyScopesMiddleware`) —
|
||||
machine auth for webhooks/public API; `authOrApiKeyMiddleware`,
|
||||
`authOrAutomationKeyMiddleware`, and `*-or-automation-key`/`*-or-api-key` variants allow
|
||||
either a session or a key.
|
||||
- `requireLicense(feature)` — self‑host license gate (`license.ts`).
|
||||
- `workspaceNotReadonlyMiddleware` — blocks writes on read‑only workspaces
|
||||
(`workspace-not-readonly.ts`).
|
||||
- `signupGuard` — signup rules (§3.2). `rateLimiter` — global rate limit (`app.ts:98`).
|
||||
- Route‑specific author/team guards: `question-author-or-team`,
|
||||
`newsfeed-comment-author-or-team`, `program-newsfeed-comment-author-or-team`.
|
||||
|
||||
**Public API scopes:** automation keys carry scopes like `public_api:*`, enforced per route
|
||||
(`apps/api/src/routes/v1/courses.ts:47`); usage categorized `read|write|publish`
|
||||
(`schema.ts:32`).
|
||||
|
||||
---
|
||||
|
||||
## 5. Deployment Modes — Self‑Hosted vs Cloud
|
||||
|
||||
The single switch is **`PUBLIC_IS_SELFHOSTED`** (`=true` ⇒ self‑hosted). Source of truth:
|
||||
`.env.example:26` (Docker stack), `apps/dashboard/.env.example`, and the local‑dev value
|
||||
`apps/dashboard/.env` (`PUBLIC_IS_SELFHOSTED=true`, `README.md:155`). The flag is read in
|
||||
both the API/db layer (`process.env.PUBLIC_IS_SELFHOSTED`) and the dashboard
|
||||
(`$env/static/public`). Every behavioral difference below is traced to a flag check.
|
||||
|
||||
### 5.1 Self‑Hosted mode (`PUBLIC_IS_SELFHOSTED=true`)
|
||||
- **Setup:** Docker Compose full stack — `cp .env.example .env` then
|
||||
`./run-docker-full-stack.sh` (auto‑generates `AUTH_BEARER_TOKEN`, `PRIVATE_SERVER_KEY`);
|
||||
`README.md:240–249`, `docker/docker-compose.yaml`, `docker/Dockerfile.{api,dashboard}`,
|
||||
`docker/docs/SELF_HOST.md`. Bundled Postgres, Redis, MinIO.
|
||||
- **Single organization:** org creation **blocks a second org**
|
||||
(`apps/api/src/services/onboarding.ts:28–33`); the new org is auto‑assigned an
|
||||
`ENTERPRISE` plan with provider `selfhosted` (`onboarding.ts:64–77`). The "add org" UI is
|
||||
**hidden** (`apps/dashboard/src/routes/(app)/org/[slug]/+layout.svelte:29–31`) and the
|
||||
org‑site→LMS redirect is **skipped** (`.../+layout.server.ts:16`). PRD `self-host-single-org.md`.
|
||||
- **Signup:** requires org context unless bootstrapping the first org or the email already
|
||||
belongs to the org (`signup-guard.ts:75–91`).
|
||||
- **Licensing:** Enterprise features (`sso`, `token-auth`, `no-tracking`) are **gated by
|
||||
`LICENSE_KEY`** — `requireLicense` actively checks (`middlewares/license.ts:15–22`);
|
||||
`.env.example:117–118`.
|
||||
- **Tracking:** cloud analytics (PostHog/Umami) are **disabled**
|
||||
(`apps/dashboard/src/lib/utils/functions/appSetup.ts:23–27`); a `no-tracking` license also
|
||||
suppresses tracking (`appSetup.ts:29–37`).
|
||||
- **URLs/links:** `BASE_URL` and invite/email links use `DASHBOARD_ORIGIN` when self‑hosted
|
||||
(`packages/db/src/constants.ts:36–37`; `.env.example:32–35`). Dashboard SSR calls the API
|
||||
internally via `PRIVATE_SERVER_URL` (`.env.example:28–30`).
|
||||
- **Storage/email:** MinIO + Zoho/SMTP from `.env` (`.env.example:62–89`).
|
||||
|
||||
### 5.2 Cloud mode (`PUBLIC_IS_SELFHOSTED` ≠ `true`)
|
||||
- **Multi‑org:** users can create/switch multiple orgs (no single‑org block); "add org" UI
|
||||
shown (`+layout.svelte:29`).
|
||||
- **Billing:** Polar subscriptions + AI token packs are active
|
||||
(`apps/dashboard/src/routes/api/polar/*`); plan set via subscription webhooks
|
||||
(`webhook/+server.ts`). License gate is a **no‑op** — cloud has no license restrictions
|
||||
(`middlewares/license.ts:15–19`).
|
||||
- **Tracking:** PostHog/Umami/UserJot analytics enabled (`appSetup.ts:17–27`).
|
||||
- **Signup:** org context optional unless org settings restrict it (`signup-guard.ts:132–139`).
|
||||
- **Infra:** hosted Postgres/Redis/S3; dashboard deployed on Cloudflare Workers
|
||||
(`apps/dashboard/wrangler.jsonc`, `svelte.config.js`).
|
||||
|
||||
### 5.3 Mode‑exclusive features (summary)
|
||||
| Concern | Self‑Hosted | Cloud |
|
||||
|---|---|---|
|
||||
| Number of orgs | One only (`onboarding.ts:28`) | Many |
|
||||
| Enterprise SSO / token‑auth | Requires `LICENSE_KEY` (`license.ts`) | Always allowed (no license) |
|
||||
| Billing (Polar) | Not used (plan = selfhosted ENTERPRISE) | Subscriptions + token packs |
|
||||
| Analytics tracking | Off (unless… still off) (`appSetup.ts:24`) | On |
|
||||
| API base / links | `DASHBOARD_ORIGIN` (`constants.ts:37`) | `PUBLIC_SERVER_URL` |
|
||||
| Add‑org modal | Hidden (`+layout.svelte:29`) | Shown |
|
||||
|
||||
> Both modes exist in source and share one codebase; **neither mode is
|
||||
> `⚠️ NOT FOUND`.** The product is one app with flag‑driven divergence.
|
||||
|
||||
---
|
||||
|
||||
## 6. End‑to‑End Flow Diagrams (text)
|
||||
|
||||
**A. Signup → Onboarding → Workspace**
|
||||
```
|
||||
Login/Signup (apps/dashboard/.../(auth)/signup)
|
||||
→ POST /api/auth/sign-up/* [signupGuard] (app.ts:106; signup-guard.ts)
|
||||
→ Better Auth creates user + profile (auth.ts; create-profile.ts)
|
||||
→ (auth)/onboarding: createOrganizationWithOwner (onboarding.ts:19)
|
||||
· self-host: block 2nd org, assign ENTERPRISE plan
|
||||
→ completeOnboarding → enqueue 'welcome' email (onboarding.ts:121)
|
||||
→ Dashboard /lms or /org/[slug]
|
||||
```
|
||||
|
||||
**B. Author a course**
|
||||
```
|
||||
Courses → New course (course.ts)
|
||||
→ add sections (section.ts) → lessons (lesson.ts) + translations (lesson-language.ts)
|
||||
→ add exercises/questions (exercise.ts, question-types)
|
||||
→ [optional] AI Assistant builds content (agent/*; requires provider key)
|
||||
→ publish → appears on org site / widget
|
||||
```
|
||||
|
||||
**C. Student learns → certificate (compliance)**
|
||||
```
|
||||
Invite (course/invite.ts) → accept /invite/[hash]
|
||||
→ enroll (people.ts) → study lessons → mark complete (lessonCompletion)
|
||||
→ submit exercises (submission.ts) → tutor grades (mark.ts)
|
||||
→ completion recorded (courseCompletionRecord schema.ts:768)
|
||||
→ certificate issued (packages/certificates; courseCertificateIssue schema.ts:850)
|
||||
→ compliance: deadlines/renewals tracked (course/compliance.ts; internal/*)
|
||||
```
|
||||
|
||||
**D. Public API automation (integration)**
|
||||
```
|
||||
Org → create automation key with scopes (organization/automation.ts; organizationApiKey)
|
||||
→ external client: GET/POST /public-api/v1/courses
|
||||
[automationKeyMiddleware + automationKeyScopesMiddleware('public_api:*')]
|
||||
→ service resolves orgId from key → returns course data (v1/courses.ts; services/v1)
|
||||
```
|
||||
|
||||
**E. Cloud upgrade / buy AI tokens (cloud only)**
|
||||
```
|
||||
Upgrade or Buy tokens (api/polar/subscribe | buy-tokens)
|
||||
→ Polar checkout
|
||||
→ Polar webhook (api/polar/webhook/+server.ts)
|
||||
· subscription.created → createOrgPlan
|
||||
· order.paid (token_pack) → recordPurchase → aiCreditBalance
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 7. Open Questions & Gaps
|
||||
|
||||
1. **Outbound webhooks — doc vs code mismatch.** `README.md:42–43` advertises
|
||||
"REST API + Webhooks … receive events (`certificate.issued`, `enrollment.completed`)",
|
||||
but the only webhook code is **inbound Polar billing**
|
||||
(`apps/dashboard/src/routes/api/polar/webhook`) and `prd/webhooks` is **todo**
|
||||
(`PRD-TRACKER.md:24`). No outbound event‑emission/delivery system was found in
|
||||
`apps/api/src` or `packages/db/src`. **Code wins: outbound webhooks appear unimplemented.**
|
||||
QA should not test event delivery as a shipped feature. ⚠️
|
||||
2. **SCORM support** — README does not promise it, but `prd/scorm-support` is **todo**
|
||||
(`PRD-TRACKER.md:16`). No SCORM code located. ⚠️ Treat as not shipped.
|
||||
3. **In‑app notification system** — `prd/notification-system` is **todo**
|
||||
(`PRD-TRACKER.md:9`). Transactional *email* exists (§3.28); in‑app notifications appear
|
||||
unbuilt. Verify before demoing. ⚠️
|
||||
4. **Selective exercise assignment** — `prd/selective-exercise-assignment` is **todo**
|
||||
(`PRD-TRACKER.md:15`). Assigning specific exercises to specific students may not exist;
|
||||
confirm against `exercise.ts` behavior. ⚠️
|
||||
5. **Course templates / cohorts / import variants** — `prd/course-templates`,
|
||||
`prd/course-cohorts`, `prd/course-import` are **todo** (`PRD-TRACKER.md:10,12,13`),
|
||||
yet related schema exists (`exerciseTemplate` `schema.ts:2401`; `courseImportDraft`
|
||||
`schema.ts:2603`; programs are DONE). Scope of what's actually wired end‑to‑end vs.
|
||||
schema‑only should be verified per feature before writing test cases. ⚠️
|
||||
6. **Anthropic in AI picker** — code supports Anthropic, but it is "not currently in the
|
||||
picker UI" (`README.md:231`, `.env.example:112`). The picker exposes Gemini + GPT‑4o;
|
||||
note the `.env.example:111` "Gemini 3 Flash" vs `README.md:228` "Gemini 2.5 Flash"
|
||||
wording inconsistency — verify the actual model id in `packages/utils/src/agent-models`.
|
||||
7. **Two operating sub‑modes within self‑host** (single‑org bootstrap vs. licensed
|
||||
Enterprise) interact with `signupGuard` and `requireLicense`. Exact UX when
|
||||
`LICENSE_KEY` is absent on self‑host (SSO/token‑auth hidden vs. 403) should be confirmed
|
||||
in the dashboard `license` feature before recording a self‑host SSO demo.
|
||||
8. **Tenant‑router / domain routing** (`apps/tenant-router`, `/domain`) was inventoried but
|
||||
not deeply traced (infra scope). Custom‑domain happy‑path is PRD‑DONE; failure/SSL
|
||||
provisioning states are unverified here.
|
||||
|
||||
---
|
||||
|
||||
## Appendix — Supporting Infrastructure (light)
|
||||
|
||||
These exist in source but are infra/dev tooling rather than demoable product features:
|
||||
|
||||
- `apps/jobs` — BullMQ worker process; consumes media/email queues
|
||||
(paired with `apps/api/src/routes/jobs`, `mediaJob`/`jobStep` tables). Dev queue UI at
|
||||
`/admin/queues` (`apps/api/src/routes/admin/queues.ts`).
|
||||
- `apps/tenant-router` — multi‑tenant/custom‑domain request routing (backs §3.20).
|
||||
- `apps/course-app` / `packages/course-app` — course‑app runtime modules
|
||||
(`pnpm-workspace.yaml:4`).
|
||||
- `apps/docs` — documentation site (`pnpm docs:dev`).
|
||||
- `apps/website` — marketing site (`pnpm website:dev`).
|
||||
- `packages/core` — shared config incl. env access (`packages/core/src/config/env.ts`,
|
||||
used by `signup-guard.ts`, `license.ts`, `onboarding.ts`).
|
||||
- `packages/storybook` — UI component stories (`pnpm storybook:dev`).
|
||||
- `packages/analytics` — analytics helpers (paired with `apps/api/src/services/analytics`).
|
||||
- `packages/tsconfig` — shared TS config.
|
||||
- `packages/ui` — shared Svelte component library (`@cio/ui`); theme classes require `ui:`
|
||||
prefix in dashboard (`AGENTS.md:313`).
|
||||
- `packages/utils` — shared Zod validation, plans, license, agent‑models, constants.
|
||||
- `packages/db` — Drizzle schema/queries/migrations/seed; `pnpm --filter @cio/db db:setup:seed`
|
||||
seeds demo data and the demo login `[email protected]` / `123456` (`README.md:215–219`).
|
||||
|
||||
---
|
||||
|
||||
### How this audit was verified
|
||||
Every section traces to files read during the audit: `README.md`, `ARCHITECHTURE.md`,
|
||||
`AGENTS.md`, `.env.example`, `PRD-TRACKER.md`, `apps/api/src/app.ts`,
|
||||
`packages/db/src/schema.ts`, the role/RBAC middlewares, `signup-guard.ts`,
|
||||
`onboarding.ts`, `appSetup.ts`, the Polar webhook, the v1 public‑API router, and the
|
||||
license constants/middleware. No application was run and no tests were executed —
|
||||
functional testing is deferred to the QA phase per the audit brief.
|
||||
@@ -75,9 +75,9 @@ To get a local copy up and running, please follow these simple steps.
|
||||
|
||||
Here is what you need to be able to run ClassroomIO.com
|
||||
|
||||
- [Node.js](https://nodejs.org/) (Version: >=20.19.3)
|
||||
- [pnpm](https://pnpm.io/installation)
|
||||
- [Docker](https://docs.docker.com/engine/install/)
|
||||
- **[Node.js](https://nodejs.org/)** (Version: >=20.19.3) — _required_
|
||||
- **[pnpm](https://pnpm.io/installation)** (v10) — _required_; the package scripts call `pnpm` directly, so npm/yarn are not substitutes
|
||||
- **[Docker](https://docs.docker.com/engine/install/)** — _required_; runs Postgres + Redis (no local install of those needed)
|
||||
|
||||
### Project Structure
|
||||
|
||||
@@ -94,6 +94,11 @@ The repository also contains shared packages under `packages/` (for example `pac
|
||||
|
||||
### Local Setup
|
||||
|
||||
> **Before you start:** make sure **pnpm** and **Docker** are installed (see
|
||||
> [Prerequisites](#prerequisites)). New to the project? Read
|
||||
> [`DEV_SETUP_NOTES.md`](DEV_SETUP_NOTES.md) first — it's a full step-by-step
|
||||
> walkthrough with a troubleshooting reference for the errors you're likely to hit.
|
||||
|
||||
1. Fork the repo, then clone it:
|
||||
|
||||
```bash
|
||||
@@ -128,22 +133,49 @@ The repository also contains shared packages under `packages/` (for example `pac
|
||||
|
||||
5. Set up your `.env` files:
|
||||
|
||||
- Go to `apps/dashboard` and `apps/api`.
|
||||
- Duplicate the `.env.example` file and rename it to `.env`
|
||||
- Populate them with required values (at minimum):
|
||||
- `apps/api/.env`: `DATABASE_URL`, `REDIS_URL`, `AUTH_BEARER_TOKEN`, `BETTER_AUTH_SECRET`
|
||||
- `apps/dashboard/.env`: `PRIVATE_SERVER_URL`, `PRIVATE_SERVER_KEY`, `PUBLIC_IS_SELFHOSTED`, `DASHBOARD_ORIGIN`
|
||||
- Optional for self-hosted Enterprise-only features (SSO, token-auth, no-tracking): set `LICENSE_KEY` in `apps/api/.env`
|
||||
Each app reads its **own** `.env`. In `apps/api` and `apps/dashboard`, duplicate
|
||||
`.env.example` to `.env` and fill the values below (host ports). The secret values are
|
||||
placeholders — **generate each one** with `openssl rand -hex 32` rather than copying
|
||||
the example text. The two `PRIVATE_SERVER_KEY` values **must match** — they
|
||||
authenticate the dashboard's server-to-server calls to the API, so generate it once
|
||||
and paste the same value into both files.
|
||||
|
||||
- `apps/api/.env`:
|
||||
|
||||
```bash
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/classroomio"
|
||||
REDIS_URL="redis://localhost:6379"
|
||||
PUBLIC_SERVER_URL="http://localhost:3002"
|
||||
TRUSTED_ORIGINS="http://localhost:5173" # the dashboard dev origin
|
||||
BETTER_AUTH_SECRET="<generate-with-openssl-rand-hex-32>"
|
||||
AUTH_BEARER_TOKEN="<generate-with-openssl-rand-hex-32>"
|
||||
PRIVATE_SERVER_KEY="<generate-with-openssl-rand-hex-32>" # generate once; use the SAME value in the dashboard
|
||||
DASHBOARD_ORIGIN="http://localhost:5173" # optional; used for invite/email links back to the dashboard
|
||||
```
|
||||
|
||||
- `apps/dashboard/.env`:
|
||||
|
||||
```bash
|
||||
PUBLIC_SERVER_URL="http://localhost:3002"
|
||||
PRIVATE_SERVER_URL="http://localhost:3002" # used by the SSR auth proxy — required, or login returns "API upstream not configured"
|
||||
PRIVATE_SERVER_KEY="<generate-with-openssl-rand-hex-32>" # must match the API (paste the value generated above)
|
||||
PUBLIC_IS_SELFHOSTED=true
|
||||
```
|
||||
|
||||
- Running the background workers? Also create `apps/jobs/.env` (e.g. `cp apps/api/.env apps/jobs/.env`); it needs `REDIS_URL`. The `packages/db/.env` used by the DB scripts is set up in step 6.
|
||||
- Optional for self-hosted Enterprise-only features (SSO, token-auth, no-tracking): set `LICENSE_KEY` in `apps/api/.env`.
|
||||
|
||||
6. Start local infrastructure for API (Postgres + Redis) and seed the DB:
|
||||
|
||||
```bash
|
||||
docker compose -f docker/docker-compose.yaml up -d postgres redis db-init
|
||||
docker compose -f docker/docker-compose.yaml up -d postgres redis
|
||||
cp packages/db/.env.example packages/db/.env # one-time (Windows: Copy-Item)
|
||||
pnpm --filter @cio/db db:setup:seed
|
||||
```
|
||||
|
||||
- Connect with `DATABASE_URL=postgresql://postgres:postgres@localhost:5432/classroomio`
|
||||
- Connect with `REDIS_URL=redis://localhost:6379`
|
||||
- The `db-init` container runs migrations/seed once Postgres is healthy.
|
||||
- `db:setup:seed` creates the schema and seeds demo data once Postgres is up. The db scripts run with their own working directory, so they read `DATABASE_URL` from `packages/db/.env` (not `apps/api/.env`).
|
||||
|
||||
7. (Optional) Start MinIO locally for object storage (media/documents):
|
||||
|
||||
@@ -172,6 +204,8 @@ The repository also contains shared packages under `packages/` (for example `pac
|
||||
pnpm dashboard:dev
|
||||
```
|
||||
|
||||
- If you see `Failed to resolve entry for package "@cio/..."`, the shared workspace packages haven't been built yet (their `dist/` is missing). Build them once with `pnpm build`, then re-run the dev commands.
|
||||
|
||||
9. Default local URLs:
|
||||
|
||||
- `api`: [http://localhost:3002](http://localhost:3002)
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
],
|
||||
"scripts": {
|
||||
"build": "tsc && tsc-alias",
|
||||
"build:clean": "rm -rf dist tsconfig.tsbuildinfo && pnpm run build",
|
||||
"build:clean": "rimraf dist tsconfig.tsbuildinfo && pnpm run build",
|
||||
"dev": "pnpm --filter @cio/api --parallel \"/^dev:.*/\"",
|
||||
"dev:server": "tsx watch src/index.ts",
|
||||
"dev:tsc": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"preview": "vite preview",
|
||||
"lint": "eslint ./src",
|
||||
"lint:fix": "eslint ./src --fix",
|
||||
"clean": "rm -rf node_modules",
|
||||
"clean": "rimraf node_modules",
|
||||
"format": "prettier --write .",
|
||||
"start": "node build/index.js",
|
||||
"prepare": "svelte-kit sync",
|
||||
|
||||
@@ -164,7 +164,7 @@ docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml up -
|
||||
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml restart api dashboard
|
||||
|
||||
# Run DB seed
|
||||
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml run --rm db-init sh -c "pnpm --filter @cio/db db:setup -- --seed"
|
||||
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml run --rm --entrypoint sh api -c "pnpm --filter @cio/db db:setup -- --seed"
|
||||
|
||||
# Stop
|
||||
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml down
|
||||
@@ -203,9 +203,9 @@ The quickest hosted deployment is the [Railway Template](https://railway.app/tem
|
||||
```zsh title="env"
|
||||
DATABASE_URL="${{db.DATABASE_URL}}"
|
||||
REDIS_URL="${{Redis.REDIS_URL}}"
|
||||
BETTER_AUTH_SECRET="random-secure-key"
|
||||
AUTH_BEARER_TOKEN="some-key"
|
||||
PRIVATE_SERVER_KEY="same-key-for-api-and-dashboard"
|
||||
BETTER_AUTH_SECRET="<generate-with-openssl-rand-hex-32>"
|
||||
AUTH_BEARER_TOKEN="<generate-with-openssl-rand-hex-32>"
|
||||
PRIVATE_SERVER_KEY="<generate-with-openssl-rand-hex-32>" # must be identical in cio-api and cio-dashboard
|
||||
PUBLIC_IS_SELFHOSTED="true"
|
||||
|
||||
# Dashboard URL for invite/email links (e.g. https://app.yourdomain.com)
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
"dev": "vite dev --port 5180",
|
||||
"prebuild": "pnpm --filter @cio/ui run build",
|
||||
"build:question-type-picker": "vite build --mode question-type-picker",
|
||||
"build:course-widget": "vite build --mode course-widget && cp src/widgets/course-widget/embed.html dist/course-widget/embed.html",
|
||||
"build": "rm -rf dist && pnpm build:question-type-picker && pnpm build:course-widget",
|
||||
"build:course-widget": "vite build --mode course-widget && node --input-type=commonjs -e \"require('fs').copyFileSync('src/widgets/course-widget/embed.html','dist/course-widget/embed.html')\"",
|
||||
"build": "rimraf dist && pnpm build:question-type-picker && pnpm build:course-widget",
|
||||
"format": "prettier --write .",
|
||||
"preview": "vite preview --port 4180",
|
||||
"upload:embeds": "tsx scripts/upload-embeds.ts",
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc && tsc-alias",
|
||||
"build": "rimraf dist tsconfig.tsbuildinfo && tsc && tsc-alias",
|
||||
"dev": "pnpm --filter @cio/jobs-worker --parallel \"/^dev:.*/\"",
|
||||
"dev:tsc": "tsc -p tsconfig.json --watch --preserveWatchOutput",
|
||||
"dev:media": "tsx watch src/workers/media.ts",
|
||||
|
||||
@@ -2,6 +2,10 @@ services:
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
container_name: cio-postgres
|
||||
# Bound to localhost only so host tools (pnpm api:dev, db:setup) can reach it
|
||||
# without exposing the database to the LAN.
|
||||
ports:
|
||||
- "127.0.0.1:5432:5432"
|
||||
environment:
|
||||
POSTGRES_DB: ${POSTGRES_DB:-classroomio}
|
||||
POSTGRES_USER: ${POSTGRES_USER:-postgres}
|
||||
@@ -24,6 +28,9 @@ services:
|
||||
redis:
|
||||
image: redis:7-alpine
|
||||
container_name: cio-redis
|
||||
# Bound to localhost only (see postgres note above).
|
||||
ports:
|
||||
- "127.0.0.1:6379:6379"
|
||||
command: ["redis-server", "--appendonly", "yes"]
|
||||
volumes:
|
||||
- redis-data:/data
|
||||
|
||||
@@ -15,7 +15,7 @@ cp .env.example .env
|
||||
./run-docker-full-stack.sh
|
||||
```
|
||||
|
||||
The script builds and starts: **postgres**, **redis**, **minio** (object storage), **db-init** (schema setup), **api** (`localhost:3081`), and **dashboard** (`localhost:3082`).
|
||||
The script builds and starts: **postgres**, **redis**, **minio** (object storage), **api** (`localhost:3081`), and **dashboard** (`localhost:3082`). The **api** container runs database schema setup on startup (skip with `SKIP_DB_SETUP=true`).
|
||||
|
||||
It auto-generates secure values for `AUTH_BEARER_TOKEN` and `PRIVATE_SERVER_KEY` when they are missing.
|
||||
|
||||
@@ -48,7 +48,7 @@ curl -I http://localhost:3082/ # Dashboard — returns 200
|
||||
|
||||
```bash
|
||||
docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml \
|
||||
run --rm db-init sh -c "pnpm --filter @cio/db db:setup -- --seed"
|
||||
run --rm --entrypoint sh api -c "pnpm --filter @cio/db db:setup -- --seed"
|
||||
```
|
||||
|
||||
## Command Reference
|
||||
@@ -64,7 +64,7 @@ dc = docker compose --env-file .env -p classroomio -f docker/docker-compose.yaml
|
||||
| Full stack (with MinIO) | `./run-docker-full-stack.sh` |
|
||||
| Skip image rebuild | `./run-docker-full-stack.sh --no-build` |
|
||||
| Exclude MinIO | `./run-docker-full-stack.sh --no-minio` |
|
||||
| API-only smoke test | `dc up --build -d postgres redis db-init api` |
|
||||
| API-only smoke test | `dc up --build -d postgres redis api` |
|
||||
| Service status | `dc ps` |
|
||||
| Stream logs | `dc logs -f api dashboard` |
|
||||
| Rebuild one service | `dc up -d --build api` |
|
||||
|
||||
+4
-2
@@ -6,14 +6,15 @@
|
||||
"start": "turbo run start",
|
||||
"dev": "turbo prepare --concurrency 13 && turbo run dev",
|
||||
"lint": "turbo run lint",
|
||||
"clean": "turbo clean && find . -name 'node_modules' -type d -prune -exec rm -rf '{}' +",
|
||||
"clean": "turbo clean && rimraf --glob \"**/node_modules\"",
|
||||
"clean:pnpm": "pnpm clean && pnpm store prune",
|
||||
"format": "turbo run format",
|
||||
"format:changed": "node scripts/format-changed.mjs --write",
|
||||
"format:check": "node scripts/format-changed.mjs --staged --check",
|
||||
"postinstall": "test -d .git && lefthook install || true",
|
||||
"postinstall": "node scripts/postinstall.mjs",
|
||||
"release": "standard-version",
|
||||
"db:reset": "pnpm --filter @cio/db db:reset",
|
||||
"db:studio": "pnpm --filter @cio/db db:studio",
|
||||
"dashboard:dev": "pnpm concurrently -n dashboard,ui -c blue,yellow \"pnpm --filter=@cio/dashboard run dev\" \"pnpm --filter=@cio/ui run dev\"",
|
||||
"api:dev": "pnpm concurrently -n api,jobs -c blue,yellow \"pnpm --filter=@cio/api run dev\" \"pnpm --filter=@cio/jobs-worker run dev\"",
|
||||
"jobs:dev": "pnpm --filter=@cio/jobs-worker run dev",
|
||||
@@ -40,6 +41,7 @@
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-svelte": "^3.4.1",
|
||||
"prettier-plugin-tailwindcss": "^0.7.2",
|
||||
"rimraf": "^6.0.1",
|
||||
"standard-version": "^9.5.0",
|
||||
"typescript": "^5.1.6"
|
||||
},
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc && tsc-alias",
|
||||
"build": "rimraf dist tsconfig.tsbuildinfo && tsc && tsc-alias",
|
||||
"dev": "tsc --watch",
|
||||
"format": "prettier --write \"src/**/*.ts\""
|
||||
},
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# Connection string for the @cio/db scripts (db:setup, db:setup:seed, drizzle-kit).
|
||||
# Matches the local Postgres started by docker/docker-compose.yaml.
|
||||
DATABASE_URL="postgresql://postgres:postgres@localhost:5432/classroomio"
|
||||
+14
-10
@@ -60,7 +60,11 @@ It performs three steps in order:
|
||||
|
||||
**Optional:** Add `--seed` (or `-s`) to run the full seed, including demo users, organizations, courses, and other example data.
|
||||
|
||||
Requires `DATABASE_URL` or `PRIVATE_DATABASE_URL` in the environment.
|
||||
Requires `DATABASE_URL` or `PRIVATE_DATABASE_URL` in the environment. The scripts load `.env` from this package directory, so bootstrap it once from the template:
|
||||
|
||||
```bash
|
||||
cp .env.example .env # then set DATABASE_URL for your DB
|
||||
```
|
||||
|
||||
## Seeding
|
||||
|
||||
@@ -105,13 +109,13 @@ pnpm seed --organizations --organization-members --organization-plan
|
||||
|
||||
### Demo accounts
|
||||
|
||||
| Email | Password | Role | Plan |
|
||||
| -------------------------------- | ---------- | ------- | ------------- |
|
||||
| `test@test.com` | `password` | Admin | Basic (free) |
|
||||
| `student@test.com` | `password` | Student | Basic (free) |
|
||||
| `enterprise@test.com` | `password` | Admin | Enterprise |
|
||||
| `enterprise-student@test.com` | `password` | Student | Enterprise |
|
||||
| `early-adopter@test.com` | `password` | Admin | Early Adopter |
|
||||
| `early-adopter-student@test.com` | `password` | Student | Early Adopter |
|
||||
| Email | Password | Role | Plan |
|
||||
| -------------------------------- | -------- | ------- | ------------- |
|
||||
| `admin@test.com` | `123456` | Admin | Basic (free) |
|
||||
| `student@test.com` | `123456` | Student | Basic (free) |
|
||||
| `enterprise@test.com` | `123456` | Admin | Enterprise |
|
||||
| `enterprise-student@test.com` | `123456` | Student | Enterprise |
|
||||
| `early-adopter@test.com` | `123456` | Admin | Early Adopter |
|
||||
| `early-adopter-student@test.com` | `123456` | Student | Early Adopter |
|
||||
|
||||
Mock user credentials are defined in `users.json`. The encrypted password above corresponds to the literal string `password`.
|
||||
Mock user credentials are defined in `users.json`. The demo accounts above use the password `123456`.
|
||||
|
||||
@@ -50,9 +50,10 @@
|
||||
"dist"
|
||||
],
|
||||
"scripts": {
|
||||
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc && tsc-alias",
|
||||
"build": "rimraf dist tsconfig.tsbuildinfo && tsc && tsc-alias",
|
||||
"dev": "tsc --watch",
|
||||
"db": "drizzle-kit",
|
||||
"db:studio": "drizzle-kit studio",
|
||||
"db:reset": "tsx src/scripts/db-reset.ts --skip-seed",
|
||||
"db:setup": "tsx src/scripts/db-setup.ts",
|
||||
"db:setup:seed": "tsx src/scripts/db-setup.ts --seed",
|
||||
|
||||
@@ -22,7 +22,10 @@ async function runPnpmCommand(commandLabel: string, args: string[]) {
|
||||
const child = spawn(pnpmBinary, args, {
|
||||
cwd: packageRoot,
|
||||
env: process.env,
|
||||
stdio: 'inherit'
|
||||
stdio: 'inherit',
|
||||
// On Windows, spawning the pnpm.cmd shim requires a shell since Node's
|
||||
// CVE-2024-27980 fix (spawning .cmd/.bat without shell throws EINVAL).
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
|
||||
@@ -22,7 +22,10 @@ async function runPnpmCommand(commandLabel: string, args: string[]) {
|
||||
const child = spawn(pnpmBinary, args, {
|
||||
cwd: packageRoot,
|
||||
env: process.env,
|
||||
stdio: 'inherit'
|
||||
stdio: 'inherit',
|
||||
// On Windows, spawning the pnpm.cmd shim requires a shell since Node's
|
||||
// CVE-2024-27980 fix (spawning .cmd/.bat without shell throws EINVAL).
|
||||
shell: process.platform === 'win32'
|
||||
});
|
||||
|
||||
child.on('error', (error) => {
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
}
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf dist tsconfig.tsbuildinfo && tsc && tsc-alias",
|
||||
"build": "rimraf dist tsconfig.tsbuildinfo && tsc && tsc-alias",
|
||||
"dev": "tsc --watch",
|
||||
"format": "prettier --write ."
|
||||
},
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"classroomio-mcp": "./dist/index.js"
|
||||
},
|
||||
"scripts": {
|
||||
"build": "rm -rf dist tsconfig.tsbuildinfo && node ./scripts/build.mjs",
|
||||
"build": "rimraf dist tsconfig.tsbuildinfo && node ./scripts/build.mjs",
|
||||
"typecheck": "tsc --noEmit",
|
||||
"dev": "tsx src/index.ts",
|
||||
"start": "node dist/index.js",
|
||||
|
||||
Generated
+18
-77
@@ -48,6 +48,9 @@ importers:
|
||||
prettier-plugin-tailwindcss:
|
||||
specifier: ^0.7.2
|
||||
version: 0.7.2([email protected]([email protected])([email protected]))([email protected])
|
||||
rimraf:
|
||||
specifier: ^6.0.1
|
||||
version: 6.1.3
|
||||
standard-version:
|
||||
specifier: ^9.5.0
|
||||
version: 9.5.0
|
||||
@@ -4096,183 +4099,155 @@ packages:
|
||||
resolution: {integrity: sha512-9B+taZ8DlyyqzZQnoeIvDVR/2F4EbMepXMc/NdVbkzsJbzkUjhXv/70GQJ7tdLA4YJgNP25zukcxpX2/SueNrA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-gvcC4ACAOPRNATg/ov8/MnbxFDJqf/pDePbBnuBDcjsI8PssmjoKMAz4LtLaVi+OnSb5FK/yIOamqDwGmXW32g==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-u7Wz6ntiSSgGSGcjZ55im6uvTrOxSIS8/dgoVMoiGE9I6JAfU50yH5BoDlYA1tcuGS7g/QNtetJnxA6QEsCVTA==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-MmWmQ3iPFZr0Iev+BAgVMb3ZyC4KeFc3jFxnNbEPas60e1cIfevbtuyf9nDGIzOaW9PdnDciJm+wFFaTlj5xYw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-9Ti+BbTYDcsbp4wfYib8Ctm1ilkugkA/uscUn6UXK1ldpC1JjiXbLfFZtRlBhjPZ5o1NCLiDbg8fhUPKStHoTA==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-viYN1KX9m+/hGkJtvYYp+CCLgnJXwiQB39damAO7WMdKWlIhmYTfHjwSbQeUK/20vY154mwezd9HflVFM1wVSw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-JMVv+AMRyGOHtO1RFBiJy/MBsgz0x4AWrT6QoEVVTyh1E39TrCUpTRI7mx9VksGX4awWASxqCYLCV4wBZHAYxA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-JTS1eldqZbJxjvKaAkxhZmBqPRGmxgu+qFKSInv8moZ2AmT5Yib3EQ1c6gp493HvrvV8QgdOXdyaIBrhvFhBMQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-y/5PCd+mP4CA/sPDKl2961b+C9d+vPAveS33s6Z3zfASk2j5upL6fXVPZi7ztePZ5CuH+1kW8JtvxgbuXHRa4Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-opC+Ok5pRNAzuvq1AG0ar+1owsu842/Ab+4qvU879ippJBHvyY5n2mxF1izXqkPYlGuP/M556uh53jRLJmzTWA==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-XrHMZwGQGvJg2V/oRSUfSAfjfPxO+4DkiRh6p2AFjLQztWUuY/o8Mq0eMQVIY7HJ1CDQUJlxGGZRw1a5bqmd1g==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-WT+d/cgqKkkKySYmqoZ8y3pxx7lx9vVejxW/W4DOFMYVSkErR+w7mf2u8m/y4+xHe7yY9DAXQMWQhpnMuFfScw==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==}
|
||||
engines: {node: ^18.17.0 || ^20.3.0 || >=21.0.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@img/[email protected]':
|
||||
resolution: {integrity: sha512-ykUW4LVGaMcU9lu9thv85CbRMAwfeadCJHRsg2GmeRa/cJxsVY9Rbd57JcMxBkKHag5U/x7TSBpScF4U8ElVzg==}
|
||||
@@ -4817,56 +4792,48 @@ packages:
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/[email protected]':
|
||||
resolution: {integrity: sha512-3R0iqjM3xYOZCnwgcxOQXH7hrz64/USDIuLbNTM1kZqQzRqaR4w7SwoWKU934zABo8d0op2oSwOp+CV3hZnM7A==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/[email protected]':
|
||||
resolution: {integrity: sha512-lAQf8PQxfgy7h0bmcfSVE3hg3qMueshPYULFsCrHM+8KefGZ9W+ZMvRyU33gLrB4w1O3Fz1orR0hmKMCRxXNrQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/[email protected]':
|
||||
resolution: {integrity: sha512-2QlvQBUhHuAE3ezD4X3CAEKMXdfgInggQ5Bj/7gb5NcYP3GyfLTj7c+mMu+BRwfC9B3AXBNyqHWbqEuuUvZyRQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/[email protected]':
|
||||
resolution: {integrity: sha512-v06iu0osHszgqJ1dLQRb6leWFU1sjG/UQk4MoVBtE6ZPewgfTkby6G9II1SpEAf2onnAuQceVYxQH9iuU3NJqw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/[email protected]':
|
||||
resolution: {integrity: sha512-+5HhNHtxsdcd7+ljXFnn9FOoCNXJX3UPgIfIE6vdwS1HqdGNH6eAcVobuqGOp54l8pvcxDQA6F4cPswCgLrQfQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/[email protected]':
|
||||
resolution: {integrity: sha512-jKwO7ZLNkjxwg7FoCLw+fJszooL9yXRZsDN0AQ1AQUTWq1l8GH/2e44k68N3fcP19jl8O8jGpqLAZcQTYk6skA==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@oxc-parser/[email protected]':
|
||||
resolution: {integrity: sha512-TYqnuKV/p3eOc+N61E0961nA7DC+gaCeJ3+V2LcjJdTwFMdikqWL6uVk1jlrpUCBrozHDATVUKDZYH7r4FQYjQ==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@oxc-parser/[email protected]':
|
||||
resolution: {integrity: sha512-ZhrVmWFifVEFQX4XPwLoVFDHw9tAWH9p9vHsHFH+5uCKdfVR+jje4WxVo6YrokWCboGckoOzHq5KKMOcPZfkRg==}
|
||||
@@ -4929,42 +4896,36 @@ packages:
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/[email protected]':
|
||||
resolution: {integrity: sha512-6E+m/Mm1t1yhB8X412stiKFG3XykmgdIOqhjWj+VL8oHkKABfu/gjFj8DvLrYVHSBNC+/u5PeNrujiSQ1zwd1Q==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/[email protected]':
|
||||
resolution: {integrity: sha512-LrGp+f02yU3BN9A+DGuY3v3bmnFUggAITBGriZHUREfNEzZh/GO06FF5u2kx8x+GBEUYfyTGamol4j3m9ANe8w==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/[email protected]':
|
||||
resolution: {integrity: sha512-cFOjABi92pMYRXS7AcQv9/M1YuKRw8SZniCDw0ssQb/noPkRzA+HBDkwmyOJYp5wXcsTrhxO0zq1U11cK9jsFg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/[email protected]':
|
||||
resolution: {integrity: sha512-GcESn8NZySmfwlTsIur+49yDqSny2IhPeZfXunQi48DMugKeZ7uy1FX83pO0X22sHntJ4Ub+9k34XQCX+oHt2A==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@parcel/[email protected]':
|
||||
resolution: {integrity: sha512-n0E2EQbatQ3bXhcH2D1XIAANAcTZkQICBPVaxMeaCVBtOpBZpWJuf7LwyWPSBDITb7In8mqQgJ7gH8CILCURXg==}
|
||||
engines: {node: '>= 10.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@parcel/[email protected]':
|
||||
resolution: {integrity: sha512-RFzklRvmc3PkjKjry3hLF9wD7ppR4AKcWNzH7kXR7GUe0Igb3Nz8fyPwtZCSquGrhU5HhUNDr/mKBqj7tqA2Vw==}
|
||||
@@ -5601,67 +5562,56 @@ packages:
|
||||
resolution: {integrity: sha512-EHMUcDwhtdRGlXZsGSIuXSYwD5kOT9NVnx9sqzYiwAc91wfYOE1g1djOEDseZJKKqtHAHGwnGPQu3kytmfaXLQ==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-+pBrqEjaakN2ySv5RVrj/qLytYhPKEUwk+e3SFU5jTLHIcAtqh2rLrd/OkbNuHJpsBgxsD8ccJt5ga/SeG0JmA==}
|
||||
cpu: [arm]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-NSqc7rE9wuUaRBsBp5ckQ5CVz5aIRKCwsoa6WMF7G01sX3/qHUw/z4pv+D+ahL1EIKy6Enpcnz1RY8pf7bjwng==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-gr5vDbg3Bakga5kbdpqx81m2n9IX8M6gIMlQQIXiLTNeQW6CucvuInJ91EuCJ/JYvc+rcLLsDFcfAD1K7fMofg==}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-gsrtB1NA3ZYj2vq0Rzkylo9ylCtW/PhpLEivlgWe0bpgtX5+9j9EZa0wtZiCjgu6zmSeZWyI/e2YRX1URozpIw==}
|
||||
cpu: [loong64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-y3qNOfTBStmFNq+t4s7Tmc9hW2ENtPg8FeUD/VShI7rKxNW7O4fFeaYbMsd3tpFlIg1Q8IapFgy7Q9i2BqeBvA==}
|
||||
cpu: [ppc64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-89sepv7h2lIVPsFma8iwmccN7Yjjtgz0Rj/Ou6fEqg3HDhpCa+Et+YSufy27i6b0Wav69Qv4WBNl3Rs6pwhebQ==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-ZcU77ieh0M2Q8Ur7D5X7KvK+UxbXeDHwiOt/CPSBTI1fBmeDMivW0dPkdqkT4rOgDjrDDBUed9x4EgraIKoR2A==}
|
||||
cpu: [riscv64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-2AdWy5RdDF5+4YfG/YesGDDtbyJlC9LHmL6rZw6FurBJ5n4vFGupsOBGfwMRjBYH7qRQowT8D/U4LoSvVwOhSQ==}
|
||||
cpu: [s390x]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-WGt5J8Ij/rvyqpFexxk3ffKqqbLf9AqrTBbWDk7ApGUzaIs6V+s2s84kAxklFwmMF/vBNGrVdYgbblCOFFezMQ==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-JzQmb38ATzHjxlPHuTH6tE7ojnMKM2kYNzt44LO/jJi8BpceEC8QuXYA908n8r3CNuG/B3BV8VR3Hi1rYtmPiw==}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@rollup/[email protected]':
|
||||
resolution: {integrity: sha512-huT3fd0iC7jigGh7n3q/+lfPcXxBi+om/Rs3yiFxjvSxbSB6aohDFXbWvlspaqjeOh+hx7DDHS+5Es5qRkWkZg==}
|
||||
@@ -6681,84 +6631,72 @@ packages:
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-CTrwomI+c7n6aSSQlsPL0roRiNMDQ/YzMD9EjcR+H4f0I1SQ8QqIuPnsVp7QgMkC1Qi8rtkekLkOFjo7OlEFRQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-4og1V+ftEPXGttOO7eCmW7VICmzzJWgMx+QXAJRAhjrSjumCwWqMfkDrNu1LXEQzNAwz28NCUpucgQPrR4S2yw==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-1px92582HkPQlaaCkdRcio71p8bc8i/ap5807tPRDK/uw953cauQBT8c5tVGkOwrHMfc2Yh6UuxaH4vtTjGvHg==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-WZA0CHRL/SP1TRbA5mp9htsppSEkWuQ4KsSUumYQnyl8ZdT39ntwqmz4IUHGN6p4XdSlYfJwM4rRzZLShHsGAQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-oCfG/mS+/+XRlwNjnsNLVwnMWYH7tn/kYPsNPh+JSOMlnt93mYNCKHYzylRhI51X+TbR+ufNhhKKzm6QkqX8ag==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-v3gyT0ivkfBLoZGF9LyHmts0Isc8jHZyVcbzio6Wpzifg/+5ZJpDiRiUhDLkcr7f/r38SWNe7ucxmGW3j3Kb/g==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-qMFzxI2YlBOLW5PhblzuSWlWfwLHaneBE0xHzLrBgNtqN6mWfs+qYbhryGSXQjFYB1Dzf5w+LN5qbUTPhW7Y5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-rTAGAkDgqbXHNp/xW0iugLVmX62wOp2PoE39BTCGKjv3Iocf6AFbRP/wZT/kuCxC9QBh9Pu8XPkv/zCZB2mcMg==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-bhJ2y2OQNlcRwwgOAGMY0xTFStt4/wyU6pvI6LSuZpRgKQwxTec0/3Scu91O8ir7qCR3AuepQKLU/kX99FouqQ==}
|
||||
engines: {node: '>= 10'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-5r1X2FKnCMUPlXTWRYpHdPYUY6a1Ar/t7P24OuiEdEOmms5lyqjDRvVY1yy9Rmioh+AunQ0rWiOTPE8F9A3v5g==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-XW3t3qwbIwiSyRCggeO2zxe3KWaEbM0/kW9e8+0XpBgyKU4ATYzcVSMKteZJ1iukJ3HgHBjbg9P5YPRCVUxlnQ==}
|
||||
engines: {node: '>= 20'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
'@tailwindcss/[email protected]':
|
||||
resolution: {integrity: sha512-LffYTvPjODiP6PT16oNeUQJzNVyJl1cjIebq/rWWBF+3eDst5JGEFSc5cWxyRCJ0Mxl+KyIkqRxk1XPEs9x8TA==}
|
||||
@@ -10864,84 +10802,72 @@ packages:
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-WKyLWztD71rTnou4xAD5kQT+982wvca7E6QoLpoawZ1gP9JM0GJj4Tp5jMUh9B3AitHbRZ2/H3W5xQmdEOUlLg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-5Vh9dGeblpTxWHpOx8iauV02popZDsCYMPIgiuw97OJ5uaDsL86cnqSFs5LZkG3ghHoX5isLgWzMs+eD1YzrnA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-mVZ7Pg2zIbe3XlNbZJdjs86YViQFoJSpc41CbVmKBPiGmC4YrfeOyz65ms2qpAobVd7WQsbW4PdsSJEMymyIMg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [arm64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-Cfd46gdmj1vQ+lR6VRTTadNHu6ALuw2pKR9lYq4FnhvgBc4zWY1EtZcAc6EffShbb1MFrIPfLDXD6Xprbnni4w==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-xGlFWRMl+0KvUhgySdIaReQdB4FNudfUTARn7q0hh/V67PVGCs3ADFjw+6++kG1RNd0zdGRlEKa+T13/tQjPMA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [glibc]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-XJaLUUFXb6/QG2lGIW6aIk6jKdtjtcffUT0NKvIqhSBY3hh9Ch+1LCeH80dR9q9LBjG3ewbDjnumefsLsP6aiA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-eowF8PrKHw9LpoZii5tdZwnBcYDxRw2rRCyvAXLi34iyeYfqCQNA9rmUM0ce62NlPhCvof1+9ivRaTY6pSKDaA==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==}
|
||||
engines: {node: '>= 12.0.0'}
|
||||
cpu: [x64]
|
||||
os: [linux]
|
||||
libc: [musl]
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-FZn+vaj7zLv//D/192WFFVA0RgHawIcHqLX9xuWiQt7P0PtdFEVaxgF9rjM/IRYHQXNnk61/H/gb2Ei+kUQ4xQ==}
|
||||
@@ -11850,6 +11776,9 @@ packages:
|
||||
resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw==}
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==}
|
||||
|
||||
@@ -12695,6 +12624,11 @@ packages:
|
||||
deprecated: Rimraf versions prior to v4 are no longer supported
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-LKg+Cr2ZF61fkcaK1UdkH2yEBBKnYjTyWzTJT6KNPcSPaiT7HSdhtMXQuN5wkTX0Xu72KQ1l8S42rlmexS2hSA==}
|
||||
engines: {node: 20 || >=22}
|
||||
hasBin: true
|
||||
|
||||
[email protected]:
|
||||
resolution: {integrity: sha512-IXgzBWvWQwE6PrDI05OvmXUIruQTcoMDzRsOd5CDvHCVLcLHMTSYvOK5Cm46kWqlV3yAbuSpBZdJ5oP5OUoStg==}
|
||||
|
||||
@@ -26677,6 +26611,8 @@ snapshots:
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]: {}
|
||||
@@ -27578,6 +27514,11 @@ snapshots:
|
||||
dependencies:
|
||||
glob: 7.2.3
|
||||
|
||||
[email protected]:
|
||||
dependencies:
|
||||
glob: 13.0.6
|
||||
package-json-from-dist: 1.0.1
|
||||
|
||||
[email protected]: {}
|
||||
|
||||
[email protected]:
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
// Cross-platform postinstall: install lefthook git hooks only when this is a
|
||||
// git working copy (skips published-tarball / CI-without-git installs). Replaces
|
||||
// the previous `test -d .git && lefthook install || true`, which relied on the
|
||||
// Unix-only `test`/`true` builtins and failed on Windows cmd.exe.
|
||||
import { existsSync } from 'node:fs';
|
||||
import { execSync } from 'node:child_process';
|
||||
|
||||
if (existsSync('.git')) {
|
||||
try {
|
||||
execSync('lefthook install', { stdio: 'inherit' });
|
||||
} catch {
|
||||
// Git hooks are a dev convenience — never fail the install over them.
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user