From e67c9f6c5cc7f029c3501f3fc86fdf3541a8ef34 Mon Sep 17 00:00:00 2001 From: Abhinaysai Kamineni Date: Thu, 25 Jun 2026 12:04:22 -0400 Subject: [PATCH 1/4] Add UI reliability foundation: error boundary, API health, and deploy fix. Wrap the dashboard in an ErrorBoundary with recovery UI, poll /health for a global status banner, and keep Vercel builds frontend-only via .vercelignore. Connection save/clear now surfaces toast feedback with unified error copy. Co-authored-by: Cursor --- .vercelignore | 28 +++ docs/DEPLOY.md | 182 ++++++++++++++++++ frontend/middleware.ts | 77 ++++++++ frontend/src/App.tsx | 57 +++++- .../src/components/layout/WorkspaceBar.tsx | 82 +++++++- frontend/src/components/ui/ErrorBoundary.tsx | 48 +++++ frontend/src/hooks/useApiHealth.ts | 40 ++++ frontend/src/lib/errors.ts | 10 +- middleware.ts | 76 ++++++++ vercel.json | 13 ++ 10 files changed, 593 insertions(+), 20 deletions(-) create mode 100644 .vercelignore create mode 100644 docs/DEPLOY.md create mode 100644 frontend/middleware.ts create mode 100644 frontend/src/components/ui/ErrorBoundary.tsx create mode 100644 frontend/src/hooks/useApiHealth.ts create mode 100644 middleware.ts create mode 100644 vercel.json diff --git a/.vercelignore b/.vercelignore new file mode 100644 index 0000000..9cc6e26 --- /dev/null +++ b/.vercelignore @@ -0,0 +1,28 @@ +# Cortex backend — never deploy Python on Vercel (API runs on Railway/Render). +# Use leading / so frontend/scripts/ is NOT ignored. + +/api/ +/pipeline/ +/connectors/ +/extraction/ +/graph/ +/intelligence/ +/memory/ +/scoring/ +/shared/ +/mcp/ +/tests/ +/scripts/ +/.github/ +pyproject.toml +uv.lock +.python-version +docker-compose.yml +railway.toml +render.yaml +Makefile +CLAUDE.md +ARCHITECTURE.md +DECISIONS.md +MISTAKES.md +SESSIONS.md diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md new file mode 100644 index 0000000..bd596e3 --- /dev/null +++ b/docs/DEPLOY.md @@ -0,0 +1,182 @@ +# Deploying Cortex + +Cortex is a **multi-service stack** (Kafka, Neo4j, Redis, API, pipeline worker, dashboard). Vercel hosts the **dashboard only**; the API and worker run elsewhere. + +## Recommended split + +| Component | Host | Notes | +|-----------|------|-------| +| Dashboard | **Vercel** (`frontend/`) | Vite static + API rewrites | +| API | Railway / Render / Fly | `api/Dockerfile` | +| pipeline-worker | Same as API | `pipeline/Dockerfile` | +| Neo4j | Neo4j Aura | Bolt URI in env | +| Redis | Upstash | Query cache | +| Kafka | Upstash Kafka / Confluent Cloud | Event bus | + +## Vercel (frontend) + +**Do not deploy the Python API on Vercel.** If the build log shows `Using Python 3.12 from pyproject.toml` or installs `uv.lock`, Vercel is treating the repo as FastAPI — that bundle (~5 GB) exceeds Lambda limits. + +**Fix (pick one):** + +| Approach | Settings | +|----------|----------| +| **Recommended** | Project Settings → General → **Root Directory** → `frontend` → Save → Redeploy | +| **Repo root** | Keep Root Directory `.` — root `vercel.json` + `.vercelignore` build `frontend/dist` only (excludes Python/`api/` so the bundle stays under Vercel limits) | + +After changing Root Directory, clear the Framework Preset override if it still says **FastAPI** — it should be **Vite** or **Other**. + +**Project settings (Root Directory = `frontend`)** + +- Root Directory: `frontend` +- Framework: Vite +- Build: `npm run build` +- API proxy: `middleware.ts` reads `CORTEX_API_ORIGIN` at **runtime** (Vercel parses `vercel.json` before build — build-time rewrites cannot inject env vars) + +**Environment variables** + +| Variable | Purpose | +|----------|---------| +| `CORTEX_API_ORIGIN` | Public API URL — Edge Middleware proxies `/query`, `/health`, etc. server-side | +| ~~`VITE_API_URL`~~ | **Do not** point at `loca.lt` — causes 511 tunnel interstitial errors | + +**Do not** import the repo root with Framework Preset **FastAPI**. That installs the full `uv.lock` (~5 GB) and exceeds Lambda limits. + +```bash +cd frontend +npx vercel deploy --prod +``` + +Set `CORTEX_API_ORIGIN=https://your-api.example.com` in the Vercel project before deploy. + +## Local full stack + +```bash +make demo +open http://localhost:3000 # dashboard (nginx → API) +open http://localhost:8000/docs +``` + +## Dev preview (laptop + tunnel) + +For short-lived public demos while the API runs locally: + +```bash +cloudflared tunnel --url http://localhost:8000 +# Set CORTEX_API_ORIGIN to the trycloudflare.com URL on Vercel, redeploy frontend +``` + +Prefer **cloudflared** over localtunnel — localtunnel returns **511** for browser `fetch()` calls. + +## Dual-workspace testing + +See [DATA_SOURCES.md](./DATA_SOURCES.md) for `local-dev` vs `oss-*` workspaces. + +```bash +python scripts/seed_demo.py --workspace local-dev +python scripts/import_github_org.py --org tiangolo --repo fastapi --dry-run +make verify-dual +``` + +## Railway / Render (API v1) + +Deploy **only the API** — not Kafka or the pipeline worker. Pre-seed Neo4j with `scripts/seed_demo.py` so `/query` works without live ingestion. + +### Railway + +```bash +# Install CLI: https://docs.railway.com/guides/cli +railway login +railway init # link this repo +railway up # uses railway.toml → api/Dockerfile +``` + +**Required environment variables** (Railway → Variables): + +| Variable | Example | +|----------|---------| +| `NEO4J_URI` | `neo4j+s://xxxx.databases.neo4j.io` | +| `NEO4J_USER` | `neo4j` | +| `NEO4J_PASSWORD` | Aura password | +| `REDIS_URL` | `rediss://default:token@host.upstash.io:6379` | +| `CORTEX_API_KEYS` | `demo-readonly:authenticated` (optional abuse control) | +| `CORTEX_SEMANTIC_ENABLED` | `false` | +| `CORTEX_SEED_DEMO` | `true` **first deploy only** — then set `false` so restarts skip re-seed | + +Copy the public Railway URL (e.g. `https://cortex-api-production.up.railway.app`), set `CORTEX_API_ORIGIN` on Vercel, and redeploy the frontend. + +**Production startup:** `scripts/start_api_production.sh` runs migrations on every boot. Demo seed runs only when `CORTEX_SEED_DEMO=true`. + +### Render + +Use [render.yaml](../render.yaml) as a Blueprint, or create a **Web Service** with Docker runtime and `api/Dockerfile` as the Dockerfile path. Same env vars as Railway. + +### Seed production graph (one-time) + +**Option A — first Railway deploy:** set `CORTEX_SEED_DEMO=true` on the API service, deploy once, then set `CORTEX_SEED_DEMO=false`. + +**Option B — from laptop** with Neo4j credentials in `.env`: + +```bash +export NEO4J_URI=neo4j+s://... +export NEO4J_USER=neo4j +export NEO4J_PASSWORD=... +uv run python graph/migrate.py +uv run python scripts/seed_demo.py --workspace local-dev --scale small +uv run python scripts/import_github_graph.py --org tiangolo --repo fastapi --workspace oss-tiangolo-fastapi --limit 30 +make verify-dual-production +``` + +Direct graph import (no Kafka): [scripts/import_github_graph.py](../scripts/import_github_graph.py). + +### Neo4j Aura migration + +See [AURA_MIGRATION.md](./AURA_MIGRATION.md). Rotate passwords after any credential exposure — update Railway env vars only, never commit secrets. + +### Optional demo API key (abuse control) + +For public demos, use a read-only key so anonymous traffic cannot hammer write endpoints: + +```bash +CORTEX_API_KEYS=demo-readonly:authenticated +``` + +Dashboard users paste the key in **Connection** settings. Open `/query` and `/health` remain usable without a key when `CORTEX_API_KEYS` is unset. + +### Wire Vercel → API + +```bash +# Vercel project → Settings → Environment Variables +CORTEX_API_ORIGIN=https://your-api.railway.app + +cd frontend && npx vercel deploy --prod +``` + +Verify: + +```bash +curl -s https://your-vercel-app.vercel.app/health +curl -s -X POST https://your-vercel-app.vercel.app/query \ + -H "Content-Type: application/json" \ + -d '{"query":"Why CockroachDB?","workspace_id":"local-dev","limit":5}' +``` + +## Auth on preview + +```bash +CORTEX_API_KEYS=preview-key:admin;authenticated +CORTEX_DEMO_API_KEY=preview-key +``` + +`make demo` and `scripts/demo.sh` send `Authorization` when these are set. + +## Optional cloud webhook path (v2) + +Full ingestion uses Kafka + pipeline-worker locally. For cloud v1 without Kafka, use direct graph import: + +```bash +make import-oss-graph # real GitHub PRs → Neo4j +make seed-oss-fastapi # synthetic OSS demo fallback +``` + +Future v2: deploy pipeline-worker as a second Railway service + Upstash Kafka, or add `POST /webhooks/github` → synchronous extract/write for demo scale. See [LAUNCH.md](./LAUNCH.md). diff --git a/frontend/middleware.ts b/frontend/middleware.ts new file mode 100644 index 0000000..14f7bbb --- /dev/null +++ b/frontend/middleware.ts @@ -0,0 +1,77 @@ +/** + * Vercel Edge Middleware — proxy API routes to CORTEX_API_ORIGIN at runtime. + * + * Build-time vercel.json rewrites cannot read env vars on Vercel (config is + * parsed before the build step). Middleware reads CORTEX_API_ORIGIN on each + * request so production deploys pick up dashboard → API wiring without rebuilds. + */ + +export const config = { + matcher: [ + "/health", + "/metrics", + "/query", + "/inject", + "/remember", + "/docs", + "/openapi.json", + "/decisions/:path*", + "/contradictions/:path*", + "/gdpr/:path*", + "/webhooks/:path*", + ], +}; + +const API_PREFIXES = config.matcher.map((m) => m.replace("/:path*", "")); + +function isApiRoute(pathname: string): boolean { + return API_PREFIXES.some( + (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`), + ); +} + +export default async function middleware(request: Request): Promise { + const origin = String( + (globalThis as typeof globalThis & { process?: { env?: Record } }) + .process?.env?.CORTEX_API_ORIGIN ?? "", + ) + .trim() + .replace(/\/$/, ""); + if (!origin) { + return new Response( + JSON.stringify({ + detail: + "CORTEX_API_ORIGIN is not set on Vercel. Add your Railway/Render API URL in Project Settings.", + }), + { status: 503, headers: { "content-type": "application/json" } }, + ); + } + + const url = new URL(request.url); + if (!isApiRoute(url.pathname)) { + return new Response("Not found", { status: 404 }); + } + + const target = `${origin}${url.pathname}${url.search}`; + const headers = new Headers(request.headers); + headers.delete("host"); + + const init: RequestInit = { + method: request.method, + headers, + redirect: "manual", + }; + if (request.method !== "GET" && request.method !== "HEAD") { + init.body = request.body; + } + + try { + return await fetch(target, init); + } catch (error) { + const message = error instanceof Error ? error.message : "upstream fetch failed"; + return new Response( + JSON.stringify({ detail: `API unreachable at ${origin}: ${message}` }), + { status: 502, headers: { "content-type": "application/json" } }, + ); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index baf3c8f..5461cae 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,6 +1,7 @@ -import { Suspense, lazy, useState } from "react"; +import { Suspense, lazy } from "react"; import { AppProvider, useApp } from "./context/AppContext"; import { ToastProvider } from "./components/ui/Toast"; +import { ErrorBoundary } from "./components/ui/ErrorBoundary"; import { Sidebar } from "./components/layout/Sidebar"; import { MobileNav } from "./components/layout/MobileNav"; import { AssistantPanel } from "./components/assistant/AssistantPanel"; @@ -10,8 +11,9 @@ import { AskView } from "./views/AskView"; import { SkeletonStack } from "./components/ui/Skeleton"; import { apiBase } from "./api/client"; import { resolveApiKey } from "./lib/auth"; -import { hasCompletedOnboarding } from "./lib/onboarding"; import { BugReportSection } from "./components/layout/BugReportSection"; +import { useApiHealth } from "./hooks/useApiHealth"; +import { IconSpark } from "./components/ui/icons"; const ExploreView = lazy(() => import("./views/ExploreView").then((m) => ({ default: m.ExploreView })), @@ -57,25 +59,57 @@ function MainContent() { ); } +function ApiHealthBanner() { + const { status, refresh } = useApiHealth(); + if (status === "ok" || status === "checking") return null; + const label = + status === "degraded" + ? "API online but a dependency is degraded (Neo4j or Redis)." + : "Cannot reach the Cortex API. Check Connection settings or try again shortly."; + return ( +
+ {label} + +
+ ); +} + function TopbarActions() { const { apiKey, setAssistantOpen } = useApp(); + const { status } = useApiHealth(120_000); const secured = Boolean(resolveApiKey(apiKey)); return (
+ + API status: {status} + {secured ? "Secured" : "Open"} @@ -88,10 +122,8 @@ function TopbarActions() { } function AppChrome() { - const { setAssistantOpen } = useApp(); - const [showOnboarding, setShowOnboarding] = useState( - () => typeof window !== "undefined" && !hasCompletedOnboarding(), - ); + const { setAssistantOpen, setView, setWorkspaceId, showOnboarding, setShowOnboarding } = + useApp(); return (
@@ -102,8 +134,13 @@ function AppChrome() { setShowOnboarding(false)} onOpenCopilot={() => setAssistantOpen(true)} + onFinishAsk={(workspace, query) => { + setWorkspaceId(workspace); + setView("ask", { q: query }); + }} /> ) : null} +
@@ -119,7 +156,9 @@ function AppChrome() {
- + + +
diff --git a/frontend/src/components/layout/WorkspaceBar.tsx b/frontend/src/components/layout/WorkspaceBar.tsx index edb11fa..98d8742 100644 --- a/frontend/src/components/layout/WorkspaceBar.tsx +++ b/frontend/src/components/layout/WorkspaceBar.tsx @@ -1,16 +1,54 @@ import { useId, useState } from "react"; import { useApp } from "../../context/AppContext"; -import { hasApiKeyConfigured } from "../../api/client"; +import { fetchHealth, hasApiKeyConfigured } from "../../api/client"; +import { useToast } from "../ui/Toast"; +import { IconLock } from "../ui/icons"; const PRESETS = ["local-dev", "acme-demo"] as const; export function WorkspaceBar() { - const { workspaceId, setWorkspaceId, apiKey, setApiKey, saveApiKey } = useApp(); + const { workspaceId, setWorkspaceId, apiKey, setApiKey, saveApiKey, clearApiKey, replayOnboarding } = + useApp(); + const { showToast } = useToast(); const [expanded, setExpanded] = useState(false); const [showKey, setShowKey] = useState(false); + const [testing, setTesting] = useState(false); + const [testOk, setTestOk] = useState(null); const keyInputId = useId(); const secured = hasApiKeyConfigured(); + async function testConnection(): Promise { + setTesting(true); + setTestOk(null); + try { + const health = await fetchHealth(); + const ok = + health.status === "ok" && + health.dependencies?.neo4j === "ok" && + health.dependencies?.redis === "ok"; + setTestOk(ok); + showToast(ok ? "API connection successful" : "API reachable but dependencies degraded"); + } catch { + setTestOk(false); + showToast("Could not reach API — check Connection settings"); + } finally { + setTesting(false); + } + } + + function handleSave(): void { + saveApiKey(); + showToast(apiKey.trim() ? "API key saved" : "API key cleared — open mode"); + void testConnection(); + } + + function handleClear(): void { + setApiKey(""); + clearApiKey(); + setTestOk(null); + showToast("API key cleared"); + } + return (
@@ -24,7 +62,7 @@ export function WorkspaceBar() { className="input connection-bar__input" value={workspaceId} onChange={(e) => setWorkspaceId(e.target.value)} - placeholder="e.g. local-dev" + placeholder="e.g. acme-engineering" /> {PRESETS.map((w) => ( @@ -59,8 +97,8 @@ export function WorkspaceBar() { {expanded ? (

- When the API has CORTEX_API_KEYS set, add a key here. Keys stay in your - browser (localStorage) unless you set VITE_CORTEX_API_KEY at build time. + When the API requires authentication, add your key here. Keys stay in your browser + unless you configure VITE_CORTEX_API_KEY at build time.

+
+ +
{secured ? (

@@ -94,9 +148,19 @@ export function WorkspaceBar() {

) : (

- Open mode — no API key sent (fine for local make demo). + Open mode — no API key sent (public demo).

)} + {testOk === true ? ( +

+ API health check passed. +

+ ) : null} + {testOk === false ? ( +

+ API unreachable — verify URL and credentials. +

+ ) : null}
) : null}
diff --git a/frontend/src/components/ui/ErrorBoundary.tsx b/frontend/src/components/ui/ErrorBoundary.tsx new file mode 100644 index 0000000..c3f8865 --- /dev/null +++ b/frontend/src/components/ui/ErrorBoundary.tsx @@ -0,0 +1,48 @@ +import { Component, type ErrorInfo, type ReactNode } from "react"; +import { StateView } from "./StateView"; + +type Props = { + children: ReactNode; +}; + +type State = { + error: Error | null; +}; + +/** Catches render errors so the dashboard never shows a blank screen. */ +export class ErrorBoundary extends Component { + state: State = { error: null }; + + static getDerivedStateFromError(error: Error): State { + return { error }; + } + + componentDidCatch(error: Error, info: ErrorInfo): void { + console.error("Cortex UI error:", error, info.componentStack); + } + + private retry = (): void => { + this.setState({ error: null }); + }; + + render(): ReactNode { + if (this.state.error) { + return ( +
+ + Try again + + } + > + {this.state.error.message || "An unexpected error occurred in the dashboard."} + +
+ ); + } + return this.props.children; + } +} diff --git a/frontend/src/hooks/useApiHealth.ts b/frontend/src/hooks/useApiHealth.ts new file mode 100644 index 0000000..3b4c856 --- /dev/null +++ b/frontend/src/hooks/useApiHealth.ts @@ -0,0 +1,40 @@ +import { useCallback, useEffect, useState } from "react"; +import { fetchHealth } from "../api/client"; +import type { Health } from "../types"; + +export type ApiHealthStatus = "ok" | "degraded" | "unreachable" | "checking"; + +export function useApiHealth(pollMs = 60_000): { + status: ApiHealthStatus; + health: Health | null; + refresh: () => Promise; +} { + const [health, setHealth] = useState(null); + const [status, setStatus] = useState("checking"); + + const refresh = useCallback(async () => { + setStatus("checking"); + try { + const payload = await fetchHealth(); + setHealth(payload); + const neo4j = payload.dependencies?.neo4j; + const redis = payload.dependencies?.redis; + if (payload.status === "ok" && neo4j === "ok" && redis === "ok") { + setStatus("ok"); + } else { + setStatus("degraded"); + } + } catch { + setHealth(null); + setStatus("unreachable"); + } + }, []); + + useEffect(() => { + void refresh(); + const id = window.setInterval(() => void refresh(), pollMs); + return () => window.clearInterval(id); + }, [refresh, pollMs]); + + return { status, health, refresh }; +} diff --git a/frontend/src/lib/errors.ts b/frontend/src/lib/errors.ts index c6cc789..5b0e27c 100644 --- a/frontend/src/lib/errors.ts +++ b/frontend/src/lib/errors.ts @@ -2,8 +2,8 @@ export function apiUnreachableMessage(): string { return ( - "Cannot reach the Cortex API. Ensure Docker is running, then start the stack with " + - "`make demo` (API on port 8000). If you use the Vite dev server, it proxies to localhost:8000." + "Cannot reach the Cortex API. Check your Connection settings and confirm the API is running. " + + "For local development, start the stack and ensure the dashboard can reach port 8000." ); } @@ -45,6 +45,12 @@ export async function parseHttpError(response: Response): Promise { const base = "Service unavailable (503). Neo4j or another dependency may be down."; return detail ? `${base} ${detail.slice(0, 120)}` : base; } + if (response.status === 511 || detail.includes("Tunnel website ahead")) { + return ( + "API tunnel blocked the request (511). Use same-origin /query via Vercel middleware " + + "(CORTEX_API_ORIGIN), not a tunnel URL in VITE_API_URL." + ); + } if (!detail) return `Request failed (${status})`; return `Request failed (${status}): ${detail.slice(0, 200)}`; } diff --git a/middleware.ts b/middleware.ts new file mode 100644 index 0000000..0b65504 --- /dev/null +++ b/middleware.ts @@ -0,0 +1,76 @@ +/** + * Vercel Edge Middleware — proxy API routes to CORTEX_API_ORIGIN at runtime. + * + * Used when the Vercel project Root Directory is the repo root (`cortex` project). + * When Root Directory = `frontend`, see frontend/middleware.ts. + */ + +export const config = { + matcher: [ + "/health", + "/metrics", + "/query", + "/inject", + "/remember", + "/docs", + "/openapi.json", + "/decisions/:path*", + "/contradictions/:path*", + "/gdpr/:path*", + "/webhooks/:path*", + ], +}; + +const API_PREFIXES = config.matcher.map((m) => m.replace("/:path*", "")); + +function isApiRoute(pathname: string): boolean { + return API_PREFIXES.some( + (prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`), + ); +} + +export default async function middleware(request: Request): Promise { + const origin = String( + (globalThis as typeof globalThis & { process?: { env?: Record } }) + .process?.env?.CORTEX_API_ORIGIN ?? "", + ) + .trim() + .replace(/\/$/, ""); + if (!origin) { + return new Response( + JSON.stringify({ + detail: + "CORTEX_API_ORIGIN is not set on Vercel. Add your Railway/Render API URL in Project Settings.", + }), + { status: 503, headers: { "content-type": "application/json" } }, + ); + } + + const url = new URL(request.url); + if (!isApiRoute(url.pathname)) { + return new Response("Not found", { status: 404 }); + } + + const target = `${origin}${url.pathname}${url.search}`; + const headers = new Headers(request.headers); + headers.delete("host"); + + const init: RequestInit = { + method: request.method, + headers, + redirect: "manual", + }; + if (request.method !== "GET" && request.method !== "HEAD") { + init.body = request.body; + } + + try { + return await fetch(target, init); + } catch (error) { + const message = error instanceof Error ? error.message : "upstream fetch failed"; + return new Response( + JSON.stringify({ detail: `API unreachable at ${origin}: ${message}` }), + { status: 502, headers: { "content-type": "application/json" } }, + ); + } +} diff --git a/vercel.json b/vercel.json new file mode 100644 index 0000000..1c6078e --- /dev/null +++ b/vercel.json @@ -0,0 +1,13 @@ +{ + "$schema": "https://openapi.vercel.sh/vercel.json", + "framework": "vite", + "installCommand": "cd frontend && npm install", + "buildCommand": "cd frontend && npm run build", + "outputDirectory": "frontend/dist", + "rewrites": [ + { + "source": "/((?!assets/).*)", + "destination": "/index.html" + } + ] +} From 27a4c055f672d94d33fa12dbd5de8e64b9ba124d Mon Sep 17 00:00:00 2001 From: Abhinaysai Kamineni Date: Thu, 25 Jun 2026 12:22:23 -0400 Subject: [PATCH 2/4] Fix Vercel deploy: whitelist .vercelignore to exclude Python backend bundle. Vercel was detecting pyproject.toml after the Vite build and packaging ~5.4 GB of Python deps. Upload only frontend/ plus root deploy config so the bundle stays under the 500 MB ephemeral storage limit. Co-authored-by: Cursor --- .gitignore | 4 ++-- .vercelignore | 25 ++++++++++++------------- docs/DEPLOY.md | 2 +- 3 files changed, 15 insertions(+), 16 deletions(-) diff --git a/.gitignore b/.gitignore index 768ea40..4a1b295 100644 --- a/.gitignore +++ b/.gitignore @@ -22,8 +22,8 @@ htmlcov/ coverage.xml *.cover -# Environment -.env +# Vercel local project link (never commit credentials) +.vercel/ .env.* !.env.example diff --git a/.vercelignore b/.vercelignore index 9cc6e26..ad4aacb 100644 --- a/.vercelignore +++ b/.vercelignore @@ -1,6 +1,15 @@ -# Cortex backend — never deploy Python on Vercel (API runs on Railway/Render). -# Use leading / so frontend/scripts/ is NOT ignored. +# Cortex monorepo — Vercel hosts the Vite dashboard only. +# Whitelist: only frontend + root deploy config are uploaded (keeps bundle < 500 MB). +# Use leading paths where noted so frontend/scripts/ is never excluded by mistake. +* +!frontend +!frontend/** +!vercel.json +!middleware.ts +!.vercelignore + +# Belt-and-suspenders: never upload Python backend even if patterns change /api/ /pipeline/ /connectors/ @@ -12,17 +21,7 @@ /shared/ /mcp/ /tests/ -/scripts/ -/.github/ pyproject.toml uv.lock .python-version -docker-compose.yml -railway.toml -render.yaml -Makefile -CLAUDE.md -ARCHITECTURE.md -DECISIONS.md -MISTAKES.md -SESSIONS.md +*.py diff --git a/docs/DEPLOY.md b/docs/DEPLOY.md index bd596e3..360e8c1 100644 --- a/docs/DEPLOY.md +++ b/docs/DEPLOY.md @@ -22,7 +22,7 @@ Cortex is a **multi-service stack** (Kafka, Neo4j, Redis, API, pipeline worker, | Approach | Settings | |----------|----------| | **Recommended** | Project Settings → General → **Root Directory** → `frontend` → Save → Redeploy | -| **Repo root** | Keep Root Directory `.` — root `vercel.json` + `.vercelignore` build `frontend/dist` only (excludes Python/`api/` so the bundle stays under Vercel limits) | +| **Repo root** | Keep Root Directory `.` — root `vercel.json` + `.vercelignore` upload only `frontend/` (excludes Python/`api/` so the bundle stays under Vercel limits) | After changing Root Directory, clear the Framework Preset override if it still says **FastAPI** — it should be **Vite** or **Other**. From ef675d3702e747f1795d0207194c6e2ae193c5de Mon Sep 17 00:00:00 2001 From: Abhinaysai Kamineni Date: Thu, 25 Jun 2026 12:52:12 -0400 Subject: [PATCH 3/4] Add Render blueprint and production API startup for free-tier deploy. Prepares Plan A API hosting alongside reliability foundation; full runbook lands in PR #24 (DEPLOY-FREE.md). Co-authored-by: Cursor --- api/Dockerfile | 4 ++- render.yaml | 36 +++++++++++++++++++++++++ scripts/start_api_production.sh | 19 +++++++++++++ scripts/verify_free_deploy.sh | 47 +++++++++++++++++++++++++++++++++ 4 files changed, 105 insertions(+), 1 deletion(-) create mode 100644 render.yaml create mode 100755 scripts/start_api_production.sh create mode 100755 scripts/verify_free_deploy.sh diff --git a/api/Dockerfile b/api/Dockerfile index ad86aa8..6ccea4b 100644 --- a/api/Dockerfile +++ b/api/Dockerfile @@ -22,4 +22,6 @@ RUN pip install --no-cache-dir --upgrade pip \ EXPOSE 8000 -CMD ["uvicorn", "api.main:app", "--host", "0.0.0.0", "--port", "8000"] +RUN chmod +x scripts/start_api_production.sh + +CMD ["sh", "scripts/start_api_production.sh"] diff --git a/render.yaml b/render.yaml new file mode 100644 index 0000000..85d1b1e --- /dev/null +++ b/render.yaml @@ -0,0 +1,36 @@ +# Cortex API — Render Blueprint (Plan A free tier) +# https://render.com/docs/blueprint-spec +# +# Dashboard: Cloudflare Pages (frontend/) — see docs/DEPLOY-FREE.md +# Connect repo → New Blueprint → point at this file. +# Fill secret env vars in the Render dashboard after first deploy. + +services: + - type: web + name: cortex-api + runtime: docker + dockerfilePath: ./api/Dockerfile + dockerContext: . + plan: free + healthCheckPath: /health + envVars: + - key: ENVIRONMENT + value: production + - key: NEO4J_URI + sync: false + - key: NEO4J_USER + value: neo4j + - key: NEO4J_PASSWORD + sync: false + - key: REDIS_URL + sync: false + - key: CORS_ORIGINS + sync: false + - key: CORTEX_API_KEYS + sync: false + - key: CORTEX_SEED_DEMO + value: "true" + - key: CORTEX_SEMANTIC_ENABLED + value: "false" + - key: EXTRACTION_BACKEND + value: heuristic diff --git a/scripts/start_api_production.sh b/scripts/start_api_production.sh new file mode 100755 index 0000000..02f5748 --- /dev/null +++ b/scripts/start_api_production.sh @@ -0,0 +1,19 @@ +#!/bin/sh +# Production API entrypoint — migrate graph schema, optional one-time seed, then serve. +# +# CORTEX_SEED_DEMO defaults to false so redeploys do not re-seed. Set to "true" only +# on first boot or when intentionally resetting demo data. +set -eu + +echo "Running Neo4j migrations..." +python graph/migrate.py + +if [ "${CORTEX_SEED_DEMO:-false}" = "true" ]; then + echo "Seeding local-dev demo workspace (CORTEX_SEED_DEMO=true)..." + python scripts/seed_demo.py --workspace local-dev --scale small +else + echo "Skipping demo seed (CORTEX_SEED_DEMO is not true)." +fi + +echo "Starting uvicorn on port ${PORT:-8000}..." +exec uvicorn api.main:app --host 0.0.0.0 --port "${PORT:-8000}" diff --git a/scripts/verify_free_deploy.sh b/scripts/verify_free_deploy.sh new file mode 100755 index 0000000..8e12503 --- /dev/null +++ b/scripts/verify_free_deploy.sh @@ -0,0 +1,47 @@ +#!/usr/bin/env bash +# Smoke-test Plan A deploy: Render API + optional Cloudflare Pages dashboard. +set -euo pipefail + +API="" +PAGES="" + +usage() { + echo "Usage: $0 --api URL [--pages URL]" + echo " --api Render API base URL (required)" + echo " --pages Cloudflare Pages URL (optional HEAD check)" + exit 1 +} + +while [ $# -gt 0 ]; do + case "$1" in + --api) API="${2%/}"; shift 2 ;; + --pages) PAGES="${2%/}"; shift 2 ;; + -h|--help) usage ;; + *) echo "Unknown option: $1"; usage ;; + esac +done + +[ -n "$API" ] || usage + +echo "==> Health: $API/health" +health=$(curl -sf "$API/health") +echo "$health" | head -c 200 +echo "" + +echo "==> Query: local-dev" +result=$(curl -sf -X POST "$API/query" \ + -H "Content-Type: application/json" \ + -d '{"query":"Why CockroachDB?","workspace_id":"local-dev","limit":5}') +count=$(echo "$result" | python3 -c "import sys,json; print(json.load(sys.stdin).get('total',0))") +echo "total=$count" +if [ "$count" -lt 1 ]; then + echo "FAIL: expected at least 1 decision — seed Neo4j or set CORTEX_SEED_DEMO=true on Render" + exit 1 +fi + +if [ -n "$PAGES" ]; then + echo "==> Pages HEAD: $PAGES" + curl -sfI "$PAGES" | head -5 +fi + +echo "OK — Plan A API checks passed" From 694f269b73766cdfaf803596e2073e655ce0bbc6 Mon Sep 17 00:00:00 2001 From: Abhinaysai Kamineni Date: Thu, 25 Jun 2026 13:04:45 -0400 Subject: [PATCH 4/4] fix(frontend): scope PR18 to self-contained reliability deps PR #18 imported icons, onboarding context, and routing from later stacked PRs, breaking TypeScript CI. Restore local onboarding state and remove forward references so the reliability foundation builds alone. Co-authored-by: Cursor --- frontend/src/App.tsx | 16 +++++++--------- frontend/src/components/layout/WorkspaceBar.tsx | 14 ++++++-------- 2 files changed, 13 insertions(+), 17 deletions(-) diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index 5461cae..3bea1ef 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -1,4 +1,4 @@ -import { Suspense, lazy } from "react"; +import { Suspense, lazy, useState } from "react"; import { AppProvider, useApp } from "./context/AppContext"; import { ToastProvider } from "./components/ui/Toast"; import { ErrorBoundary } from "./components/ui/ErrorBoundary"; @@ -11,9 +11,9 @@ import { AskView } from "./views/AskView"; import { SkeletonStack } from "./components/ui/Skeleton"; import { apiBase } from "./api/client"; import { resolveApiKey } from "./lib/auth"; +import { hasCompletedOnboarding } from "./lib/onboarding"; import { BugReportSection } from "./components/layout/BugReportSection"; import { useApiHealth } from "./hooks/useApiHealth"; -import { IconSpark } from "./components/ui/icons"; const ExploreView = lazy(() => import("./views/ExploreView").then((m) => ({ default: m.ExploreView })), @@ -102,7 +102,7 @@ function TopbarActions() { onClick={() => setAssistantOpen(true)} aria-label="Open Cortex Assist" > - Assist + ✦ Assist typeof window !== "undefined" && !hasCompletedOnboarding(), + ); return (
@@ -134,10 +136,6 @@ function AppChrome() { setShowOnboarding(false)} onOpenCopilot={() => setAssistantOpen(true)} - onFinishAsk={(workspace, query) => { - setWorkspaceId(workspace); - setView("ask", { q: query }); - }} /> ) : null} diff --git a/frontend/src/components/layout/WorkspaceBar.tsx b/frontend/src/components/layout/WorkspaceBar.tsx index 98d8742..eaacd06 100644 --- a/frontend/src/components/layout/WorkspaceBar.tsx +++ b/frontend/src/components/layout/WorkspaceBar.tsx @@ -1,14 +1,14 @@ import { useId, useState } from "react"; import { useApp } from "../../context/AppContext"; import { fetchHealth, hasApiKeyConfigured } from "../../api/client"; +import { persistApiKey } from "../../lib/auth"; +import { setClientApiKey } from "../../api/client"; import { useToast } from "../ui/Toast"; -import { IconLock } from "../ui/icons"; const PRESETS = ["local-dev", "acme-demo"] as const; export function WorkspaceBar() { - const { workspaceId, setWorkspaceId, apiKey, setApiKey, saveApiKey, clearApiKey, replayOnboarding } = - useApp(); + const { workspaceId, setWorkspaceId, apiKey, setApiKey, saveApiKey } = useApp(); const { showToast } = useToast(); const [expanded, setExpanded] = useState(false); const [showKey, setShowKey] = useState(false); @@ -44,7 +44,8 @@ export function WorkspaceBar() { function handleClear(): void { setApiKey(""); - clearApiKey(); + persistApiKey(""); + setClientApiKey(""); setTestOk(null); showToast("API key cleared"); } @@ -88,7 +89,7 @@ export function WorkspaceBar() { className={`connection-bar__status ${secured ? "connection-bar__status--secured" : ""}`} aria-hidden > - {secured ? : "◇"} + {secured ? "🔒" : "◇"} Connection @@ -138,9 +139,6 @@ export function WorkspaceBar() { > {testing ? "Testing…" : "Test connection"} -
{secured ? (