From d7cdf71e5e6622802d659f56a2d40c3c0b342ebe Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 29 May 2026 15:39:42 +0000 Subject: [PATCH 01/58] feat(studio): scaffold Karrio Studio TanStack Start foundation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds apps/studio as a new full-stack TanStack Start app that will replace the Next.js dashboard via phased cutover, plus the program PRD and Playwright harness. - App shell: Sidebar + Topbar with Ship/Build/Govern mode IA, route-derived mode, keyboard shortcuts, theme + sidebar-collapse persistence - Design tokens ported from the design handoff (sharp, dense, dark-default + light) - Core UI: Sheet (drawer workhorse), Field, Icon set, CarrierLogo - File-based routing: __root document, /_app shell layout, screen-dispatch route with IA validation; every nav route navigable (real Home + Placeholder) - Studio-native Drizzle schema (app config, agents, MCP) — shipping data stays in Karrio via @karrio/hooks - Server-side auth skeleton proxying Karrio token_auth into an httpOnly session - Playwright: new 'studio' project + shell/routing smoke specs + helper - PRDs/KARRIO_STUDIO.md: full design, agent/subagent plan, test strategy Refs Karrio Studio (Linear) --- PRDs/KARRIO_STUDIO.md | 324 +++++++++++++++ apps/studio/.env.sample | 12 + apps/studio/.gitignore | 10 + apps/studio/README.md | 73 ++++ apps/studio/drizzle.config.ts | 10 + apps/studio/package.json | 41 ++ apps/studio/src/components/shell/Sidebar.tsx | 97 +++++ apps/studio/src/components/shell/Topbar.tsx | 128 ++++++ apps/studio/src/components/ui/CarrierLogo.tsx | 39 ++ apps/studio/src/components/ui/Sheet.tsx | 100 +++++ apps/studio/src/components/ui/icons.tsx | 110 +++++ apps/studio/src/db/index.ts | 23 ++ apps/studio/src/db/schema.ts | 107 +++++ apps/studio/src/router.tsx | 17 + apps/studio/src/routes/__root.tsx | 61 +++ apps/studio/src/routes/_app.$screen.tsx | 19 + apps/studio/src/routes/_app.tsx | 113 ++++++ apps/studio/src/routes/index.tsx | 8 + apps/studio/src/screens/HomeScreen.tsx | 60 +++ apps/studio/src/screens/Placeholder.tsx | 31 ++ apps/studio/src/screens/registry.tsx | 15 + apps/studio/src/server/auth.ts | 73 ++++ apps/studio/src/styles/tokens.css | 380 ++++++++++++++++++ apps/studio/tsconfig.json | 21 + apps/studio/vite.config.ts | 16 + packages/e2e/helpers/studio.ts | 22 + packages/e2e/playwright.config.ts | 11 + packages/e2e/tests/studio/routing.spec.ts | 20 + packages/e2e/tests/studio/shell.spec.ts | 64 +++ 29 files changed, 2005 insertions(+) create mode 100644 PRDs/KARRIO_STUDIO.md create mode 100644 apps/studio/.env.sample create mode 100644 apps/studio/.gitignore create mode 100644 apps/studio/README.md create mode 100644 apps/studio/drizzle.config.ts create mode 100644 apps/studio/package.json create mode 100644 apps/studio/src/components/shell/Sidebar.tsx create mode 100644 apps/studio/src/components/shell/Topbar.tsx create mode 100644 apps/studio/src/components/ui/CarrierLogo.tsx create mode 100644 apps/studio/src/components/ui/Sheet.tsx create mode 100644 apps/studio/src/components/ui/icons.tsx create mode 100644 apps/studio/src/db/index.ts create mode 100644 apps/studio/src/db/schema.ts create mode 100644 apps/studio/src/router.tsx create mode 100644 apps/studio/src/routes/__root.tsx create mode 100644 apps/studio/src/routes/_app.$screen.tsx create mode 100644 apps/studio/src/routes/_app.tsx create mode 100644 apps/studio/src/routes/index.tsx create mode 100644 apps/studio/src/screens/HomeScreen.tsx create mode 100644 apps/studio/src/screens/Placeholder.tsx create mode 100644 apps/studio/src/screens/registry.tsx create mode 100644 apps/studio/src/server/auth.ts create mode 100644 apps/studio/src/styles/tokens.css create mode 100644 apps/studio/tsconfig.json create mode 100644 apps/studio/vite.config.ts create mode 100644 packages/e2e/helpers/studio.ts create mode 100644 packages/e2e/tests/studio/routing.spec.ts create mode 100644 packages/e2e/tests/studio/shell.spec.ts diff --git a/PRDs/KARRIO_STUDIO.md b/PRDs/KARRIO_STUDIO.md new file mode 100644 index 000000000..db3f5433f --- /dev/null +++ b/PRDs/KARRIO_STUDIO.md @@ -0,0 +1,324 @@ +# Karrio Studio — Full-Stack TanStack Start Migration + +| Field | Value | +|-------|-------| +| Project | Karrio | +| Version | 1.0 | +| Date | 2026-05-29 | +| Status | Planning | +| Owner | Studio Program (dan@karrio.io) | +| Type | Architecture / Enhancement / Migration | +| Reference | [AGENTS.md](../AGENTS.md), design handoff `design_handoff_karrio_studio/` | +| Linear | Project "Karrio Studio" (tracked; agents/subagents = issues) | + +--- + +## Table of Contents + +1. [Executive Summary](#executive-summary) +2. [Open Questions & Decisions](#open-questions--decisions) +3. [Problem Statement](#problem-statement) +4. [Goals & Success Criteria](#goals--success-criteria) +5. [Existing Code Analysis](#existing-code-analysis) +6. [Technical Design](#technical-design) +7. [Modes, Screens & Feature Matrix](#modes-screens--feature-matrix) +8. [New Features: Carrier Integration, Editor, Agents & MCP](#new-features) +9. [Agent / Subagent Orchestration Plan (Linear)](#agent--subagent-orchestration-plan-linear) +10. [Implementation Plan](#implementation-plan) +11. [Testing Strategy (Playwright)](#testing-strategy-playwright) +12. [Risk Assessment](#risk-assessment) +13. [Migration & Rollback](#migration--rollback) + +--- + +## Executive Summary + +Karrio Studio replaces the Next.js dashboard (`apps/dashboard`) with a full-stack +**TanStack Start** application (`apps/studio`) that reimagines Karrio as a modular, +agent-friendly "studio" — a WordPress-like experience for managing shipping +operations, carrier plugins, commerce apps, and a developer platform from one +interface. + +The IA is organized around three **modes** — **Ship**, **Build**, **Govern** — driven +by a collapsible sidebar. Studio migrates every dashboard feature, adds **self-editable +/ customizable** app surfaces (theme/density/layout tweaks, a visual editor), and +introduces three net-new capability areas: **agent-first plugin/carrier integration +(Editor)**, **MCP server management**, and an **AI Assistant** woven through the app. + +Decisions locked with the owner: + +| Decision | Choice | +|----------|--------| +| App strategy | **New app `apps/studio`**, phased cutover; `apps/dashboard` stays live until parity | +| Data layer | **Studio-local DB (Drizzle)** for Studio-native state + **Karrio GraphQL/REST** (via `@karrio/hooks`) for shipping data | +| Session 1 scope | Linear project + agent plan, this PRD, and the `apps/studio` foundation scaffold + Playwright harness | + +## Open Questions & Decisions + +| # | Question | Decision | Rationale | +|---|----------|----------|-----------| +| 1 | Replace dashboard in place or build new? | Build `apps/studio` new, deprecate dashboard at parity | Keeps production dashboard running during the multi-phase migration | +| 2 | Where does Studio-native state live? | Drizzle DB (Postgres in prod, SQLite in dev) for Studio-only entities; Karrio API for all shipping data | Avoids backend churn for app-config/agent/MCP state while keeping one source of truth for shipping | +| 3 | Auth model | TanStack Start server functions proxy Karrio auth (JWT) + httpOnly session cookie; reuse Karrio `mutation token_auth`/refresh | Server-side session beats the dashboard's client NextAuth for SSR + security | +| 4 | Component reuse | Reuse `@karrio/types`, `@karrio/lib`; introduce a Studio token/`@karrio/ui-studio` layer for the enterprise aesthetic | `@karrio/ui` is bulma/plex-styled; Studio aesthetic is distinct (sharp, dense, dark-default) | +| 5 | Hooks reuse | `@karrio/hooks` is `"use client"` + NextAuth-coupled. Provide a Studio session adapter so hooks work under TanStack Start; net-new queries use server functions | Maximizes reuse of ~50 existing TanStack Query hooks | + +## Problem Statement + +The current dashboard is a Next.js app composed of many `@karrio/*` packages. It is +feature-rich but: (a) its visual language is generic, (b) it has no first-class +story for AI agents / MCP / self-customization, and (c) carrier/plugin integration +is a developer-CLI task, not an in-product experience. The Studio vision unifies +operations, extensibility, and governance with an enterprise-grade, agent-native UX. + +## Goals & Success Criteria + +| Goal | Success Criteria | +|------|------------------| +| Feature parity with dashboard | Every Ship/Build/Govern screen below implemented and wired to live Karrio APIs | +| Pixel-faithful enterprise UI | Design tokens from `styles.css` ported; sharp corners, 1px borders, dark-default, light theme | +| Full-stack TanStack Start | SSR routing, server functions, server-side auth/session, Drizzle DB, forms (TanStack Form), monitoring | +| New: Carrier/plugin Editor | Agent-first 3-pane IDE; scaffold + edit connectors via `@karrio/app-store`/SDK | +| New: MCP management | Start/stop server, tools table, install snippets, client + invocation monitoring (ref `packages/mcp`, `PRDs/KARRIO_MCP_SERVER.md`) | +| New: Agents | AI Assistant chat + agent sessions/runs, persisted in Studio DB | +| Self-editable app | Theme/accent/density/font + layout customization persisted per user/org | +| Full test coverage | Playwright spec per feature, run against live Karrio GraphQL+REST | +| Tracked in Linear | Project with epics/issues mirroring the agent plan below | + +## Existing Code Analysis + +| Asset | Location | Reuse plan | +|-------|----------|-----------| +| Karrio API client | `@karrio/types` `KarrioClient`, `packages/hooks/karrio.tsx` | Reuse client; replace NextAuth session with Studio server session | +| Data hooks (~50) | `packages/hooks/*` (shipment, tracker, order, pickup, carrier-connections, apps, webhooks, api-keys, admin-*, workflows…) | Reuse via a Studio `ClientProvider` + session adapter | +| Types | `@karrio/types` (`graphql/`, `rest/`, `base.ts`) | Reuse directly — never define inline | +| Lib utils | `@karrio/lib` (`KARRIO_API`, `url$`, `getCookie`, auth, autocomplete) | Reuse directly | +| App store / plugins | `packages/app-store`, `packages/mcp`, `plugins/` | Power Apps/Plugins/MCP screens + Editor scaffolding | +| MCP server | `packages/mcp`, `PRDs/KARRIO_MCP_SERVER.md`, `SPRINT_MCP.md` | Back the MCP management screen | +| Playwright harness | `packages/e2e` (config, `helpers/auth.ts`, `auth.setup.ts`) | Extend with a `studio` project + per-feature specs | +| Design handoff | `design_handoff_karrio_studio/prototype/studio/*` | Source of truth for tokens, IA, screen layouts, data shapes | + +**Key constraint:** `@karrio/hooks` files start with `"use client"` and read the +NextAuth session via `useSyncedSession`. Studio provides a compatible session +context so these hooks run unchanged inside client islands; SSR data + Studio-native +entities go through TanStack Start **server functions** + Drizzle. + +## Technical Design + +### Architecture overview + +``` +┌──────────────────────────────────────────────────────────────────────┐ +│ apps/studio (TanStack Start) │ +│ │ +│ ┌────────────┐ ┌──────────────────────────────────────────────┐ │ +│ │ Router │ │ App Shell (CSS grid: [sidebar] [main]) │ │ +│ │ (file-based │──▶│ Sidebar: workspace · Mode(Ship/Build/Govern) │ │ +│ │ routes, │ │ · nav · user footer │ │ +│ │ SSR) │ │ Topbar: ⌘K · test-mode · theme · workbench │ │ +│ └────────────┘ │ · app launcher · create │ │ +│ │ │ Page: routed screen (internal scroll) │ │ +│ │ └──────────────────────────────────────────────┘ │ +│ ▼ │ +│ ┌──────────────────────┐ ┌──────────────────────────────┐ │ +│ │ Server Functions │ │ Client islands │ │ +│ │ - auth/session (JWT) │ │ - @karrio/hooks (TanStack Q) │ │ +│ │ - Studio-native CRUD │ │ - forms (TanStack Form) │ │ +│ │ - agent/MCP/editor │ │ - sheets/overlays/palette │ │ +│ └─────────┬─────────────┘ └───────────────┬──────────────┘ │ +│ │ │ │ +└────────────┼───────────────────────────────────────┼─────────────────────┘ + │ │ + ┌─────────▼──────────┐ ┌──────────▼───────────────┐ + │ Studio DB (Drizzle) │ │ Karrio Backend │ + │ - app_config │ │ - GraphQL (/graphql) │ + │ - layouts/tweaks │ │ - REST (/v1/*) │ + │ - agent_sessions │ │ - auth (token_auth) │ + │ - agent_runs/msgs │ │ - MCP server (packages/mcp│ + │ - mcp_servers │ │ + Django) │ + │ - mcp_clients │ └────────────────────────────┘ + └────────────────────┘ +``` + +### Auth & session sequence + +``` +Browser Studio server fn Karrio GraphQL Studio DB + │ POST /login (email,pw) │ │ │ + ├─────────────────────────▶│ mutation token_auth│ │ + │ ├───────────────────▶│ │ + │ │◀── access+refresh ──┤ │ + │ │ set httpOnly cookie │ │ + │◀── 302 → /home ──────────┤ │ │ + │ (SSR pages read cookie → inject token into KarrioClient + hooks) │ +``` + +### Tech stack + +| Concern | Choice | +|---------|--------| +| Framework | TanStack Start (Vite + TanStack Router, SSR + server functions) | +| Data (shipping) | `@karrio/hooks` (TanStack Query) → Karrio GraphQL + REST | +| Data (Studio-native) | Drizzle ORM (Postgres prod / SQLite dev) via server functions | +| Forms | TanStack Form + Zod validation | +| Styling | CSS custom properties ported from `styles.css` (+ Tailwind optional, tokens-first) | +| Auth | Server functions → Karrio `token_auth`/refresh; httpOnly session cookie | +| Monitoring | Workbench overlay (logs/events/health/workers/tracing) wired to Karrio admin hooks + Sentry/PostHog (`instrumentation`) | +| Tests | Playwright (`packages/e2e`, new `studio` project) | + +## Modes, Screens & Feature Matrix + +Routing derives mode from route (deep links land in the right mode). Defaults: +Ship→`home`, Build→`apps`, Govern→`admin`. + +| Mode | Screen | Route | Karrio API | Prototype ref | +|------|--------|-------|-----------|---------------| +| Ship | Home | `home` | shipment/tracker stats | `screens-ops.jsx` HomeScreen | +| Ship | Shipments + Sheet | `shipments` | REST `/v1/shipments`, GraphQL | ShipmentsScreen, ShipmentSheet | +| Ship | Trackers + Sheet | `trackers` | `/v1/trackers` | TrackersScreen, TrackerSheet | +| Ship | Orders + Sheet | `orders` | GraphQL orders | OrdersScreen, OrderSheet | +| Ship | Pickups + Create | `pickups` | `/v1/pickups` | PickupsScreen, PickupSheet, CreatePickupSheet | +| Ship | Connections + Sheet | `connections` | carrier-connections | ConnectionsScreen, ConnectionSheet | +| Ship | Shipping rules + Sheet | `rules` | shipping-rules | ShippingRulesScreen, RuleSheet | +| Ship | Addresses + Sheet | `addresses` | address | AddressesScreen, AddressSheet | +| Ship | Parcels + Sheet | `parcels` | parcel | ParcelsScreen, ParcelSheet | +| Ship | Products + Sheet | `products` | product | ProductsScreen, ProductSheet | +| Ship | Documents + Editor | `documents` | document-template | DocumentsScreen, DocumentSheet | +| Build | Apps + Sheet | `apps` | app-store, apps hook | AppsScreen, AppSheet, AppLauncher | +| Build | Plugins + Sheet | `plugins` | plugins/registry | PluginsScreen, PluginSheet | +| Build | MCP | `mcp` | `packages/mcp` + Django | McpScreen | +| Build | Editor (agent IDE) | `editor` | Studio DB + SDK scaffold | EditorScreen | +| Build | Workbench overlay | (overlay) | admin/log/event/health/worker/tracing | workbench.jsx | +| Build | Webhooks | `webhooks` | webhook hook | screens-develop.jsx | +| Build | API keys | `apikeys` | api-keys/api-token | screens-develop.jsx | +| Govern | Admin overview | `admin` | admin-* hooks | AdminScreen | +| Govern | Tenants | `tenants` | admin-platform | AdminScreen | +| Govern | Team & roles | `team` | organization/admin-users | screens-platform.jsx | +| Govern | Security | `security` | admin/session | screens-platform.jsx | +| Govern | Audit log | `audit` | event/tracing | screens-platform.jsx | +| Govern | Settings | `settings` | workspace-config, user | SettingsScreen | +| Cross | Auth flow | `/login…` | token_auth, register, verify, 2FA, reset | `auth.jsx` | +| Cross | Command palette ⌘K | overlay | search hook | CommandPalette | +| Cross | Tweaks panel | overlay | Studio DB app_config | tweaks-panel.jsx | + +### Core reusable components (Studio UI layer) + +`Sheet` (right drawer sm/md/lg + fullscreen), `ActivityFeed` + `JsonView`, +`CommandPalette`, `AppLauncher`/`AppSheet`, `Toast`, `Sidebar`/`Topbar`, +`Icon`, `CarrierLogo`, `Field`. All detail/create/edit views build on `Sheet`. + +### Design tokens + +Ported verbatim from `styles.css`: accent `#8B5CF6`; radii xs2/sm3/md4/lg6/pill3; +dark-default + light theme palettes; status colors; density variants +(compact/regular/comfy); Inter / JetBrains Mono / IBM Plex Sans. Theme persists +(`localStorage` + Studio DB), applied pre-render to avoid flash. + +## New Features + +### Carrier integration & plugin Editor (`editor`) +Agent-first 3-pane IDE: **left** agent sessions, **center** Assistant chat (default) ++ closeable code tabs with inline AI diff (Apply/Reject), **right** plugin file tree. +Scaffolds connectors using the SDK extension pattern (`./bin/cli sdk add-extension`) +and `@karrio/app-store`, persisting sessions/runs/messages in Studio DB. + +### MCP management (`mcp`) +Server status (start/stop, URL, stats), exposed-tools table, install snippets +(Claude Desktop/Cursor JSON + SSE URL), connected clients, recent invocations — +backed by `packages/mcp` and the MCP server PRD. + +### AI Assistant & Agents +Assistant chat surface (Editor + ⌘K actions) backed by the Claude API; agent +sessions/runs/messages persisted in Studio DB; @-context chips reference Studio +entities. Tool execution flows through the Karrio MCP server. + +### Self-editable / customizable app +Tweaks panel (accent/font/density/theme) + layout customization persisted per +user/org in Studio DB; applied via CSS custom properties. + +## Agent / Subagent Orchestration Plan (Linear) + +The build is decomposed into **orchestrator agents** (epics) and **subagents** +(issues). This maps 1:1 to the Linear project "Karrio Studio". + +``` +Karrio Studio (Linear Project) +├─ EPIC A · Foundation Agent +│ ├─ A1 scaffold apps/studio (TanStack Start, Vite, SSR) +│ ├─ A2 design tokens + global CSS (port styles.css) +│ ├─ A3 app shell: Sidebar + Topbar + mode IA + routing +│ ├─ A4 core components: Sheet, ActivityFeed, JsonView, Toast, Field, Icon, CarrierLogo +│ ├─ A5 Studio DB (Drizzle) schema + migrations +│ └─ A6 @karrio/hooks session adapter + ClientProvider +├─ EPIC B · Auth Agent +│ ├─ B1 server-fn auth (token_auth/refresh) + httpOnly session +│ ├─ B2 auth screens (sign in/up, verify, 2FA, forgot/reset, invite, change pw) +│ └─ B3 route guards + org/test-mode context +├─ EPIC C · Ship Agent +│ ├─ C1 Home ├─ C2 Shipments+Sheet ├─ C3 Trackers+Sheet ├─ C4 Orders+Sheet +│ ├─ C5 Pickups+Create ├─ C6 Connections ├─ C7 Shipping rules +│ ├─ C8 Addresses ├─ C9 Parcels ├─ C10 Products └─ C11 Documents+template editor +├─ EPIC D · Build Agent +│ ├─ D1 Apps + AppLauncher/AppSheet ├─ D2 Plugins ├─ D3 MCP management +│ ├─ D4 Editor (agent IDE) ├─ D5 Workbench overlay ├─ D6 Webhooks └─ D7 API keys +├─ EPIC E · Govern Agent +│ ├─ E1 Admin overview ├─ E2 Tenants ├─ E3 Team & roles +│ ├─ E4 Security ├─ E5 Audit log └─ E6 Settings +├─ EPIC F · Agents & MCP Agent (net-new) +│ ├─ F1 Assistant chat + Claude API ├─ F2 agent sessions/runs persistence +│ ├─ F3 MCP tool execution wiring └─ F4 carrier-integration scaffolding flow +├─ EPIC G · Customization Agent +│ ├─ G1 Tweaks panel ├─ G2 layout customization └─ G3 per-user/org persistence +├─ EPIC H · Cross-cutting Agent +│ ├─ H1 Command palette ⌘K ├─ H2 keyboard shortcuts ├─ H3 quick-create +│ └─ H4 monitoring (Sentry/PostHog) + health +└─ EPIC I · QA / Playwright Agent + ├─ I1 studio Playwright project + auth setup + ├─ I2 per-feature specs (one per C/D/E screen) + ├─ I3 GraphQL+REST integration fixtures + └─ I4 CI wiring +``` + +Each subagent issue carries: scope, prototype ref, Karrio API/hook, acceptance +criteria, and a paired Playwright spec (EPIC I). + +## Implementation Plan + +| Phase | Agent(s) | Deliverable | +|-------|----------|-------------| +| 0 (this session) | Foundation A1–A4 (scaffold), I1 | `apps/studio` boots, shell + tokens, Playwright `studio` project | +| 1 | A5–A6, B | Studio DB, hooks adapter, full auth | +| 2 | C | Ship mode parity, each screen + spec | +| 3 | D | Build mode incl. MCP + Editor + Workbench | +| 4 | E, G, H | Govern, customization, cross-cutting | +| 5 | F | Agents & MCP execution, carrier scaffolding | +| 6 | I, cutover | Full Playwright suite green; deprecate dashboard | + +## Testing Strategy (Playwright) + +- Extend `packages/e2e`: add a **`studio`** Playwright project (baseURL + `KARRIO_STUDIO_URL`, default `http://localhost:3003`) with its own `auth.setup.ts` + (Studio server-session login) producing `playwright/.auth/studio.json`. +- **One spec per feature** (matrix above): list/table render, filters/tabs, row → + Sheet open, create/edit/delete via Sheet forms, and the net-new MCP/Editor/Agent + flows. Each asserts against **live Karrio GraphQL + REST** responses (seeded test + org), not mocks. +- Follow repo Playwright conventions (role-based locators, `networkidle`, auth state + reuse). `unittest`/`karrio test` remain for SDK/Django; Playwright covers Studio UI. +- CI: run `studio` project after `setup`; fixtures seed shipments/trackers/orders. + +## Risk Assessment + +| Risk | Mitigation | +|------|-----------| +| `@karrio/hooks` NextAuth coupling | Session adapter providing the same context shape under TanStack Start | +| `@karrio/ui` aesthetic mismatch | New Studio token layer; reuse types/lib only, not bulma UI | +| Scope (huge surface) | Phased per-agent delivery tracked in Linear; dashboard stays live | +| TanStack Start maturity | Pin versions; isolate SSR/server-fn boundaries; lean on TanStack Query for data | +| Agent/MCP security | Tool execution via MCP server with org scoping; no secrets in client | + +## Migration & Rollback + +Studio ships alongside the dashboard. Cutover is per-mode behind a feature flag; +rollback = route users back to `apps/dashboard`. No destructive backend changes — +Studio-native state is additive (Drizzle DB), shipping data untouched. diff --git a/apps/studio/.env.sample b/apps/studio/.env.sample new file mode 100644 index 000000000..9e3bc4831 --- /dev/null +++ b/apps/studio/.env.sample @@ -0,0 +1,12 @@ +# Karrio backend (GraphQL + REST) — reused by @karrio/hooks +KARRIO_API=http://localhost:5002 +NEXT_PUBLIC_KARRIO_PUBLIC_URL=http://localhost:5002 + +# Studio server session (httpOnly cookie signing) +STUDIO_SESSION_SECRET=change-me-in-production + +# Studio-native database (Postgres in prod, local for dev) +DATABASE_URL=postgres://karrio:karrio@localhost:5432/karrio_studio + +# Optional: AI Assistant / agents (Claude API) +ANTHROPIC_API_KEY= diff --git a/apps/studio/.gitignore b/apps/studio/.gitignore new file mode 100644 index 000000000..f9b297abd --- /dev/null +++ b/apps/studio/.gitignore @@ -0,0 +1,10 @@ +node_modules +.output +.nitro +.tanstack +dist +.env +.env.local +# Generated by the TanStack Start / Router plugin +src/routeTree.gen.ts +src/db/migrations diff --git a/apps/studio/README.md b/apps/studio/README.md new file mode 100644 index 000000000..9eeeeff05 --- /dev/null +++ b/apps/studio/README.md @@ -0,0 +1,73 @@ +# Karrio Studio (`@karrio/studio`) + +Full-stack **TanStack Start** app that replaces the Next.js dashboard +(`apps/dashboard`) with a modular, agent-friendly "studio" — Ship / Build / +Govern modes, a self-editable UI, and net-new carrier-integration, Editor, +Agent, and MCP-management surfaces. + +See [`PRDs/KARRIO_STUDIO.md`](../../PRDs/KARRIO_STUDIO.md) for the full design, +the agent/subagent plan, and the testing strategy. Work is tracked in the +**"Karrio Studio"** Linear project (epics = orchestrator agents, issues = +subagents). + +## Status — Phase 0 (foundation) + +| Area | State | +|------|-------| +| TanStack Start app (Vite, SSR, file routes) | ✅ scaffolded | +| Design tokens + global CSS (ported from handoff) | ✅ `src/styles/tokens.css` | +| App shell: Sidebar + Topbar + mode IA + routing | ✅ | +| Core components: `Sheet`, `Field`, `Icon`, `CarrierLogo` | ✅ (more in later phases) | +| Every IA route navigable (Placeholder + real Home) | ✅ `src/screens/` | +| Studio-native DB schema (Drizzle) | ✅ `src/db/schema.ts` | +| Server-side auth skeleton (Karrio `token_auth`) | ✅ `src/server/auth.ts` | +| Playwright `studio` project + smoke specs | ✅ `packages/e2e/tests/studio/` | +| Feature screens, auth UI, agents/MCP/editor, monitoring | ⏳ tracked in Linear | + +## Architecture + +- **Shipping data** → reused `@karrio/hooks` (TanStack Query) over Karrio + GraphQL + REST. No data is duplicated. +- **Studio-native state** (app config/tweaks, agent sessions, MCP config) → + Drizzle DB via TanStack Start server functions. +- **Auth** → server functions proxy Karrio JWT auth into an httpOnly session + cookie used by SSR + the Karrio client. + +## Develop + +```bash +cp .env.sample .env # set KARRIO_API + DATABASE_URL +npm install # from repo root (workspaces) +npm run dev -w @karrio/studio # → http://localhost:3003 (generates routeTree.gen.ts) +``` + +## Test + +```bash +# with the studio dev server running on :3003 +cd packages/e2e +KARRIO_STUDIO_URL=http://localhost:3003 npx playwright test --project=studio +``` + +## Layout + +``` +apps/studio/ +├── vite.config.ts # TanStack Start plugin +├── drizzle.config.ts +├── src/ +│ ├── router.tsx # createRouter (routeTree.gen.ts is generated) +│ ├── routes/ +│ │ ├── __root.tsx # document, fonts, theme-init, QueryClient +│ │ ├── index.tsx # / → /home +│ │ ├── _app.tsx # shell layout (sidebar + topbar + shortcuts) +│ │ └── _app.$screen.tsx # screen dispatch (validates IA, 404s unknown) +│ ├── screens/ # registry + Home + Placeholder +│ ├── components/ +│ │ ├── shell/ # Sidebar, Topbar +│ │ └── ui/ # Sheet, Field, icons, CarrierLogo +│ ├── lib/ # modes (IA), theme +│ ├── db/ # Drizzle schema + client +│ ├── server/ # auth server functions +│ └── styles/tokens.css # ported design tokens + shell/sheet CSS +``` diff --git a/apps/studio/drizzle.config.ts b/apps/studio/drizzle.config.ts new file mode 100644 index 000000000..75fac0a64 --- /dev/null +++ b/apps/studio/drizzle.config.ts @@ -0,0 +1,10 @@ +import { defineConfig } from "drizzle-kit"; + +export default defineConfig({ + schema: "./src/db/schema.ts", + out: "./src/db/migrations", + dialect: "postgresql", + dbCredentials: { + url: process.env.DATABASE_URL ?? "postgres://localhost:5432/karrio_studio", + }, +}); diff --git a/apps/studio/package.json b/apps/studio/package.json new file mode 100644 index 000000000..f4b802a66 --- /dev/null +++ b/apps/studio/package.json @@ -0,0 +1,41 @@ +{ + "name": "@karrio/studio", + "version": "1.0.0", + "private": true, + "type": "module", + "scripts": { + "dev": "vite dev --port 3003", + "build": "vite build", + "start": "node .output/server/index.mjs", + "lint": "eslint .", + "db:generate": "drizzle-kit generate", + "db:migrate": "drizzle-kit migrate", + "db:push": "drizzle-kit push" + }, + "dependencies": { + "@karrio/lib": "*", + "@karrio/types": "*", + "@tanstack/react-form": "^0.41.0", + "@tanstack/react-query": "^5.59.0", + "@tanstack/react-router": "^1.95.0", + "@tanstack/react-start": "^1.95.0", + "drizzle-orm": "^0.36.0", + "postgres": "^3.4.5", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "zod": "^3.23.8" + }, + "devDependencies": { + "@tanstack/router-plugin": "^1.95.0", + "@types/react": "^18.3.12", + "@types/react-dom": "^18.3.1", + "@vitejs/plugin-react": "^4.3.4", + "drizzle-kit": "^0.28.0", + "eslint": "^8.48.0", + "eslint-config-custom": "*", + "tsconfig": "*", + "typescript": "^5.6.3", + "vite": "^6.0.0", + "vite-tsconfig-paths": "^5.1.0" + } +} diff --git a/apps/studio/src/components/shell/Sidebar.tsx b/apps/studio/src/components/shell/Sidebar.tsx new file mode 100644 index 000000000..acb8435d5 --- /dev/null +++ b/apps/studio/src/components/shell/Sidebar.tsx @@ -0,0 +1,97 @@ +// Sidebar.tsx — mode-driven navigation (Ship / Build / Govern). +import { Icon, MODE_LABELS, NAV, type Mode } from "~/lib/modes"; + +const MODES: Mode[] = ["ship", "build", "govern"]; + +export function Sidebar({ + route, + mode, + collapsed, + onGo, + onMode, +}: { + route: string; + mode: Mode; + collapsed: boolean; + onGo: (route: string) => void; + onMode: (mode: Mode) => void; +}) { + return ( + + ); +} diff --git a/apps/studio/src/components/shell/Topbar.tsx b/apps/studio/src/components/shell/Topbar.tsx new file mode 100644 index 000000000..8703399e1 --- /dev/null +++ b/apps/studio/src/components/shell/Topbar.tsx @@ -0,0 +1,128 @@ +// Topbar.tsx — search trigger, test-mode, theme, workbench, app launcher, create. +import { useEffect, useRef, useState } from "react"; +import { Icon } from "~/components/ui/icons"; +import type { Mode } from "~/lib/modes"; +import type { Theme } from "~/lib/theme"; + +export type CreateKind = + | "shipment" + | "tracker" + | "order" + | "pickup" + | "plugin" + | "apikey"; + +export function Topbar({ + mode, + route, + theme, + testMode, + onToggleSidebar, + onPalette, + onTestMode, + onTheme, + onOpenWorkbench, + onCreate, +}: { + mode: Mode; + route: string; + theme: Theme; + testMode: boolean; + onToggleSidebar: () => void; + onPalette: () => void; + onTestMode: (on: boolean) => void; + onTheme: () => void; + onOpenWorkbench: () => void; + onCreate: (kind: CreateKind) => void; +}) { + const [createOpen, setCreateOpen] = useState(false); + const createRef = useRef(null); + + useEffect(() => { + const onClick = (e: MouseEvent) => { + if (createRef.current && !createRef.current.contains(e.target as Node)) { + setCreateOpen(false); + } + }; + document.addEventListener("mousedown", onClick); + return () => document.removeEventListener("mousedown", onClick); + }, []); + + const showLauncher = (mode === "ship" || mode === "build") && route !== "apps"; + + return ( +
+ + +
+ + + + {showLauncher && ( + + )} +
+ + {createOpen && ( +
+ } label="New shipment" kbd="⌘L" onClick={() => fire("shipment")} /> + } label="Track a shipment" kbd="⌘T" onClick={() => fire("tracker")} /> + } label="New order" onClick={() => fire("order")} /> + } label="Schedule a pickup" onClick={() => fire("pickup")} /> +
+ } label="Install a plugin" onClick={() => fire("plugin")} /> + } label="Generate API key" onClick={() => fire("apikey")} /> +
+ )} +
+
+
+ ); + + function fire(kind: CreateKind) { + setCreateOpen(false); + onCreate(kind); + } +} + +function MenuItem({ + icon, + label, + kbd, + onClick, +}: { + icon: React.ReactNode; + label: string; + kbd?: string; + onClick: () => void; +}) { + return ( +
+ {icon} + {label} + {kbd && {kbd}} +
+ ); +} diff --git a/apps/studio/src/components/ui/CarrierLogo.tsx b/apps/studio/src/components/ui/CarrierLogo.tsx new file mode 100644 index 000000000..313506f23 --- /dev/null +++ b/apps/studio/src/components/ui/CarrierLogo.tsx @@ -0,0 +1,39 @@ +// CarrierLogo.tsx — square carrier badge (ported from the design handoff) + +export const CARRIERS: Record = { + ups: { name: "UPS", bg: "#351B0E", fg: "#FFB81C", abbr: "UPS" }, + fedex: { name: "FedEx", bg: "#4D148C", fg: "#FF6600", abbr: "FDX" }, + dhl: { name: "DHL", bg: "#FFCC00", fg: "#D40511", abbr: "DHL" }, + usps: { name: "USPS", bg: "#004B87", fg: "#fff", abbr: "USPS" }, + canpost: { name: "Canada Post", bg: "#EE2D24", fg: "#fff", abbr: "CP" }, + purolator: { name: "Purolator", bg: "#1C3F94", fg: "#fff", abbr: "PUR" }, + royalmail: { name: "Royal Mail", bg: "#E60000", fg: "#fff", abbr: "RM" }, + landmark: { name: "Landmark", bg: "#E0153A", fg: "#fff", abbr: "LM" }, + smartkargo: { name: "SmartKargo", bg: "#FFFFFF", fg: "#1A66B5", abbr: "SK" }, + dpd: { name: "DPD", bg: "#DC0032", fg: "#fff", abbr: "DPD" }, + australia: { name: "Australia Post", bg: "#D70000", fg: "#fff", abbr: "AUS" }, + tnt: { name: "TNT", bg: "#FF6600", fg: "#fff", abbr: "TNT" }, + aramex: { name: "Aramex", bg: "#E10A17", fg: "#fff", abbr: "ARX" }, +}; + +export function CarrierLogo({ + carrier, + size = "md", +}: { + carrier: string; + size?: "sm" | "md" | "lg"; +}) { + const meta = CARRIERS[carrier] || { bg: "#52525B", fg: "#fff", abbr: "?" }; + return ( +
+ {meta.abbr} +
+ ); +} diff --git a/apps/studio/src/components/ui/Sheet.tsx b/apps/studio/src/components/ui/Sheet.tsx new file mode 100644 index 000000000..2c54f0430 --- /dev/null +++ b/apps/studio/src/components/ui/Sheet.tsx @@ -0,0 +1,100 @@ +// Sheet.tsx — the workhorse right drawer. Build ALL detail/create/edit views +// on this. Supports sm/md/lg widths, fullscreen expand, ESC/backdrop close. +import { useEffect, type ReactNode } from "react"; +import { Icon } from "~/components/ui/icons"; + +export type SheetProps = { + open: boolean; + onClose: () => void; + size?: "sm" | "md" | "lg"; + fullscreen?: boolean; + onToggleFullscreen?: () => void; + crumb?: string; + title?: ReactNode; + id?: string; + headRight?: ReactNode; + footer?: ReactNode; + children?: ReactNode; +}; + +export function Sheet({ + open, + onClose, + size, + fullscreen, + onToggleFullscreen, + crumb, + title, + id, + headRight, + footer, + children, +}: SheetProps) { + useEffect(() => { + if (!open) return; + const onKey = (e: KeyboardEvent) => { + if (e.key === "Escape") onClose(); + }; + window.addEventListener("keydown", onKey); + return () => window.removeEventListener("keydown", onKey); + }, [open, onClose]); + + return ( + <> +
+ + + ); +} + +export function Field({ + label, + children, +}: { + label: string; + children: ReactNode; +}) { + return ( + + ); +} diff --git a/apps/studio/src/components/ui/icons.tsx b/apps/studio/src/components/ui/icons.tsx new file mode 100644 index 000000000..36f6c52fb --- /dev/null +++ b/apps/studio/src/components/ui/icons.tsx @@ -0,0 +1,110 @@ +// icons.tsx — Karrio Studio stroke icon set (ported from the design handoff) +import type { ReactNode, CSSProperties } from "react"; + +type IconProps = { + size?: number; + stroke?: number; + fill?: string; + style?: CSSProperties; + className?: string; +}; + +const Ic = ({ + d, + size = 16, + stroke = 1.6, + fill = "none", + style, + className, +}: IconProps & { d: ReactNode }) => ( + + {typeof d === "string" ? : d} + +); + +export const Icon = { + Home: (p: IconProps) => , + Box: (p: IconProps) => ( + } /> + ), + Truck: (p: IconProps) => ( + } /> + ), + Pin: (p: IconProps) => ( + } /> + ), + Inbox: (p: IconProps) => ( + } /> + ), + Plug: (p: IconProps) => ( + } /> + ), + Grid: (p: IconProps) => ( + } /> + ), + Code: (p: IconProps) => ( + } /> + ), + Shield: (p: IconProps) => , + Settings: (p: IconProps) => ( + } /> + ), + Search: (p: IconProps) => } />, + Plus: (p: IconProps) => , + ChevronD: (p: IconProps) => , + ChevronR: (p: IconProps) => , + X: (p: IconProps) => , + Check: (p: IconProps) => , + Filter: (p: IconProps) => , + Refresh: (p: IconProps) => ( + } /> + ), + Doc: (p: IconProps) => ( + } /> + ), + Tag: (p: IconProps) => ( + } /> + ), + User: (p: IconProps) => ( + } /> + ), + Activity: (p: IconProps) => , + Lock: (p: IconProps) => ( + } /> + ), + Webhook: (p: IconProps) => ( + } /> + ), + Key: (p: IconProps) => ( + } /> + ), + Workspace: (p: IconProps) => ( + } /> + ), + Sun: (p: IconProps) => ( + } /> + ), + Moon: (p: IconProps) => , + Terminal: (p: IconProps) => ( + } /> + ), + Sidebar: (p: IconProps) => ( + } /> + ), + Pkg: (p: IconProps) => ( + } /> + ), +}; + +export type IconName = keyof typeof Icon; diff --git a/apps/studio/src/db/index.ts b/apps/studio/src/db/index.ts new file mode 100644 index 000000000..b7ca2417a --- /dev/null +++ b/apps/studio/src/db/index.ts @@ -0,0 +1,23 @@ +// db/index.ts — Drizzle client for Studio-native state. Server-only. +import { drizzle } from "drizzle-orm/postgres-js"; +import postgres from "postgres"; +import * as schema from "~/db/schema"; + +const connectionString = process.env.DATABASE_URL; + +// Lazily create the client so the app can boot (and tests can run UI-only) +// without a database configured. Server functions that need the DB call db(). +let _client: ReturnType> | null = null; + +export function db() { + if (!connectionString) { + throw new Error("DATABASE_URL is not set — Studio-native DB unavailable."); + } + if (!_client) { + const sql = postgres(connectionString, { max: 5 }); + _client = drizzle(sql, { schema }); + } + return _client; +} + +export { schema }; diff --git a/apps/studio/src/db/schema.ts b/apps/studio/src/db/schema.ts new file mode 100644 index 000000000..7d200e222 --- /dev/null +++ b/apps/studio/src/db/schema.ts @@ -0,0 +1,107 @@ +// schema.ts — Studio-native state ONLY. All shipping data (shipments, trackers, +// orders, carriers, etc.) lives in the Karrio backend and is read/written via +// @karrio/hooks. This DB holds app customization, agents, and MCP config. +import { sql } from "drizzle-orm"; +import { + integer, + jsonb, + pgTable, + text, + timestamp, + uuid, + varchar, +} from "drizzle-orm/pg-core"; + +const id = () => uuid("id").primaryKey().defaultRandom(); +const timestamps = { + created_at: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), + updated_at: timestamp("updated_at", { withTimezone: true }).defaultNow().notNull(), +}; + +// Self-editable app: per-user/org theme, accent, density, font, layout tweaks. +export const appConfig = pgTable("app_config", { + id: id(), + org_id: varchar("org_id", { length: 64 }).notNull(), + user_id: varchar("user_id", { length: 64 }), + theme: varchar("theme", { length: 16 }).default("dark").notNull(), + accent: varchar("accent", { length: 16 }).default("#8B5CF6").notNull(), + density: varchar("density", { length: 16 }).default("regular").notNull(), + font_stack: varchar("font_stack", { length: 32 }).default("Inter").notNull(), + layout: jsonb("layout"), + ...timestamps, +}); + +// AI Assistant / agent sessions (Editor + ⌘K actions). +export const agentSessions = pgTable("agent_sessions", { + id: id(), + org_id: varchar("org_id", { length: 64 }).notNull(), + user_id: varchar("user_id", { length: 64 }).notNull(), + title: text("title").notNull(), + plugin: varchar("plugin", { length: 128 }), + status: varchar("status", { length: 24 }).default("idle").notNull(), + ...timestamps, +}); + +export const agentRuns = pgTable("agent_runs", { + id: id(), + session_id: uuid("session_id") + .notNull() + .references(() => agentSessions.id, { onDelete: "cascade" }), + model: varchar("model", { length: 64 }).notNull(), + mode: varchar("mode", { length: 24 }).default("assistant").notNull(), + status: varchar("status", { length: 24 }).default("running").notNull(), + ...timestamps, +}); + +export const agentMessages = pgTable("agent_messages", { + id: id(), + session_id: uuid("session_id") + .notNull() + .references(() => agentSessions.id, { onDelete: "cascade" }), + run_id: uuid("run_id").references(() => agentRuns.id, { onDelete: "set null" }), + role: varchar("role", { length: 16 }).notNull(), // user | assistant | tool + content: text("content").notNull(), + context: jsonb("context"), // @-context chips, file refs, diffs + created_at: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), +}); + +// MCP server management (backed by packages/mcp + Django MCP server). +export const mcpServers = pgTable("mcp_servers", { + id: id(), + org_id: varchar("org_id", { length: 64 }).notNull(), + name: varchar("name", { length: 128 }).notNull(), + url: text("url"), + status: varchar("status", { length: 24 }).default("stopped").notNull(), + config: jsonb("config"), + ...timestamps, +}); + +export const mcpClients = pgTable("mcp_clients", { + id: id(), + server_id: uuid("server_id") + .notNull() + .references(() => mcpServers.id, { onDelete: "cascade" }), + name: varchar("name", { length: 128 }).notNull(), + kind: varchar("kind", { length: 32 }), // claude-desktop | cursor | sse + last_seen: timestamp("last_seen", { withTimezone: true }), + created_at: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(), +}); + +export const mcpInvocations = pgTable("mcp_invocations", { + id: id(), + server_id: uuid("server_id") + .notNull() + .references(() => mcpServers.id, { onDelete: "cascade" }), + tool: varchar("tool", { length: 128 }).notNull(), + client_id: uuid("client_id").references(() => mcpClients.id, { onDelete: "set null" }), + duration_ms: integer("duration_ms"), + status: varchar("status", { length: 24 }).default("ok").notNull(), + payload: jsonb("payload"), + created_at: timestamp("created_at", { withTimezone: true }) + .default(sql`now()`) + .notNull(), +}); + +export type AppConfig = typeof appConfig.$inferSelect; +export type AgentSession = typeof agentSessions.$inferSelect; +export type McpServer = typeof mcpServers.$inferSelect; diff --git a/apps/studio/src/router.tsx b/apps/studio/src/router.tsx new file mode 100644 index 000000000..3a79ddb7e --- /dev/null +++ b/apps/studio/src/router.tsx @@ -0,0 +1,17 @@ +import { createRouter as createTanStackRouter } from "@tanstack/react-router"; +import { routeTree } from "./routeTree.gen"; + +// routeTree.gen.ts is generated by the TanStack Start Vite plugin on dev/build. +export function createRouter() { + return createTanStackRouter({ + routeTree, + defaultPreload: "intent", + scrollRestoration: true, + }); +} + +declare module "@tanstack/react-router" { + interface Register { + router: ReturnType; + } +} diff --git a/apps/studio/src/routes/__root.tsx b/apps/studio/src/routes/__root.tsx new file mode 100644 index 000000000..f0eb17ca2 --- /dev/null +++ b/apps/studio/src/routes/__root.tsx @@ -0,0 +1,61 @@ +import { + HeadContent, + Outlet, + Scripts, + createRootRoute, +} from "@tanstack/react-router"; +import { QueryClient, QueryClientProvider } from "@tanstack/react-query"; +import { useState } from "react"; +import { THEME_INIT_SCRIPT } from "~/lib/theme"; +import tokensCss from "~/styles/tokens.css?url"; + +export const Route = createRootRoute({ + head: () => ({ + meta: [ + { charSet: "utf-8" }, + { name: "viewport", content: "width=device-width, initial-scale=1" }, + { title: "Karrio Studio" }, + ], + links: [ + { rel: "stylesheet", href: tokensCss }, + { + rel: "stylesheet", + href: "https://fonts.googleapis.com/css2?family=Inter:wght@400;450;500;600;700&family=JetBrains+Mono:wght@400;500&family=IBM+Plex+Sans:wght@400;500;600&display=swap", + }, + ], + }), + component: RootComponent, +}); + +function RootComponent() { + // One QueryClient per app instance; shipping data hooks (@karrio/hooks) use it. + const [queryClient] = useState( + () => + new QueryClient({ + defaultOptions: { queries: { staleTime: 30_000, retry: 1 } }, + }), + ); + return ( + + + + + + ); +} + +function RootDocument({ children }: { children: React.ReactNode }) { + return ( + + + {/* Apply theme before paint to avoid a flash of the wrong theme. */} +