Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
59 commits
Select commit Hold shift + click to select a range
d7cdf71
feat(studio): scaffold Karrio Studio TanStack Start foundation
claude May 29, 2026
3849d08
fix(studio): sync package-lock for new @karrio/studio workspace
claude May 29, 2026
cf6a5ba
refactor(studio): make Studio standalone, decoupled from Next.js + mo…
claude May 29, 2026
5bf365c
feat(studio): Shipments screen + sheet (C2), wired to decoupled client
claude May 29, 2026
52aa5eb
feat(studio): Trackers (C3) + Orders (C4) + reusable component layer
claude May 29, 2026
be3cb25
feat(studio): complete Ship-mode screens C5–C11
claude May 29, 2026
ddba61c
refactor(studio): a11y hardening + drop unused dep (review remediation)
claude May 29, 2026
5fa4663
feat(studio): responsive layout + dark/light review
claude May 29, 2026
965283b
feat(studio): Build mode — Apps, Plugins, MCP, Webhooks, API keys (D1…
claude May 29, 2026
3c9077d
feat(studio): Govern mode — Admin, Tenants, Team, Security, Audit, Se…
claude May 29, 2026
98397a7
feat(studio): Auth UI — sign in / sign up / forgot (B2)
claude May 29, 2026
3c0e5ba
feat(studio): command palette (H1) + workbench overlay (D5) + quick-c…
claude May 29, 2026
20f2efb
feat(studio): Editor agent IDE (D4) + AI Assistant (F1)
claude May 29, 2026
c02d383
feat(studio): auth route guards (B3)
claude May 29, 2026
3206e84
feat(studio): customization / Tweaks panel (G1-G3)
claude May 29, 2026
f1c894d
feat(studio): write interactions — Address create/edit/delete (forms …
claude May 29, 2026
f999a43
feat(studio): seeded Karrio sandbox + live integration tests + respon…
claude May 29, 2026
c9ee708
fix(documents): offline-resilient stylesheet load in generator
claude May 29, 2026
aae1bb0
perf(studio): meta description + theme-color; record Lighthouse baseline
claude May 29, 2026
f8a9850
feat(studio): Parcel create/edit/delete forms (C9 write interactions)
claude May 29, 2026
6241025
feat(studio): Product create/edit/delete forms (C10 write interactions)
claude May 29, 2026
c234046
perf(studio): code-split screens via React.lazy + Suspense (EBE-87)
claude May 29, 2026
c529efe
docs+feat(studio): dashboard parity audit + monitoring seam + error b…
claude May 29, 2026
7ed390f
fix(studio): track Build-mode screens (gitignored) + Connections/Webh…
claude May 29, 2026
bf5b6ee
test(studio): accessibility + keyboard-navigation coverage
claude May 29, 2026
170693b
feat(studio): agent-driven carrier scaffolding in the Editor (F4)
claude May 29, 2026
7fe15ec
feat(studio): dashboard parity — Manifests, Batches, Workflows, Rate …
claude May 30, 2026
02a92fb
docs(studio): UI screenshots from seeded sandbox (dark+light+mobile)
claude May 30, 2026
f09c319
fix(studio): high-fidelity data, carrier badges, mobile topbar + disp…
claude May 30, 2026
62913f2
chore: ignore Claude Code agent worktrees
claude May 30, 2026
71806ec
test(studio): render-smoke + CI workflow + visual baseline scaffold (…
danh91 May 30, 2026
7ae95ee
fix(studio): align Studio GraphQL queries to live OSS schema (#1109)
danh91 May 30, 2026
4c89e11
feat(studio): preferences persistence module + backend sync seam (#1110)
danh91 May 30, 2026
99a67a6
feat(studio): agent definitions + MCP server config persistence (#1111)
danh91 May 30, 2026
5b2ab40
feat(studio): real Home metrics (C1) + JWT token refresh (B1)
claude May 31, 2026
af7576b
fix(studio): Home links use client-side router navigation (H5 review)
claude May 31, 2026
81d99a6
docs(studio): refresh screenshots from seeded sandbox (real Home metr…
claude May 31, 2026
4be66a7
docs(studio): correct PARITY audit — manifests/batches/rate-sheets/wo…
claude May 31, 2026
dfc2eab
docs(studio): add ARCHITECTURE.md — implementation, dataflow, Karrio …
claude May 31, 2026
af51198
refactor(studio): F0 foundation for GraphQL-first rebuild
claude May 31, 2026
d936578
feat(studio): Units B+C 404-fixes — api_keys/team/admin/usage/audit o…
claude May 31, 2026
0f6f22a
fix(studio): seed valid shipment statuses (GraphQL ShipmentStatusEnum)
claude May 31, 2026
24135a0
feat(studio): Unit A — all Ship reads on canonical GraphQL
claude May 31, 2026
aed6599
feat(studio): honest 'not available' states for no-source screens (B/…
claude May 31, 2026
bbc9a10
feat(studio): Unit E — webhook list on GraphQL; all reads GraphQL-first
claude May 31, 2026
96bfefc
docs(studio): mark GraphQL rebuild status (A/B/C/E done, D backend-bl…
claude May 31, 2026
76231d1
Merge branch 'claude/studio-graphql-rebuild' into claude/lucid-wright…
claude May 31, 2026
732b8a3
feat(studio): Unit D — real backend persistence via Karrio metafields…
claude May 31, 2026
01b14ff
docs(studio): metafield-backed studio state (EBE-99 done) in ARCHITEC…
claude May 31, 2026
c9a90c3
feat(studio): real shell — user menu + logout, workspace menu, workin…
claude May 31, 2026
cbc35ac
feat(studio): usage + home trend charts from real system_usage time-s…
claude May 31, 2026
1da4641
feat(studio): dynamic per-carrier connection forms + configs (EBE-110…
claude May 31, 2026
4560ad3
feat(studio): feature-flag gating of nav via /v1/references (EBE-109)
claude May 31, 2026
b002c83
feat(studio): document template editor — code + live preview, GraphQL…
claude May 31, 2026
fbedf44
feat(studio): rate sheet editor + CSV import (EBE-105)
claude May 31, 2026
c3be85b
feat(studio): functional bulk actions on Shipments — CSV export + pri…
claude May 31, 2026
eefd5fe
feat(studio): real admin overview from /admin/graphql (EBE-108 phase 1)
claude May 31, 2026
4544b9f
feat(studio): bulk label purchase + admin user CRUD (EBE-107/EBE-108)
claude May 31, 2026
985b2b7
fix(studio): use Web Crypto CSPRNG for invite password
claude May 31, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
94 changes: 94 additions & 0 deletions .github/workflows/studio-e2e.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
name: studio-e2e

# Run on every push and on PRs touching Studio source or its E2E specs.
# The mocked `studio` Playwright project does NOT require a Django backend or
# Postgres — it intercepts all API calls at the Playwright route level.
on:
push:
paths:
- "apps/studio/**"
- "packages/e2e/tests/studio/**"
- "packages/e2e/helpers/studio.ts"
- "packages/e2e/playwright.config.ts"
- ".github/workflows/studio-e2e.yml"
pull_request:
paths:
- "apps/studio/**"
- "packages/e2e/tests/studio/**"
- "packages/e2e/helpers/studio.ts"
- "packages/e2e/playwright.config.ts"
- ".github/workflows/studio-e2e.yml"
workflow_dispatch:

permissions:
contents: read

jobs:
studio-e2e:
runs-on: ubuntu-latest
timeout-minutes: 20

steps:
- uses: actions/checkout@v4
with:
submodules: false

- uses: actions/setup-node@v4
with:
node-version: 22.x
cache: "npm"
cache-dependency-path: apps/studio/package-lock.json

# Install Studio app dependencies (standalone lockfile, no turborepo).
- name: Install Studio deps
working-directory: apps/studio
run: npm ci

# Build the Studio app so the dev server can serve the compiled output.
# `vite build` produces apps/studio/dist (client) + .output/server (SSR).
- name: Build Studio
working-directory: apps/studio
run: npm run build

# Install Playwright + Chromium only (mocked suite only needs one browser).
- name: Install Playwright browsers
working-directory: packages/e2e
run: |
npm ci
npx playwright install chromium --with-deps

# Start the Studio dev server in the background and wait for it.
# We use `npm run dev` (vite dev) instead of the built server so hot-reload
# is available; the mocked suite doesn't care which mode the server is in.
- name: Start Studio dev server
working-directory: apps/studio
run: npm run dev &
env:
PORT: 3003

- name: Wait for Studio to be ready
run: |
for i in $(seq 1 30); do
curl -sf http://localhost:3003 && break || true
echo "Waiting for Studio ($i/30)..."
sleep 2
done
curl -sf http://localhost:3003 || (echo "Studio never became ready" && exit 1)

# Run the mocked `studio` Playwright project.
# No KARRIO_LIVE or Django API needed — all API calls are intercepted.
- name: Run Studio E2E (mocked)
working-directory: packages/e2e
run: npx playwright test --project=studio --reporter=github,html
env:
KARRIO_STUDIO_URL: http://localhost:3003
CI: "true"

# Upload the HTML report so failures are inspectable via the Actions UI.
- name: Upload Playwright report
if: always()
uses: actions/upload-artifact@v4
with:
name: studio-playwright-report
path: packages/e2e/playwright-report/
retention-days: 14
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -157,3 +157,6 @@ htmlcov/
# Temporary files
*.tmp
*.backup

# Claude Code harness-managed temporary agent worktrees
.claude/worktrees/
331 changes: 331 additions & 0 deletions PRDs/KARRIO_STUDIO.md

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions apps/studio/.env.sample
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
# Karrio backend (GraphQL + REST) — the single source of truth for ALL state.
# Shipping data AND Studio-native state (customization, agents, MCP config) are
# persisted on the Karrio backend (passthrough). No separate Studio database.
# Server-side base URL
KARRIO_API=http://localhost:5002
# Client-side base URL (Vite exposes only VITE_-prefixed vars to the browser)
VITE_KARRIO_API=http://localhost:5002

# Studio server session (httpOnly cookie signing)
STUDIO_SESSION_SECRET=change-me-in-production

# Optional: AI Assistant / agents (Claude API)
ANTHROPIC_API_KEY=
16 changes: 16 additions & 0 deletions apps/studio/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
node_modules
.output
.nitro
.tanstack
dist
.env
.env.local
# Generated by the TanStack Start / Router plugin
src/routeTree.gen.ts

# The repo-root .gitignore has `lib/` and `build/` (build output) rules that
# would swallow our source dirs of the same name; re-include them.
!src/lib/
!src/lib/**
!src/screens/build/
!src/screens/build/**
1 change: 1 addition & 0 deletions apps/studio/.npmrc
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ignore-scripts=false
191 changes: 191 additions & 0 deletions apps/studio/ARCHITECTURE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
# Karrio Studio — Architecture

Studio is a **standalone full‑stack TanStack Start app** (`apps/studio/`) that
replaces the legacy Next.js dashboard. It is intentionally **decoupled** from the
monorepo: its own `package.json`/lockfile, its own data layer, shadcn + Tailwind
(no `@karrio/ui`), and its own auth (no `@karrio/hooks`/NextAuth). It talks to a
running Karrio server purely as a **client** — there is **no Studio database**;
Karrio is the single source of truth (passthrough).

```
┌──────────────────────────────────────────────────────────┐
│ apps/studio (SSR) │
│ │
Browser ──┤ TanStack Router (file routes) │
│ __root → _app (guard) → _app.$screen (lazy registry) │
│ │
│ React screens ──useQuery──► data hooks (lib/karrio) │
│ │ │
│ Server Functions (BFF) │ KarrioCtx │
│ auth.* / assistant.* ▼ │
│ │ client.ts (REST + GraphQL) │
└────────┼───────────────────────────┼───────────────────────┘
│ (server‑to‑server) │ (browser fetch, Bearer)
▼ ▼
Anthropic API Karrio server (REST /v1/*, /graphql,
(Claude, optional) /api/token[/refresh])
```

## 1. Module map

```
apps/studio/src/
├── routes/ # TanStack Router file routes
│ ├── __root.tsx # Providers (QueryClient, Session), <html> shell
│ ├── index.tsx # "/" → redirect to /home or /login
│ ├── login.tsx signup.tsx forgot.tsx # public auth pages
│ ├── _app.tsx # authenticated layout + beforeLoad guard
│ └── _app.$screen.tsx # dynamic screen dispatcher (one route, N screens)
├── server/ # Server Functions = Studio's BFF "API"
│ ├── auth.ts # login/refresh/logout/register/reset/getSession
│ └── assistant.ts # sendAssistantMessage → Anthropic (server-side)
├── lib/karrio/ # Decoupled data layer (no @karrio/*)
│ ├── env.ts # base URL resolution (KARRIO_API / VITE_KARRIO_API)
│ ├── client.ts # restGet/restMutate/graphql + authedFetch (401→refresh)
│ ├── session.tsx # SessionProvider → KarrioCtx, refresh handler
│ ├── hooks.ts # 19 REST + 6 GraphQL TanStack Query hooks + mutations
│ ├── types.ts # local API types (no @karrio/types)
│ ├── display.ts # entity → display-string helpers
│ ├── agents.ts # AgentDef + McpServerConfig store (metafields + LS cache)
│ ├── preferences.ts # UI prefs (metafields + LS cache)
│ └── metastore.ts # per-user backend KV over Karrio metafields
├── lib/{modes,studio-state}.ts # nav model (Ship/Build/Govern) + UI state
├── screens/ # registry.tsx (React.lazy) → ship/* build/* govern/*
└── components/ # shell (Sidebar/Topbar), ui (primitives/Sheet/...),
# overlays (CommandPalette/Workbench/TweaksPanel)
```

## 2. Request / data flow

Reads use TanStack Query hooks; writes use mutation hooks that invalidate the
relevant query keys. Every call carries auth + tenant context derived from the
session.

```
Screen (useShipments) lib/karrio/hooks.ts
│ useQuery(["shipments", …]) ───► queryFn: restGet(ctx, "/v1/shipments")
│ │
│ client.ts authedFetch(ctx, url)
│ │ headers: Authorization: Bearer <access>
│ │ x-org-id, x-test-mode
│ ▼
│ Karrio GET /v1/shipments ──► 200 JSON
│ │
│ on 401 ──► refreshHandler() ──► refreshSession() (server fn) ──► new access
│ (de-duped, single retry with the rotated token)
render
```

`KarrioCtx = { baseUrl, token, orgId, testMode }` is assembled once by
`SessionProvider` and read by every hook via `useKarrioCtx()`. The token comes
from the httpOnly session cookie (never exposed to JS except as the in-memory
bearer surfaced through the session query).

**REST vs GraphQL split** (mirrors the Karrio API surface):

| Transport | Used for |
|---|---|
| **REST `/v1/*`** | shipments, trackers, connections, pickups, documents, manifests, batches, apps, plugins, webhooks, api_keys, admin, mcp, usage (19 hooks) |
| **GraphQL `/graphql`** | orders, address/parcel/product templates, rate_sheets, shipping_rules, workflows (6 hooks) + mutations (create/update/delete address/parcel/product, update_user, register_user, request_password_reset) |

## 3. Auth & session flow

Studio runs a thin **Backend‑for‑Frontend**: server functions proxy Karrio's JWT
endpoints and keep tokens in a single httpOnly cookie so SSR can authenticate and
the browser never stores raw tokens.

```
Login page ──login({email,password})──► server/auth.ts
POST {KARRIO_API}/api/token
◄─ { access, refresh }
Set httpOnly cookie "karrio-studio-session"
SSR / hydrate ──getSession()──► reads cookie → { access, email } → SessionProvider

401 on any API call:
client.authedFetch ── refreshHandler ──► refreshSession() (server fn)
POST /api/token/refresh { refresh }
◄─ { access, refresh } (rotates)
updates cookie + cached session
◄── new access ── retry original request once
```

Route protection: `_app.tsx`'s `beforeLoad` redirects unauthenticated requests to
`/login`; the public auth routes opt out of the session storage state.

## 4. Karrio integration points

| Concern | Endpoint(s) | Notes |
|---|---|---|
| Auth | `POST /api/token`, `POST /api/token/refresh`, `POST /api/logout` | simplejwt; tokens in httpOnly cookie |
| Operational data | `GET/POST/PATCH/DELETE /v1/*` | REST resources |
| Relational/template data | `POST /graphql` | connections-style queries + typed mutations |
| Tenancy | headers `x-org-id`, `x-test-mode` | passed from `KarrioCtx` on every call |
| Studio-native state | `metafields` (create/update/delete_metafield + filter by key) | per-user JSON under `studio.*` (prefs, agents, MCP configs) — round-trips |
| Registration / reset | `register_user`, `request_password_reset` GraphQL | |

No schema/migrations are added to Karrio. The one backend file touched on this
branch is `modules/documents/.../generator.py` — an offline‑CSS guard for the
local sandbox, unrelated to Studio's runtime.

## 5. Custom Studio "APIs" (server functions)

These are the **net‑new endpoints Studio adds** — TanStack Start server functions
(typed RPC), not REST routes. They form the BFF layer:

| Server function | Method | Purpose |
|---|---|---|
| `login` | POST | exchange credentials at `/api/token`, set httpOnly session |
| `refreshSession` | POST | rotate access via `/api/token/refresh`, update cookie |
| `logout` | POST | clear the session cookie |
| `register` | POST | `register_user` GraphQL mutation |
| `requestPasswordReset` | POST | `request_password_reset` GraphQL mutation |
| `getSession` | GET | read the session cookie for SSR + client |
| `sendAssistantMessage` | POST | proxy to the Anthropic Messages API (Claude) server‑side, keeping `ANTHROPIC_API_KEY` off the client; returns a deterministic stub when unset |

## 6. Net‑new Studio capabilities (beyond dashboard parity)

- **Command palette (⌘K)**, **Workbench** dev overlay, **Tweaks** appearance panel.
- **Self‑editable appearance** — `preferences.ts` is the single source of truth for
theme/accent/density/font/layout. Persisted **per‑user to the Karrio backend** as a
JSON **metafield** (`studio.customization`) via `metastore.ts`, with `localStorage`
as the offline cache. `loadFromBackend()` hydrates on login, so settings follow the
user across devices (full create/read/update/delete round trip — no backend change).
- **Agents & MCP management** — `agents.ts` is a typed store for `AgentDef` and
`McpServerConfig`, persisted as metafields (`studio.agents` / `studio.mcp-servers`)
with a `localStorage` write‑through cache. MCP entries show an honest **"config‑only"**
status — OSS has no execution proxy, so no connection state is fabricated.
- **Agent IDE / Assistant** — the Editor screen pairs a Claude chat (via
`sendAssistantMessage`) with a client‑side connector **scaffolder**.

## 7. What is NOT built (honest scope / roadmap)

Studio is a **frontend + thin BFF**. The following require Karrio **backend** work
that this branch does **not** include:

- **On‑the‑fly plugin editing with hot reload.** The Editor's `scaffold()` generates
connector files **in the browser** from an SDK‑extension template (mappers /
providers / schemas / tests) for display and AI‑assisted editing. It does **not**
write to the server's plugins directory, and nothing reloads carrier modules at
runtime. A real version needs: (a) an API to persist/edit connector files under
the Karrio plugins path, and (b) a runtime plugin loader/reloader (Karrio imports
connectors as Python packages at boot; hot‑reloading them is non‑trivial). Tracked
as roadmap.
- **MCP tool execution** — config management only; no backend exec proxy.
- **Deeper write‑wizards** — rate‑buy, recurring pickups, rule builder,
create‑manifest/run‑batch, markup editor, fulfillment/role actions, billing.
See `PARITY.md` (all marked 🟡).

## 8. Testing & CI

- `packages/e2e/tests/studio/*` — ~200 mocked Playwright specs (route‑level API
mocking, inline session cookie) + a live‑gated smoke project.
- `render-smoke.spec.ts` — loads every route and fails on any runtime `pageerror`
(catches "build‑green‑but‑broken" crashes that `tsc`/build miss).
- `visual.spec.ts` — screenshot baselines, gated behind `KARRIO_VISUAL=1`.
- `.github/workflows/studio-e2e.yml` — builds Studio, starts the dev server, runs
the mocked project (no Django/Postgres needed).
- Sandbox: `bin/studio-sandbox` + `apps/studio/sandbox/seed.py` provision
high‑fidelity data (shipments with addresses/parcels/charges, trackers,
orders) with a seed self‑check.
```
Loading
Loading