Skip to content

Commit 1bfe463

Browse files
authored
Merge pull request #98 from chirpz-ai/dashboard
Dashboard
2 parents 947f435 + 8995a6f commit 1bfe463

31 files changed

Lines changed: 1857 additions & 714 deletions

backend/CLAUDE.md

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
The repo-wide `CLAUDE.md` at the project root covers monorepo layout, the two-plane API model, backend domain layering, and shared conventions. This file adds the backend-specific details that are easy to get wrong without reading several files.
6+
7+
## Commands
8+
9+
All targets are exposed at the repo root (`make backend-*`) and as host-side targets in `backend/Makefile`. From inside `backend/`:
10+
11+
| Goal | Command |
12+
|---|---|
13+
| Install (locked) | `uv sync --frozen` |
14+
| API server (host) | `make dev` — uvicorn on `:8000` with reload, `APP_ENV=development` |
15+
| Celery worker (host) | `make worker` — needs Redis on `:6379` and Postgres on `:5432` |
16+
| Lint / format | `make lint` / `make format` (ruff over `app/` and `tests/`) |
17+
| Unit tests (host, no infra) | `make test-unit` (or `uv run --group test pytest tests/unit/ -v`) |
18+
| Single unit test | `uv run --group test pytest tests/unit/test_traces.py::test_name -v` |
19+
| Integration tests | From repo root: `make test-integration` — spins up `docker-compose.test.yml` (Postgres :5433, Redis :6380), runs `tests/integration/`, tears down with `-v` |
20+
| New migration | `make migration msg="describe change"` — runs `alembic revision --autogenerate` against **local** Postgres on `:5432` |
21+
| Apply migrations | `make migrate` (auto-applied on `make up` via Docker entrypoint) |
22+
23+
To run integration tests against an already-running test stack: set `POSTGRES_PORT=5433 POSTGRES_DB=pandaprobe_test_db REDIS_PORT=6380` and run `pytest tests/integration/`. `tests/conftest.py` already wires these env vars + `APP_ENV=test` + `CELERY_TASK_ALWAYS_EAGER=true`.
24+
25+
## Two auth dependencies — pick the right one
26+
27+
`app/api/dependencies.py` exposes three dependencies. Routes must pick deliberately:
28+
29+
- `get_api_context`**management plane**. Bearer JWT only. Use for `/user`, `/organizations`, `/projects`, `/api-keys`, `/subscriptions`. Returns `ApiContext` with `organization` always set and `project` always `None`.
30+
- `get_data_plane_context`**data plane**. Accepts Bearer JWT (with `X-Project-ID`) **or** `X-API-Key` (with `X-Project-Name`). **When both are sent, API key wins** — this is intentional to prevent failures when Swagger UI sends a stale JWT alongside a valid key.
31+
- `require_project` — thin wrapper around `get_data_plane_context` that 422s if `ctx.project is None`. Use this on every `/traces`, `/sessions`, `/evaluations` handler.
32+
33+
`_resolve_jwt` JIT-provisions: upserts the user from the IdP claims, auto-creates "My Organization" on first sign-in (plan = DEVELOPMENT when auth is disabled, else default tier), and on new-user creation enqueues welcome/follow-up emails + CRM sync via Celery. Routes get the resolved org/user via `ApiContext` — they should never re-query identity themselves.
34+
35+
`_resolve_api_key` resolves projects by *name* within the API key's org and **auto-creates the project if missing**. This is why SDK clients can call `POST /traces` with any new `X-Project-Name`.
36+
37+
## Celery worker: NullPool + per-task asyncio.run
38+
39+
`app/infrastructure/queue/tasks.py` has a critical pattern documented in its module docstring — don't deviate:
40+
41+
- Worker uses a **dedicated `NullPool` engine** (`_worker_engine`), not the request-path pool from `infrastructure/db/engine.py`. Every task creates a fresh connection via `_worker_session()` and discards it. Reusing a pooled connection across `asyncio.run()` calls causes `"attached to a different loop"` errors because each `asyncio.run()` creates a new event loop.
42+
- Each Celery task body is a sync function that immediately calls `asyncio.run(_async_helper(...))`. Don't add async Celery tasks — use this pattern.
43+
- **Heavy imports go inside the task function**, not at module top. This keeps worker bootup fast and avoids importing FastAPI/auth code into the worker process.
44+
45+
### Dispatcher + per-org worker fanout
46+
47+
For periodic jobs that touch many orgs (usage sync, overage billing, eval monitors), the pattern is **one dispatcher task that queries eligible IDs and fans out one sub-task per org/monitor**:
48+
49+
- `dispatch_sync_usage` → fans out `sync_single_org_usage(org_id)` per active org
50+
- `dispatch_overage_billing` → fans out `bill_single_org(org_id)` per paid active org (rate-limited to `80/s` to stay under Stripe's `100/s` live cap)
51+
- `dispatch_hobby_reset` → fans out `reset_single_hobby_org(org_id)`
52+
- `check_eval_monitors` → fans out `process_single_monitor(monitor_id, project_id)` (and uses a Redis lock `check_eval_monitors` with `timeout=60` so only one beat worker drives the tick)
53+
54+
When adding new periodic work, follow the same shape — failures stay isolated to a single org, and workers parallelise across slots.
55+
56+
### Beat schedule
57+
58+
Configured in `infrastructure/queue/celery_app.py` using `RedBeatScheduler` (Redis-backed; no on-disk schedule file). Current cadence: eval monitors and usage sync every 5 min; overage billing and hobby reset every 6 hours; invitation expiry every hour.
59+
60+
## Auth adapter selection
61+
62+
`infrastructure/auth/adapters.py::get_auth_adapter()` dispatches:
63+
- `AUTH_ENABLED=false``DevelopmentAdapter` (no-op JWT verify, returns a fixed dev identity). Allowed **only** in `APP_ENV=development``Settings._apply_environment_settings` forces `AUTH_ENABLED=true` everywhere else and logs an override warning.
64+
- `AUTH_PROVIDER=firebase``FirebaseAdapter` (uses Firebase Admin SDK + ADC; `GOOGLE_CLOUD_PROJECT` required)
65+
- `AUTH_PROVIDER=supabase` (default) → `SupabaseAdapter` (uses `SUPABASE_URL` + `SUPABASE_KEY`)
66+
67+
Adapters expose a single `verify_token(token) -> Claims` method called via `asyncio.to_thread` since IdP SDKs are sync.
68+
69+
## Settings: env-aware defaults
70+
71+
`Settings._apply_environment_settings` (`app/registry/settings.py`) overrides `DEBUG`, `LOG_LEVEL`, `LOG_FORMAT` per environment **only when the env var is not explicitly set**. So `LOG_LEVEL=DEBUG` in `.env.production` would still win, but the default behaviour gives you JSON `WARNING` logs in prod and console `DEBUG` logs in dev.
72+
73+
The lifespan in `main.py` calls `_validate_stripe_settings()` which **fails fast at startup** if `STRIPE_SECRET_KEY` or `STRIPE_WEBHOOK_SECRET` are missing in staging/production. Don't gate this behind a flag — it's intentional.
74+
75+
## Domain errors, not HTTPException
76+
77+
Raise subclasses of `PandaProbeError` (`registry/exceptions.py`) — `NotFoundError`, `AuthenticationError`, `AuthorizationError`, `ConflictError`, `ValidationError`, `QuotaExceededError`, `OrgLimitReachedError`. The handler in `main.py` translates them to `{"detail": "..."}` JSON with the right status code. Routes that raise `HTTPException` directly bypass this and break the error contract — use the domain exceptions.
78+
79+
Pydantic request-body validation errors are reshaped by `validation_exception_handler` into `{"detail": "Validation error", "errors": [{field, message}, ...]}` — clients depend on this shape.
80+
81+
## Domain entities vs ORM models
82+
83+
Repositories in `infrastructure/db/repositories/` return **core domain entities** (Pydantic models in `app/core/*/entities.py`), not SQLAlchemy `*Model` rows. Services and routes only see entities. If you add a column to a `*Model`, also add it to the entity and the repo mapping — otherwise the field is invisible to callers.
84+
85+
`get_db_session` (`infrastructure/db/engine.py`) is the per-request session dependency. It auto-`commit()`s on success and `rollback()`s on exception. Don't sprinkle `await session.commit()` at the end of route handlers — let the dependency handle it. Workers, in contrast, **must** commit explicitly because they don't go through this dependency.
86+
87+
## Integration test mechanics
88+
89+
`tests/integration/conftest.py` does several non-obvious things — read it before writing new integration tests:
90+
91+
- **`nest_asyncio.apply()`** at import time so Celery's `asyncio.run(...)` works inside the pytest-asyncio loop (eager mode).
92+
- **TRUNCATE-based isolation**, not transaction rollback. The Celery task creates its own session/connection (matching real worker behaviour), so wrapping each test in a transaction would hide its commits from the test code. After each test, every table is `TRUNCATE ... CASCADE`d and the async engine pool is `dispose()`d so pooled connections don't leak into the next test's event loop.
93+
- **Fixed seed UUIDs** (`TEST_ORG_ID`, `TEST_PROJECT_ID`) so every fixture in a test shares the same identity.
94+
- **`autouse` dep overrides** replace `get_db_session`, `require_project`, and `get_redis` for every test. The overridden `require_project` returns a pre-built `ApiContext` — no auth roundtrip.
95+
- **Redis `FLUSHDB`** runs at the end of every test so rate-limiter counters, eval locks, and usage counters don't bleed across tests.
96+
97+
If you need a test that exercises auth resolution itself (not bypassing it), override only `get_db_session` and `get_redis` and let `require_project` run for real.
98+
99+
## Conventions
100+
101+
- Ruff: `line-length = 119`, Google docstrings (`D`), `B`/`ERA` rules on. `D203`/`D213`/`B904`/`B008`/`D107`/`E501`/`F401` are intentionally ignored. `unfixable = ["B"]` — bugbear findings are surfaced but not auto-fixed.
102+
- `tests/*` get `D100`/`D103`/`D104` waived; `__init__.py` files get `E402`/`D104` waived.
103+
- API keys are stored hashed (`registry/security.py::hash_api_key`); never log or persist the raw key — only the prefix shown in `IdentityRepository`.
104+
- New tables / column changes require an Alembic migration **and** an updated ORM model. `migration` runs autogenerate against the local Postgres on `:5432` — bring up `make up` (or just the postgres service) first.
105+
- The only required runtime env vars in dev are Postgres/Redis connection vars; LLM, Stripe, Resend, Attio, PostHog keys are all optional and the corresponding services no-op when unset (`is_configured()` checks).

frontend/CLAUDE.md

Lines changed: 66 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,66 @@
1-
@AGENTS.md
1+
# CLAUDE.md
2+
3+
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
4+
5+
The repo-wide `CLAUDE.md` at the project root covers monorepo layout, the two-plane API model, backend architecture, and a high-level frontend overview. This file adds the frontend-specific details that are easy to get wrong without reading several files.
6+
7+
## Commands
8+
9+
All targets are exposed at the repo root (`make frontend-*`) and as host-side targets in `frontend/Makefile`. From inside `frontend/`:
10+
11+
| Goal | Command |
12+
|---|---|
13+
| Install (locked) | `yarn install --frozen-lockfile` |
14+
| Dev server (host) | `yarn dev` — runs Next.js on `:3000` against `NEXT_PUBLIC_API_URL` |
15+
| Production build | `yarn build` (uses `output: "standalone"` from `next.config.ts`) |
16+
| Lint / autofix | `yarn lint` / `yarn lint:fix` |
17+
| Type check | `yarn typecheck` (`tsc --noEmit`) |
18+
| Format | `yarn format` (write) / `yarn format:check` (CI gate) |
19+
| Unit tests | `yarn test` (Jest + jsdom) |
20+
| Single Jest test | `yarn test src/__tests__/lib/api/traces.test.ts` (or pass `-t "name"`) |
21+
| E2E (Playwright) | `yarn playwright install --with-deps` once, then `yarn test:e2e` |
22+
23+
Playwright's `webServer` runs `yarn dev` on `:3000` and reuses an existing server outside CI — leaving `yarn dev` running while you iterate on E2E is fine.
24+
25+
## Routing model (App Router)
26+
27+
Authenticated UI lives under `src/app/org/[orgId]/project/[projectId]/{traces,sessions,evaluations,analytics}`. Org-level pages without a project context live under `src/app/org/[orgId]/settings/{organization,members,api-keys,projects,plans,billing}`. The `(auth)` route group holds `/login`.
28+
29+
When adding a page that operates on traces, sessions, or evals, nest it under `org/[orgId]/project/[projectId]/...``OrganizationProvider` and `ProjectProvider` read from the URL segments, and the API client only injects the `X-Organization-ID` / `X-Project-ID` headers when those providers resolve a context.
30+
31+
`src/middleware.ts` is the auth gate. It redirects unauthenticated requests under `/org/*` to `/login?callbackUrl=...` based on the `__pp_session` cookie (`SESSION_COOKIE_NAME` in `lib/auth/config.ts`). The cookie is a presence flag set by `auth-service.setSessionCookie()` after Firebase sign-in — the real bearer token comes from Firebase on each request. When `NEXT_PUBLIC_AUTH_ENABLED=false`, the middleware no-ops and pages render without auth.
32+
33+
## Auth toggle is build-time
34+
35+
`AUTH_ENABLED` is evaluated from `process.env.NEXT_PUBLIC_AUTH_ENABLED` in `src/lib/auth/config.ts`. Next.js constant-folds the comparison at build time, so the same image cannot be flipped between auth-enabled and auth-disabled at runtime — the public GHCR image is built with auth off, the private image with auth on. If you're changing how auth gating works, remember it has to behave correctly under both build flavors.
36+
37+
## API client and provider order
38+
39+
`src/lib/api/client.ts` is a singleton axios instance. It is configured exactly once via `configureAuth({ getToken, forceRefreshToken, getOrgId, getProjectId, onUnauthorized })`, called from `components/providers/ApiConfigProvider.tsx` at mount. The request interceptor pulls a Firebase ID token via `getToken()` and stamps `Authorization`, `X-Organization-ID`, `X-Project-ID` onto every request. Do not build URLs or attach headers by hand — go through the per-resource modules in `src/lib/api/` (`traces.ts`, `sessions.ts`, `evaluations.ts`, etc.).
40+
41+
The response interceptor handles 401 by calling `forceRefreshToken()` and retrying the original request once. Concurrent 401s share a single in-flight refresh via `refreshPromise`. If refresh fails (or the second attempt 401s), `onUnauthorized()` runs — wired in `ApiConfigProvider` to clear the session cookie and redirect to `/login`.
42+
43+
Provider nesting in `components/providers/Providers.tsx` is load-bearing:
44+
```
45+
AuthProvider → PostHogProvider → ToastProvider → QueryClientProvider → ApiConfigProvider
46+
```
47+
`ApiConfigProvider` must sit inside `AuthProvider` (needs the Firebase user) and inside `QueryClientProvider` (so the unauthorized handler can clear caches). `OrganizationProvider` / `ProjectProvider` are mounted lower, inside the `org/[orgId]` and `project/[projectId]` layouts, because they read URL params.
48+
49+
## Query keys
50+
51+
Always pull cache keys from `src/lib/query/keys.ts` (`queryKeys.traces.detail(traceId)`, `queryKeys.evaluations.monitors.list(projectId, params)`, etc.). Inline tuples will silently miss `invalidateQueries` calls keyed off the helpers. When adding a new resource, extend `keys.ts` rather than scattering string literals.
52+
53+
`src/__tests__/lib/query/keys.test.ts` pins the shapes — update both together.
54+
55+
## Tests
56+
57+
- Jest config (`jest.config.ts`): `jsdom`, `ts-jest`, path alias `@/* → src/*`, tests live at `src/__tests__/**/*.test.ts?(x)`, setup at `src/__tests__/setup.ts`.
58+
- `src/__mocks__/` holds MSW handlers and module mocks used across tests. API tests mock at the network layer with MSW rather than stubbing the axios client.
59+
- E2E tests live in `frontend/e2e/` and run against a real `yarn dev` server. The Playwright config only ships a Chromium project.
60+
61+
## Conventions
62+
63+
- Path alias `@/...` resolves to `src/...` (both `tsconfig.json` and Jest).
64+
- Prettier + ESLint via `eslint-config-next` (flat config in `eslint.config.mjs`). `format:check` is the CI gate.
65+
- `next.config.ts` sets security headers (CSP `frame-ancestors 'self'`, HSTS, etc.) for every route — keep an eye on this when wiring up embeds or third-party iframes.
66+
- The only required public env var is `NEXT_PUBLIC_API_URL`. Firebase keys are required only when `NEXT_PUBLIC_AUTH_ENABLED=true`. `NODE_ENV` is owned by Next.js — never set it in `.env.*`.

frontend/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
"format:check": "prettier --check 'src/**/*.{ts,tsx}'"
1818
},
1919
"dependencies": {
20+
"@radix-ui/react-accordion": "^1.2.12",
2021
"@radix-ui/react-avatar": "^1.1.11",
2122
"@radix-ui/react-dialog": "^1.1.15",
2223
"@radix-ui/react-dropdown-menu": "^2.1.16",
1.42 MB
Binary file not shown.
2.45 MB
Binary file not shown.

frontend/src/app/org/[orgId]/page.tsx

Lines changed: 32 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,13 @@
11
"use client";
22

33
import { useEffect } from "react";
4+
import Link from "next/link";
45
import { useParams, useRouter } from "next/navigation";
6+
import { ArrowRight, FolderPlus } from "lucide-react";
57
import { useOrganization } from "@/components/providers/OrganizationProvider";
68
import { useResolvedProjectId } from "@/hooks/useNavigation";
79
import { useDocumentTitle } from "@/hooks/useDocumentTitle";
810
import { Spinner } from "@/components/ui/Spinner";
9-
import { OnboardingChecklist } from "@/components/features/OnboardingChecklist";
1011

1112
export default function OrgPage() {
1213
const { orgId } = useParams();
@@ -32,18 +33,45 @@ export default function OrgPage() {
3233
);
3334
}
3435

36+
// Edge case: user lands here with zero projects.
37+
const projectsHref = `/org/${orgId as string}/settings/projects`;
38+
3539
return (
36-
<div className="max-w-3xl mx-auto py-8 px-4 space-y-6 animate-fade-in">
40+
<div className="max-w-2xl mx-auto py-8 px-4 space-y-6 animate-fade-in">
3741
<div>
3842
<h1 className="text-lg font-mono text-primary">
3943
Welcome to PandaProbe
4044
</h1>
4145
<p className="text-sm font-mono text-text-dim mt-1">
42-
Let&apos;s get you set up before you can send traces.
46+
Create your first project to start tracing your AI agents.
4347
</p>
4448
</div>
4549

46-
<OnboardingChecklist />
50+
<section
51+
className="border-engraved bg-surface"
52+
aria-label="Create your first project"
53+
>
54+
<div className="flex items-center gap-4 px-5 py-4">
55+
<span className="flex-shrink-0 flex items-center justify-center h-10 w-10 border border-primary/40 bg-primary/10 text-primary">
56+
<FolderPlus className="h-4 w-4" />
57+
</span>
58+
<div className="flex-1 min-w-0">
59+
<h2 className="text-sm font-mono text-text">Create a project</h2>
60+
<p className="text-xs font-mono text-text-muted mt-0.5 leading-snug">
61+
Projects isolate traces, sessions, and evaluations. Once
62+
you&apos;ve created one, we&apos;ll walk you through sending your
63+
first trace.
64+
</p>
65+
</div>
66+
<Link
67+
href={projectsHref}
68+
className="flex-shrink-0 inline-flex items-center gap-1.5 px-3 py-1.5 border border-info/40 bg-info/10 text-xs font-mono text-info hover:bg-info/20 transition-colors"
69+
>
70+
Create project
71+
<ArrowRight className="h-3 w-3" />
72+
</Link>
73+
</div>
74+
</section>
4775
</div>
4876
);
4977
}

0 commit comments

Comments
 (0)