diff --git a/CLAUDE.md b/CLAUDE.md index d2155df..a33ab71 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,7 +4,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Project -Cameleer SaaS — multi-tenant SaaS platform wrapping the Cameleer observability stack (Java agent + server) for Apache Camel applications. Customers get managed observability for their Camel integrations without running infrastructure. +Cameleer SaaS — **vendor management plane** for the Cameleer observability stack. Two personas: **vendor** (platform:admin) manages the platform and provisions tenants; **tenant admin** (tenant:manage) manages their observability instance. The vendor creates tenants, which provisions per-tenant cameleer3-server + UI instances via Docker API. No example tenant — clean slate bootstrap, vendor creates everything. ## Ecosystem @@ -29,10 +29,21 @@ Agent-server protocol is defined in `cameleer3/cameleer3-common/PROTOCOL.md`. Th - `PublicConfigController.java` — GET /api/config (Logto endpoint, SPA client ID, scopes) - `MeController.java` — GET /api/me (authenticated user, tenant list) -**tenant/** — Tenant lifecycle +**tenant/** — Tenant data model - `TenantEntity.java` — JPA entity (id, name, slug, tier, status, logto_org_id, stripe IDs, settings JSONB) -- `TenantService.java` — create tenant -> Logto org, activate, suspend -- `TenantController.java` — POST create, GET list, GET by ID + +**vendor/** — Vendor console (platform:admin only) +- `VendorTenantService.java` — orchestrates tenant creation: DB record -> Logto org -> admin user -> license -> Docker provisioning -> OIDC config push -> redirect URI registration +- `VendorTenantController.java` — REST at `/api/vendor/tenants` (platform:admin required) + +**portal/** — Tenant admin portal (org-scoped) +- `TenantPortalService.java` — customer-facing: dashboard (health from server), license, OIDC config, team, settings +- `TenantPortalController.java` — REST at `/api/tenant/*` (org-scoped) + +**provisioning/** — Pluggable tenant provisioning +- `TenantProvisioner.java` — pluggable interface (like server's RuntimeOrchestrator) +- `DockerTenantProvisioner.java` — Docker implementation, creates per-tenant server + UI containers +- `TenantProvisionerAutoConfig.java` — auto-detects Docker socket **license/** — License management - `LicenseEntity.java` — JPA entity (id, tenant_id, tier, features JSONB, limits JSONB, expires_at) @@ -51,15 +62,16 @@ Agent-server protocol is defined in `cameleer3/cameleer3-common/PROTOCOL.md`. Th ### React Frontend (`ui/src/`) - `main.tsx` — React 19 root -- `router.tsx` — /login, /callback, / -> OrgResolver -> Layout -> pages +- `router.tsx` — `/vendor/*` + `/tenant/*` with `RequireScope` guards and `LandingRedirect` that waits for scopes +- `Layout.tsx` — persona-aware sidebar: vendor sees expandable "Vendor" section (Tenants + Logto link), tenant admin sees Dashboard/License/OIDC/Team/Settings +- `OrgResolver.tsx` — merges global + org-scoped token scopes (vendor's platform:admin is global) - `config.ts` — fetch Logto config from /platform/api/config - `auth/useAuth.ts` — auth hook (isAuthenticated, logout, signIn) - `auth/useOrganization.ts` — Zustand store for current tenant - `auth/useScopes.ts` — decode JWT scopes, hasScope() - `auth/ProtectedRoute.tsx` — guard (redirects to /login) -- `pages/DashboardPage.tsx` — tenant dashboard -- `pages/LicensePage.tsx` — license info -- `pages/AdminTenantsPage.tsx` — platform admin tenant management +- **Vendor pages**: `VendorTenantsPage.tsx`, `CreateTenantPage.tsx`, `TenantDetailPage.tsx` +- **Tenant pages**: `TenantDashboardPage.tsx`, `TenantLicensePage.tsx`, `OidcConfigPage.tsx`, `TeamPage.tsx`, `SettingsPage.tsx` ### Custom Sign-in UI (`ui/sign-in/src/`) @@ -68,12 +80,7 @@ Agent-server protocol is defined in `cameleer3/cameleer3-common/PROTOCOL.md`. Th ## Architecture Context -The existing cameleer3-server already has single-tenant auth (JWT, RBAC, bootstrap tokens, OIDC). The SaaS layer must: -- Add multi-tenancy (tenant isolation of agent data, diagrams, configs) -- Provide self-service signup, billing, and team management -- Generate per-tenant bootstrap tokens for agent registration -- Proxy or federate access to tenant-specific cameleer3-server instances -- Enforce usage quotas and metered billing +The SaaS platform is a **vendor management plane**. It does not proxy requests to servers — instead it provisions dedicated per-tenant cameleer3-server instances via Docker API. Each tenant gets isolated server + UI containers with their own database schemas, networks, and Traefik routing. ### Routing (single-domain, path-based via Traefik) @@ -82,7 +89,9 @@ All services on one hostname. Two env vars control everything: `PUBLIC_HOST` + ` | Path | Target | Notes | |------|--------|-------| | `/platform/*` | cameleer-saas:8080 | SPA + API (`server.servlet.context-path: /platform`) | -| `/server/*` | cameleer3-server-ui:80 | Server dashboard (strip-prefix + `BASE_PATH=/server`) | +| `/platform/vendor/*` | (SPA routes) | Vendor console (platform:admin) | +| `/platform/tenant/*` | (SPA routes) | Tenant admin portal (org-scoped) | +| `/t/{slug}/*` | per-tenant server-ui | Provisioned tenant UI containers (Traefik labels) | | `/` | redirect -> `/platform/` | Via `docker/traefik-dynamic.yml` | | `/*` (catch-all) | cameleer-logto:3001 (priority=1) | Custom sign-in UI, OIDC, interaction | @@ -91,17 +100,25 @@ All services on one hostname. Two env vars control everything: `PUBLIC_HOST` + ` - TLS: self-signed cert init container (`traefik-certs`) for dev, ACME for production - Root `/` -> `/platform/` redirect via Traefik file provider (`docker/traefik-dynamic.yml`) - LoginPage auto-redirects to Logto OIDC (no intermediate button) +- Per-tenant server containers get Traefik labels for `/t/{slug}/*` routing at provisioning time ### Docker Networks -Two networks in docker-compose.yml: +Compose-defined networks: | Network | Name on Host | Purpose | |---------|-------------|---------| -| `cameleer` | `cameleer-saas_cameleer` | Compose default — all services (DB, Logto, SaaS, server) | -| `cameleer-traefik` | `cameleer-traefik` (fixed `name:`) | Traefik + server + deployed app containers | +| `cameleer` | `cameleer-saas_cameleer` | Compose default — shared services (DB, Logto, SaaS) | +| `cameleer-traefik` | `cameleer-traefik` (fixed `name:`) | Traefik + provisioned tenant containers | -The `cameleer-traefik` network uses `name: cameleer-traefik` (no compose project prefix) so `DockerNetworkManager.ensureNetwork("cameleer-traefik")` in the server finds it. The server joins with DNS alias `cameleer3-server`, matching `CAMELEER_SERVER_URL=http://cameleer3-server:8081`. Per-environment networks (`cameleer-env-{slug}`) are created dynamically by the server's `DockerNetworkManager`. +Per-tenant networks (created dynamically by `DockerTenantProvisioner`): + +| Network | Name Pattern | Purpose | +|---------|-------------|---------| +| Tenant network | `cameleer-tenant-{slug}` | Internal bridge, no internet — isolates tenant server + apps | +| Environment network | `cameleer-env-{tenantId}-{envSlug}` | Tenant-scoped (includes tenantId to prevent slug collision across tenants) | + +Server containers join three networks: tenant network (primary), shared services network (`cameleer`), and traefik network. Apps deployed by the server use the tenant network as primary. ### Custom sign-in UI (`ui/sign-in/`) @@ -119,32 +136,38 @@ Separate Vite+React SPA replacing Logto's default sign-in page. Visually matches - Tenant isolation enforced by `TenantIsolationInterceptor` (a single `HandlerInterceptor` on `/api/**` that resolves JWT org_id to TenantContext and validates `{tenantId}`, `{environmentId}`, `{appId}` path variables; fail-closed, platform admins bypass) - 13 OAuth2 scopes on the Logto API resource (`https://api.cameleer.local`): 10 platform scopes + 3 server scopes (`server:admin`, `server:operator`, `server:viewer`), served to the frontend from `GET /platform/api/config` - Server scopes map to server RBAC roles via JWT `scope` claim (SaaS platform path) or `roles` claim (server-ui OIDC login path) -- 4-role model: `saas-vendor` (global, hosted only), org `owner` -> `server:admin`, org `operator` -> `server:operator`, org `viewer` -> `server:viewer` -- `saas-vendor` global role injected via `docker/vendor-seed.sh` (not standard bootstrap) — has `platform:admin` + all tenant scopes +- Org roles: `owner` -> `server:admin` + `tenant:manage`, `operator` -> `server:operator`, `viewer` -> `server:viewer` +- `saas-vendor` global role injected via `docker/vendor-seed.sh` (`VENDOR_SEED_ENABLED=true` in dev) — has `platform:admin` + all tenant scopes - Custom `JwtDecoder` in `SecurityConfig.java` — ES384 algorithm, `at+jwt` token type, split issuer-uri (string validation) / jwk-set-uri (Docker-internal fetch), audience validation (`https://api.cameleer.local`) - Logto Custom JWT (Phase 7b in bootstrap) injects a `roles` claim into access tokens based on org roles and global roles — this makes role data available to the server without Logto-specific code -### Server integration (cameleer3-server env vars) +### Auth routing by persona + +| Persona | Logto role | Key scope | Landing route | +|---------|-----------|-----------|---------------| +| Vendor | `saas-vendor` (global) | `platform:admin` | `/vendor/tenants` | +| Tenant admin | org `owner` | `tenant:manage` | `/tenant` (dashboard) | +| Regular user (operator/viewer) | org member | `server:operator` or `server:viewer` | Redirected to server dashboard directly | + +- `LandingRedirect` component waits for scopes to load, then routes to the correct persona landing page +- `RequireScope` guard on route groups enforces scope requirements +- SSO bridge: Logto session carries over to provisioned server's OIDC flow (Traditional Web App per tenant) + +### Per-tenant server env vars (set by DockerTenantProvisioner) + +These env vars are injected into provisioned per-tenant server containers: | Env var | Value | Purpose | |---------|-------|---------| | `CAMELEER_OIDC_ISSUER_URI` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}/oidc` | Token issuer claim validation | | `CAMELEER_OIDC_JWK_SET_URI` | `http://logto:3001/oidc/jwks` | Docker-internal JWK fetch | -| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` | Skip cert verify for OIDC discovery (dev only — disable in production) | +| `CAMELEER_OIDC_TLS_SKIP_VERIFY` | `true` | Skip cert verify for OIDC discovery (dev only) | | `CAMELEER_CORS_ALLOWED_ORIGINS` | `${PUBLIC_PROTOCOL}://${PUBLIC_HOST}` | Allow browser requests through Traefik | -| `BASE_PATH` (server-ui) | `/server` | React Router basename + `` tag | - -### Server runtime env vars (docker-compose.dev.yml) - -| Env var | Value | Purpose | -|---------|-------|---------| | `CAMELEER_RUNTIME_ENABLED` | `true` | Enable Docker orchestration | -| `CAMELEER_JAR_STORAGE_PATH` | `/data/jars` | Where JARs are stored inside server container | -| `CAMELEER_RUNTIME_BASE_IMAGE` | `gitea.siegeln.net/cameleer/cameleer-runtime-base:latest` | Base image for deployed apps | -| `CAMELEER_SERVER_URL` | `http://cameleer3-server:8081` | Server URL agents connect to | +| `CAMELEER_SERVER_URL` | `http://cameleer3-server-{slug}:8081` | Per-tenant server URL (DNS alias on tenant network) | | `CAMELEER_ROUTING_DOMAIN` | `${PUBLIC_HOST}` | Domain for Traefik routing labels | | `CAMELEER_ROUTING_MODE` | `path` | `path` or `subdomain` routing | -| `CAMELEER_JAR_DOCKER_VOLUME` | `cameleer-saas_jardata` | Named volume for Docker-in-Docker JAR mounting | +| `BASE_PATH` (server-ui) | `/t/{slug}` | React Router basename + `` tag | ### Server OIDC role extraction (two paths) @@ -172,25 +195,38 @@ Key files: - `docker/runtime-base/Dockerfile` — base image with agent JAR, maps env vars to `-D` system properties - `ServerApiClient.java` — M2M token acquisition for SaaS->server API calls (agent status). Uses `X-Cameleer-Protocol-Version: 1` header - Docker socket access: `group_add: ["0"]` in docker-compose.dev.yml (not root group membership in Dockerfile) -- Network: deployed containers join `cameleer-traefik` (routing) + `cameleer-env-{slug}` (isolation) +- Network: deployed containers join `cameleer-tenant-{slug}` (primary, isolation) + `cameleer-traefik` (routing) + `cameleer-env-{tenantId}-{envSlug}` (environment isolation) ### Bootstrap (`docker/logto-bootstrap.sh`) -Idempotent script run via `logto-bootstrap` init container. Phases: -1. Wait for Logto + server health +Idempotent script run via `logto-bootstrap` init container. **Clean slate** — no example tenant, no viewer user, no server configuration. Phases: +1. Wait for Logto health (no server to wait for — servers are provisioned per-tenant) 2. Get Management API token (reads `m-default` secret from DB) -3. Create Logto apps (SPA, Traditional with `skipConsent`, M2M with Management API role + server API role) +3. Create Logto apps (SPA, Traditional Web App with `skipConsent`, M2M with Management API role + server API role) 3b. Create API resource scopes (10 platform + 3 server scopes) 4. Create org roles (owner, operator, viewer with API resource scope assignments) + M2M server role (`cameleer-m2m-server` with `server:admin` scope) -5. Create users (platform owner with Logto console access, viewer for testing read-only OIDC) -6. Create organization, add users with org roles (owner + viewer) -7. Configure cameleer3-server OIDC (`rolesClaim: "roles"`, `audience`, `defaultRoles: ["VIEWER"]`) +5. Create admin user (platform owner with Logto console access) 7b. Configure Logto Custom JWT for access tokens (maps org roles -> `roles` claim: admin->server:admin, member->server:viewer) 8. Configure Logto sign-in branding (Cameleer colors `#C6820E`/`#D4941E`, logo from `/platform/logo.svg`) 9. Cleanup seeded Logto apps 10. Write bootstrap results to `/data/logto-bootstrap.json` -Platform owner credentials (`SAAS_ADMIN_USER`/`SAAS_ADMIN_PASS`) work for both the SaaS platform and the Logto console (port 3002). The `saas-vendor` global role (hosted only) is created separately via `docker/vendor-seed.sh`. +Vendor user is seeded separately via `docker/vendor-seed.sh` (`VENDOR_SEED_ENABLED=true` in dev). The compose stack is: Traefik + PostgreSQL + ClickHouse + Logto + logto-bootstrap + cameleer-saas. No `cameleer3-server` or `cameleer3-server-ui` in compose — those are provisioned per-tenant by `DockerTenantProvisioner`. + +### Tenant Provisioning Flow + +When vendor creates a tenant via `VendorTenantService`: +1. Create `TenantEntity` (status=PROVISIONING) + Logto organization +2. Create admin user in Logto with owner org role +3. Add vendor user to new org for support access +4. Register OIDC redirect URIs for `/t/{slug}/oidc/callback` on Logto Traditional Web App +5. Generate license (tier-appropriate, 365 days) +6. Create tenant-isolated Docker network (`cameleer-tenant-{slug}`) +7. Create server + UI containers with correct env vars, Traefik labels, health check +8. Wait for health check (`/api/v1/health`, not `/actuator/health` which requires auth) +9. Push license token to server via M2M API +10. Push OIDC config (Logto Traditional Web App credentials) to server for SSO +11. Update tenant status -> ACTIVE ## Database Migrations @@ -211,13 +247,116 @@ PostgreSQL (Flyway): `src/main/resources/db/migration/` - CI: `.gitea/workflows/` — Gitea Actions - K8s target: k3s cluster at 192.168.50.86 - Docker images: CI builds and pushes all images — Dockerfiles use multi-stage builds, no local builds needed - - `cameleer-saas` — SaaS app (frontend + JAR baked in) + - `cameleer-saas` — SaaS vendor management plane (frontend + JAR baked in) - `cameleer-logto` — custom Logto with sign-in UI baked in + - `cameleer3-server` / `cameleer3-server-ui` — provisioned per-tenant (not in compose, created by `DockerTenantProvisioner`) - `cameleer-runtime-base` — base image for deployed apps (agent JAR + JRE). CI downloads latest agent SNAPSHOT from Gitea Maven registry. Uses `CAMELEER_SERVER_URL` env var (not CAMELEER_EXPORT_ENDPOINT). - Docker builds: `--no-cache`, `--provenance=false` for Gitea compatibility -- `docker-compose.dev.yml` — exposes ports for direct access, sets `SPRING_PROFILES_ACTIVE: dev`. Volume-mounts `./ui/dist` into the container so local UI builds are served without rebuilding the Docker image (`SPRING_WEB_RESOURCES_STATIC_LOCATIONS` overrides classpath). Adds Docker socket mount, jardata volume, and runtime env vars for container orchestration. +- `docker-compose.dev.yml` — exposes ports for direct access, sets `SPRING_PROFILES_ACTIVE: dev`, `VENDOR_SEED_ENABLED: true`. Volume-mounts `./ui/dist` into the container so local UI builds are served without rebuilding the Docker image (`SPRING_WEB_RESOURCES_STATIC_LOCATIONS` overrides classpath). Adds Docker socket mount for tenant provisioning. - Design system: import from `@cameleer/design-system` (Gitea npm registry) ## Disabled Skills - Do NOT use any `gsd:*` skills in this project. This includes all `/gsd:` prefixed commands. + + +# GitNexus — Code Intelligence + +This project is indexed by GitNexus as **cameleer-saas** (1686 symbols, 2709 relationships, 97 execution flows). Use the GitNexus MCP tools to understand code, assess impact, and navigate safely. + +> If any GitNexus tool warns the index is stale, run `npx gitnexus analyze` in terminal first. + +## Always Do + +- **MUST run impact analysis before editing any symbol.** Before modifying a function, class, or method, run `gitnexus_impact({target: "symbolName", direction: "upstream"})` and report the blast radius (direct callers, affected processes, risk level) to the user. +- **MUST run `gitnexus_detect_changes()` before committing** to verify your changes only affect expected symbols and execution flows. +- **MUST warn the user** if impact analysis returns HIGH or CRITICAL risk before proceeding with edits. +- When exploring unfamiliar code, use `gitnexus_query({query: "concept"})` to find execution flows instead of grepping. It returns process-grouped results ranked by relevance. +- When you need full context on a specific symbol — callers, callees, which execution flows it participates in — use `gitnexus_context({name: "symbolName"})`. + +## When Debugging + +1. `gitnexus_query({query: ""})` — find execution flows related to the issue +2. `gitnexus_context({name: ""})` — see all callers, callees, and process participation +3. `READ gitnexus://repo/cameleer-saas/process/{processName}` — trace the full execution flow step by step +4. For regressions: `gitnexus_detect_changes({scope: "compare", base_ref: "main"})` — see what your branch changed + +## When Refactoring + +- **Renaming**: MUST use `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` first. Review the preview — graph edits are safe, text_search edits need manual review. Then run with `dry_run: false`. +- **Extracting/Splitting**: MUST run `gitnexus_context({name: "target"})` to see all incoming/outgoing refs, then `gitnexus_impact({target: "target", direction: "upstream"})` to find all external callers before moving code. +- After any refactor: run `gitnexus_detect_changes({scope: "all"})` to verify only expected files changed. + +## Never Do + +- NEVER edit a function, class, or method without first running `gitnexus_impact` on it. +- NEVER ignore HIGH or CRITICAL risk warnings from impact analysis. +- NEVER rename symbols with find-and-replace — use `gitnexus_rename` which understands the call graph. +- NEVER commit changes without running `gitnexus_detect_changes()` to check affected scope. + +## Tools Quick Reference + +| Tool | When to use | Command | +|------|-------------|---------| +| `query` | Find code by concept | `gitnexus_query({query: "auth validation"})` | +| `context` | 360-degree view of one symbol | `gitnexus_context({name: "validateUser"})` | +| `impact` | Blast radius before editing | `gitnexus_impact({target: "X", direction: "upstream"})` | +| `detect_changes` | Pre-commit scope check | `gitnexus_detect_changes({scope: "staged"})` | +| `rename` | Safe multi-file rename | `gitnexus_rename({symbol_name: "old", new_name: "new", dry_run: true})` | +| `cypher` | Custom graph queries | `gitnexus_cypher({query: "MATCH ..."})` | + +## Impact Risk Levels + +| Depth | Meaning | Action | +|-------|---------|--------| +| d=1 | WILL BREAK — direct callers/importers | MUST update these | +| d=2 | LIKELY AFFECTED — indirect deps | Should test | +| d=3 | MAY NEED TESTING — transitive | Test if critical path | + +## Resources + +| Resource | Use for | +|----------|---------| +| `gitnexus://repo/cameleer-saas/context` | Codebase overview, check index freshness | +| `gitnexus://repo/cameleer-saas/clusters` | All functional areas | +| `gitnexus://repo/cameleer-saas/processes` | All execution flows | +| `gitnexus://repo/cameleer-saas/process/{name}` | Step-by-step execution trace | + +## Self-Check Before Finishing + +Before completing any code modification task, verify: +1. `gitnexus_impact` was run for all modified symbols +2. No HIGH/CRITICAL risk warnings were ignored +3. `gitnexus_detect_changes()` confirms changes match expected scope +4. All d=1 (WILL BREAK) dependents were updated + +## Keeping the Index Fresh + +After committing code changes, the GitNexus index becomes stale. Re-run analyze to update it: + +```bash +npx gitnexus analyze +``` + +If the index previously included embeddings, preserve them by adding `--embeddings`: + +```bash +npx gitnexus analyze --embeddings +``` + +To check whether embeddings exist, inspect `.gitnexus/meta.json` — the `stats.embeddings` field shows the count (0 means no embeddings). **Running analyze without `--embeddings` will delete any previously generated embeddings.** + +> Claude Code users: A PostToolUse hook handles this automatically after `git commit` and `git merge`. + +## CLI + +| Task | Read this skill file | +|------|---------------------| +| Understand architecture / "How does X work?" | `.claude/skills/gitnexus/gitnexus-exploring/SKILL.md` | +| Blast radius / "What breaks if I change X?" | `.claude/skills/gitnexus/gitnexus-impact-analysis/SKILL.md` | +| Trace bugs / "Why is X failing?" | `.claude/skills/gitnexus/gitnexus-debugging/SKILL.md` | +| Rename / extract / split / refactor | `.claude/skills/gitnexus/gitnexus-refactoring/SKILL.md` | +| Tools, resources, schema reference | `.claude/skills/gitnexus/gitnexus-guide/SKILL.md` | +| Index, status, clean, wiki CLI commands | `.claude/skills/gitnexus/gitnexus-cli/SKILL.md` | + +