From bd13d0724d98e09c56921424c879d85e58d5270e Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 25 Jun 2026 22:49:27 +0100 Subject: [PATCH 1/2] docs(agents): add Cursor Cloud dev environment setup notes (#676) * docs(agents): add Cursor Cloud dev environment setup notes Co-authored-by: Rotimi Best * docs(agents): document self-hosted vs cloud deployment modes Co-authored-by: Rotimi Best * docs(agents): document seeded tenants and cloud-mode multi-tenant testing Co-authored-by: Rotimi Best * Update AGENTS.md Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --------- Co-authored-by: Cursor Agent Co-authored-by: Rotimi Best Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> --- AGENTS.md | 65 +++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 65 insertions(+) diff --git a/AGENTS.md b/AGENTS.md index c3b381e4..9c773751 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -411,3 +411,68 @@ When moving from `apps/dashboard/src/lib/components/` to `apps/dashboard/src/lib 3. **Update exports** in `features/ui/index.ts` 4. **Update imports**: `import { Component } from '$features/ui';` 5. **Delete old files** and verify no remaining references + +## Cursor Cloud specific instructions + +The full setup/run flow lives in `README.md` and `DEV_SETUP_NOTES.md`. The notes below cover only the non-obvious, cloud-VM-specific caveats. The update script already runs `pnpm install` on startup. + +### Node version (important) +The sandbox injects `/exec-daemon/node` (Node 22) at the front of `PATH`, so a bare `node`/`pnpm` runs under Node 22 even though the repo requires Node `^20.19.3` (`.nvmrc`). Node 20.19.3 is installed via nvm and set as the nvm default. Before running any `pnpm`/dev command, prepend the repo Node to `PATH`: + +```bash +export PATH="$HOME/.nvm/versions/node/v20.19.3/bin:$PATH" +``` + +`pnpm install` itself succeeds under either version (there is no `engine-strict`), but run the dev servers and DB scripts on Node 20. + +### Services & how to run them +Docker is installed and provides Postgres 16 + Redis 7 (the app does not run them natively). After a fresh VM boot the Docker daemon is usually not running — start it (it needs the snapshot's `/etc/docker/daemon.json`, which already sets `fuse-overlayfs` + disables the containerd snapshotter, plus `iptables-legacy`): + +```bash +sudo dockerd > /tmp/dockerd.log 2>&1 & +sudo chmod 666 /var/run/docker.sock # or prefix docker commands with sudo +docker compose -f docker/docker-compose.yaml up -d postgres redis +``` + +The per-app `.env` files (`apps/api/.env`, `apps/dashboard/.env`, `apps/jobs/.env`, `packages/db/.env`) are gitignored but persist in the VM snapshot with a matching `PRIVATE_SERVER_KEY`. If any are missing, recreate them per `README.md` (generate secrets with `openssl rand -hex 32`; the two `PRIVATE_SERVER_KEY` values must match). + +The Postgres docker volume persists in the snapshot already seeded with demo data (login `admin@test.com` / `123456`). Re-seed only if needed: `pnpm --filter @cio/db db:setup:seed`. + +Run the app (two long-lived processes; use tmux): +- `pnpm api:dev` → API on `http://localhost:3002` + 5 BullMQ workers. +- `pnpm dashboard:dev` → dashboard on `http://localhost:5173` + the `@cio/ui` Tailwind CSS watcher. + +Do not use `pnpm dev` (it trips turbo's concurrency cap). Build shared package `dist/` once with `pnpm build` if you see `Failed to resolve entry for package "@cio/*"`. + +### Known caveats +- **Vite SSR circular dependency (layerchart):** authenticated/chart pages occasionally render "Something unexpected occurred" on a cold load. Reload the page (or restart `dashboard:dev`) and it renders — it is intermittent, not a setup failure. +- **MinIO is optional and not started by default.** Without it, image/media thumbnails show "Failed to load"; that is expected. Start it with `docker compose -f docker-compose.yaml --profile minio up -d minio minio-init` and add the `OBJECT_STORAGE_*` vars from `README.md` to `apps/api/.env`. +- **Pre-existing lint/test issues (not environment problems):** `pnpm --filter @cio/api lint` fails (missing ESLint v9 `eslint.config.*`); `pnpm --filter @cio/dashboard lint` runs but reports pre-existing errors; api `vitest run` passes 61 tests but 5 files fail to load `@cio/core/services/*/*` subpaths (Vite nested-wildcard exports quirk; Node resolves them fine); `pnpm --filter @cio/dashboard test` (jest) fails to parse `jest.config.ts`. The pre-commit gate `pnpm format:check` passes. +- The optional `@cio/storybook` build fails on an unresolved `@lucide/svelte/icons/bot` import; it does not affect api/dashboard. + +### Deployment mode: self-hosted vs cloud (affects which features you can test) +A single flag, `PUBLIC_IS_SELFHOSTED`, switches the whole product between **self-hosted** and **cloud** behavior. It is read in two places: the dashboard via `$env/static/public` (e.g. `apps/dashboard/src/lib/features/app/layout-setup.ts`) and the API/db layer via `@cio/core/config/env` (e.g. `apps/api/src/services/license.ts`, `apps/api/src/middlewares/license.ts`, `apps/api/src/services/onboarding.ts`). `FEATURE_AUDIT.md` §5 is the source-of-truth feature-by-feature map. + +**This VM's local env is configured for CLOUD mode** (`PUBLIC_IS_SELFHOSTED=false` in both `apps/dashboard/.env` and `apps/api/.env`) so multi-tenant and all license-gated features are testable. The README contributor default is self-hosted; to switch this VM back, set `PUBLIC_IS_SELFHOSTED=true` in both files (or remove it from `apps/api/.env`) and restart both dev servers. + +- **Self-hosted (`PUBLIC_IS_SELFHOSTED=true`)** — the README contributor default: + - Single organization, single domain: creating a 2nd org is blocked (`onboarding.ts`), the "add org" UI is hidden, and the org is auto-assigned a `selfhosted` `ENTERPRISE` plan. + - Enterprise features `sso`, `token-auth`, `no-tracking` are gated behind `LICENSE_KEY`, verified against the external `https://enterprise-api.classroomio.dev`. Without a key, `requireLicense` returns 403. + - Polar billing and PostHog/Umami analytics are off. +- **Cloud (`PUBLIC_IS_SELFHOSTED` unset or `false`)**: + - Multi-tenant: multiple orgs per user, "add org" shown; org is resolved from the **subdomain** (`acme.`) or a custom domain. Locally there are no subdomains, so an org public site is simulated with the `?org=` query param (sets the `_orgSiteName` cookie — see `layout-setup.ts`). + - The license gate is a **no-op**, so all enterprise features are unlocked **without** a `LICENSE_KEY` — i.e. cloud mode is the easiest way to exercise SSO/token-auth/multi-org in dev. + - Polar billing and analytics activate but are themselves gated by their own keys (absent keys just no-op), so they don't block startup. + +Non-obvious gotcha: the README local-dev setup only sets `PUBLIC_IS_SELFHOSTED=true` in `apps/dashboard/.env`; `apps/api/.env` leaves it unset, so the **API behaves as cloud** (license no-op, 2nd org allowed) while the **dashboard UI is self-hosted**. To exercise a coherent mode, set the flag the same way in both files. The flag is read at process start, so restart the dev servers after editing it. + +### Seeded tenants (good for multi-tenant testing) +The seed (`packages/db/src/utils/seed/organizationmember.ts`) creates **three independent organizations**, each with its own admin + student. All accounts use password `123456`: + +| Org (siteName) | Admin | Student | Example courses | +|---|---|---|---| +| Udemy Test (`udemy-test`) | `admin@test.com` | `student@test.com` | Modern Web Development with React; Getting started with MVC; Data Science with Python and Pandas | +| Coursera Test (`coursera-test`) | `enterprise@test.com` | `enterprise-student@test.com` | SOC 2 Security Basics; HIPAA Awareness 2026 | +| Skillshare Test (`skillshare-test`) | `early-adopter@test.com` | `early-adopter-student@test.com` | Product Management Fundamentals | + +In cloud mode you can demonstrate tenant isolation on the single local instance by visiting each org's public catalog via the `?org=` param, e.g. `http://localhost:5173/?org=coursera-test` vs `?org=udemy-test` vs `?org=skillshare-test` — each renders its own branded catalog and courses. (Note: a user enrolled in courses across orgs becomes a member of multiple tenants, so after login the dashboard may open whichever org that user most recently used.) From 4fe45ac64286038ab2910418de57b13b70d45b02 Mon Sep 17 00:00:00 2001 From: Rotimi Best Date: Thu, 25 Jun 2026 23:53:16 +0100 Subject: [PATCH 2/2] fix(dashboard): course menu icons, delete scroll, and settings tags auth (#691) * fix(dashboard): course menu icons, delete scroll, and settings tags auth Remove inconsistent dropdown icons, scroll to settings sections for delete and related actions when already on the settings page, and skip org tag group fetches for non-admin users to avoid ORG_TEAM_NOT_AUTHORIZED errors. * fix(api): allow org members to fetch organization tags Use orgMemberMiddleware for GET /organization/tags instead of restricting reads to admins. Revert the dashboard settings workaround that skipped tag group fetches for non-admin users. --------- --- apps/api/src/routes/organization/tags.ts | 3 +- .../course-context-menu-content.svelte | 41 ++++++++++++++----- .../lib/features/course/pages/settings.svelte | 11 +++++ 3 files changed, 43 insertions(+), 12 deletions(-) diff --git a/apps/api/src/routes/organization/tags.ts b/apps/api/src/routes/organization/tags.ts index 9477c5fc..b39c3b1d 100644 --- a/apps/api/src/routes/organization/tags.ts +++ b/apps/api/src/routes/organization/tags.ts @@ -23,6 +23,7 @@ import { authMiddleware } from '@api/middlewares/auth'; import { authOrAutomationKeyMiddleware } from '@api/middlewares/auth-or-automation-key'; import { handleError } from '@api/utils/errors'; import { orgAdminMiddleware } from '@api/middlewares/org-admin'; +import { orgMemberMiddleware } from '@api/middlewares/org-member'; import { orgAdminOrAutomationKeyMiddleware } from '@api/middlewares/org-admin-or-automation-key'; import { assertMcpAutomationUsageAllowed, recordMcpAutomationUsage } from '@api/services/organization/automation-usage'; import { zValidator } from '@hono/zod-validator'; @@ -44,7 +45,7 @@ export const tagsRouter = new Hono() return handleError(c, error, 'Failed to fetch public tags'); } }) - .get('/', authMiddleware, orgAdminMiddleware, async (c) => { + .get('/', authMiddleware, orgMemberMiddleware, async (c) => { try { const orgId = c.req.header('cio-org-id')!; const groups = await getOrganizationTagGroups(orgId); diff --git a/apps/dashboard/src/lib/features/course/components/course-context-menu-content.svelte b/apps/dashboard/src/lib/features/course/components/course-context-menu-content.svelte index dfd90821..415e52e0 100644 --- a/apps/dashboard/src/lib/features/course/components/course-context-menu-content.svelte +++ b/apps/dashboard/src/lib/features/course/components/course-context-menu-content.svelte @@ -1,8 +1,6 @@