diff --git a/.claude/agents/infra.md b/.claude/agents/infra.md index 8af63bb..1577d22 100644 --- a/.claude/agents/infra.md +++ b/.claude/agents/infra.md @@ -32,8 +32,7 @@ Manage deployment configuration, containerization, CI/CD pipelines, and local de - **docker-compose.yml:** - `app` — Rust backend (with hot reload via cargo-watch in dev) - - `postgres` — PostgreSQL 16 for development - - `postgres-test` — separate PostgreSQL instance for integration tests + - `postgres` — PostgreSQL 16 for development (tests reuse this server; `#[sqlx::test]` spins up per-test throwaway DBs) - Volume mounts for persistent data - Environment variables from `.env` diff --git a/.env.example b/.env.example index 714a363..676e6fb 100644 --- a/.env.example +++ b/.env.example @@ -1,6 +1,5 @@ # Database DATABASE_URL=postgres://planner:planner@localhost:5434/planner -DATABASE_TEST_URL=postgres://planner:planner@localhost:5433/planner_test # Auth JWT_SECRET=change-me-in-production diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 32f68c9..dffdb20 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,6 @@ on: env: CARGO_TERM_COLOR: always DATABASE_URL: postgres://planner:planner@localhost:5432/planner - DATABASE_TEST_URL: postgres://planner:planner@localhost:5432/planner_test SQLX_OFFLINE: "true" jobs: @@ -46,10 +45,6 @@ jobs: cache-on-failure: true cache-all-crates: true - - name: Create test database - run: | - PGPASSWORD=planner psql -h localhost -U planner -d planner -c "CREATE DATABASE planner_test;" - - name: Check formatting working-directory: backend run: cargo fmt -- --check @@ -76,7 +71,6 @@ jobs: run: cargo test env: DATABASE_URL: postgres://planner:planner@localhost:5432/planner - DATABASE_TEST_URL: postgres://planner:planner@localhost:5432/planner_test frontend: name: Frontend (React) diff --git a/Makefile b/Makefile index b50360b..f727e32 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -.PHONY: help install db-up db-down db-reset migrate prepare backend frontend dev test test-backend test-frontend lint build clean deploy +.PHONY: help install db-up db-down db-reset migrate prepare backend frontend dev test test-backend test-frontend lint build clean deploy check-backend check-frontend fullcheck help: @grep -E '^[a-zA-Z_-]+:.*?## .*$$' $(MAKEFILE_LIST) | awk 'BEGIN {FS = ":.*?## "}; {printf " \033[36m%-18s\033[0m %s\n", $$1, $$2}' @@ -53,6 +53,17 @@ lint: ## Run clippy (zero warnings) + prettier check cd backend && cargo clippy -- -D warnings cd backend && cargo fmt --check +check-backend: ## Mirror backend CI: fmt + clippy + sqlx-prepare + tests (requires `make db-up`) + cd backend && cargo fmt -- --check + cd backend && SQLX_OFFLINE=true cargo clippy -- -D warnings + cd backend && SQLX_OFFLINE=false cargo sqlx prepare --workspace --check -- --all-targets + cd backend && SQLX_OFFLINE=true cargo test + +check-frontend: ## Mirror frontend CI: prettier + tests + build + cd frontend && npm run check + +fullcheck: check-backend check-frontend ## Run backend + frontend CI checks locally (DB must be running) + build: ## Production build for both sides cd backend && cargo build --release cd frontend && npm run build diff --git a/docker-compose.yml b/docker-compose.yml index c6e2db5..4c750d8 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,19 +15,5 @@ services: timeout: 5s retries: 5 - postgres-test: - image: postgres:16-alpine - environment: - POSTGRES_USER: planner - POSTGRES_PASSWORD: planner - POSTGRES_DB: planner_test - ports: - - "5433:5432" - healthcheck: - test: ["CMD-SHELL", "pg_isready -U planner"] - interval: 5s - timeout: 5s - retries: 5 - volumes: pgdata: diff --git a/frontend/package.json b/frontend/package.json index b91b888..20b119d 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -9,7 +9,8 @@ "preview": "vite preview", "format": "prettier --write 'src/**/*.{js,jsx,ts,tsx,css,json}'", "test": "vitest", - "format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx,css,json}'" + "format:check": "prettier --check 'src/**/*.{js,jsx,ts,tsx,css,json}'", + "check": "npm run format:check && npm test -- --run && npm run build" }, "dependencies": { "@tailwindcss/vite": "^4.2.2", diff --git a/frontend/src/index.css b/frontend/src/index.css index 02c46ee..e081f53 100644 --- a/frontend/src/index.css +++ b/frontend/src/index.css @@ -1,5 +1,10 @@ @import "tailwindcss"; +body { + color: #eeedf5; + background-color: #08060f; +} + @theme { /* Surface depth tokens */ --color-base: #08060f; diff --git a/frontend/src/pages/admin/AdminDashboard.jsx b/frontend/src/pages/admin/AdminDashboard.jsx index ab55bf6..c7aee25 100644 --- a/frontend/src/pages/admin/AdminDashboard.jsx +++ b/frontend/src/pages/admin/AdminDashboard.jsx @@ -13,7 +13,7 @@ function sumCost(metrics) { function activeProvider(settings) { if (!settings) return "—"; - const entry = settings.find((s) => s.key === "active_provider"); + const entry = settings.find((s) => s.key === "llm_default_provider"); return entry ? entry.value : "—"; } diff --git a/frontend/src/pages/admin/AdminDashboard.test.jsx b/frontend/src/pages/admin/AdminDashboard.test.jsx index d252036..a7c803b 100644 --- a/frontend/src/pages/admin/AdminDashboard.test.jsx +++ b/frontend/src/pages/admin/AdminDashboard.test.jsx @@ -74,7 +74,7 @@ describe("AdminDashboard", () => { it("displays the active provider from mock settings", async () => { renderDashboard(); - // active_provider setting is "gemini" + // llm_default_provider setting is "gemini" expect(await screen.findByText("gemini")).toBeInTheDocument(); }); diff --git a/frontend/src/pages/admin/AdminProviders.jsx b/frontend/src/pages/admin/AdminProviders.jsx index ca5131a..2bf21ff 100644 --- a/frontend/src/pages/admin/AdminProviders.jsx +++ b/frontend/src/pages/admin/AdminProviders.jsx @@ -15,13 +15,29 @@ const LLM_SETTINGS = [ key: "llm_gemini_model", label: "Gemini Model", type: "text", - placeholder: "e.g. gemini-2.0-flash", + placeholder: "e.g. gemini-2.5-flash-preview-05-20", + suggestions: [ + "gemini-2.5-pro-preview-05-06", + "gemini-2.5-flash-preview-05-20", + "gemini-2.0-flash", + "gemini-2.0-flash-exp", + "gemini-1.5-pro", + "gemini-1.5-flash", + ], }, { key: "llm_claude_model", label: "Claude Model", type: "text", - placeholder: "e.g. claude-3-5-haiku-20241022", + placeholder: "e.g. claude-sonnet-4-20250514", + suggestions: [ + "claude-opus-4-7", + "claude-sonnet-4-6", + "claude-haiku-4-5-20251001", + "claude-sonnet-4-20250514", + "claude-3-5-sonnet-20241022", + "claude-3-5-haiku-20241022", + ], }, ]; @@ -132,8 +148,8 @@ export default function AdminProviders() { {/* Chat kill-switch */}

Chat Feature

@@ -143,8 +159,8 @@ export default function AdminProviders() { {/* LLM settings form */}

LLM Configuration

@@ -152,7 +168,7 @@ export default function AdminProviders() {
{LLM_SETTINGS.map( - ({ key, label, type, options, placeholder }) => ( + ({ key, label, type, options, placeholder, suggestions }) => (
), diff --git a/frontend/src/test/mocks/handlers.js b/frontend/src/test/mocks/handlers.js index 554d446..51638f6 100644 --- a/frontend/src/test/mocks/handlers.js +++ b/frontend/src/test/mocks/handlers.js @@ -30,7 +30,7 @@ let mockUserRole = "user"; let mockSettings = [ { - key: "active_provider", + key: "llm_default_provider", value: "gemini", updated_at: "2026-01-01T00:00:00Z", }, @@ -93,7 +93,7 @@ const mockAuditLog = [ actor_email: "admin@test.com", action: "setting.update", target_type: "setting", - target_id: "active_provider", + target_id: "llm_default_provider", payload: { old: "claude", new: "gemini" }, created_at: "2026-04-01T12:00:00Z", }, @@ -170,7 +170,7 @@ export function resetMockState() { mockUserRole = "user"; mockSettings = [ { - key: "active_provider", + key: "llm_default_provider", value: "gemini", updated_at: "2026-01-01T00:00:00Z", }, diff --git a/frontend/src/test/setup.js b/frontend/src/test/setup.js index 4894d9f..4e272a7 100644 --- a/frontend/src/test/setup.js +++ b/frontend/src/test/setup.js @@ -3,6 +3,37 @@ import { afterAll, afterEach, beforeAll } from "vitest"; import { server } from "./mocks/server"; import { resetMockState } from "./mocks/handlers"; +function createStorageShim() { + const store = new Map(); + return { + getItem: (key) => (store.has(key) ? store.get(key) : null), + setItem: (key, value) => { + store.set(String(key), String(value)); + }, + removeItem: (key) => { + store.delete(key); + }, + clear: () => { + store.clear(); + }, + key: (i) => Array.from(store.keys())[i] ?? null, + get length() { + return store.size; + }, + }; +} + +Object.defineProperty(window, "localStorage", { + value: createStorageShim(), + writable: true, + configurable: true, +}); +Object.defineProperty(window, "sessionStorage", { + value: createStorageShim(), + writable: true, + configurable: true, +}); + Object.defineProperty(window, "matchMedia", { writable: true, value: (query) => ({ @@ -23,6 +54,7 @@ afterEach(() => { server.resetHandlers(); resetMockState(); localStorage.clear(); + sessionStorage.clear(); }); afterAll(() => server.close());