Merge branch 'main' of https://github.com/classroomio/classroomio
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