Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
3 changes: 1 addition & 2 deletions .claude/agents/infra.md
Original file line number Diff line number Diff line change
Expand Up @@ -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`

Expand Down
1 change: 0 additions & 1 deletion .env.example
Original file line number Diff line number Diff line change
@@ -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
Expand Down
6 changes: 0 additions & 6 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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
Expand All @@ -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)
Expand Down
13 changes: 12 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
@@ -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}'
Expand Down Expand Up @@ -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
Expand Down
14 changes: 0 additions & 14 deletions docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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:
3 changes: 2 additions & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
5 changes: 5 additions & 0 deletions frontend/src/index.css
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
@import "tailwindcss";

body {
color: #eeedf5;
background-color: #08060f;
}

@theme {
/* Surface depth tokens */
--color-base: #08060f;
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/admin/AdminDashboard.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 : "—";
}

Expand Down
2 changes: 1 addition & 1 deletion frontend/src/pages/admin/AdminDashboard.test.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});

Expand Down
58 changes: 43 additions & 15 deletions frontend/src/pages/admin/AdminProviders.jsx
Original file line number Diff line number Diff line change
Expand Up @@ -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",
],
},
];

Expand Down Expand Up @@ -132,8 +148,8 @@ export default function AdminProviders() {
{/* Chat kill-switch */}
<section className="rounded-xl border border-purple-500/20 bg-[#161227] p-5">
<h2
className="mb-3 text-base font-semibold text-[#f1eff8]"
style={{ fontFamily: "Outfit, sans-serif" }}
className="mb-3 text-base font-semibold"
style={{ fontFamily: "Outfit, sans-serif", color: "#f1eff8" }}
>
Chat Feature
</h2>
Expand All @@ -143,16 +159,16 @@ export default function AdminProviders() {
{/* LLM settings form */}
<section className="rounded-xl border border-purple-500/20 bg-[#161227] p-5">
<h2
className="mb-4 text-base font-semibold text-[#f1eff8]"
style={{ fontFamily: "Outfit, sans-serif" }}
className="mb-4 text-base font-semibold"
style={{ fontFamily: "Outfit, sans-serif", color: "#f1eff8" }}
>
LLM Configuration
</h2>

<form onSubmit={handleSave} noValidate>
<div className="space-y-4">
{LLM_SETTINGS.map(
({ key, label, type, options, placeholder }) => (
({ key, label, type, options, placeholder, suggestions }) => (
<div key={key}>
<label
htmlFor={`setting-${key}`}
Expand All @@ -175,14 +191,26 @@ export default function AdminProviders() {
))}
</select>
) : (
<input
id={`setting-${key}`}
type="text"
value={form[key]}
onChange={(e) => handleChange(key, e.target.value)}
placeholder={placeholder}
className="w-full rounded-lg border border-purple-500/30 bg-[#1e1836] px-3 py-2 text-sm text-[#f1eff8] placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-purple-500/60"
/>
<>
<input
id={`setting-${key}`}
type="text"
value={form[key]}
onChange={(e) => handleChange(key, e.target.value)}
placeholder={placeholder}
list={
suggestions ? `${key}-suggestions` : undefined
}
className="w-full rounded-lg border border-purple-500/30 bg-[#1e1836] px-3 py-2 text-sm text-[#f1eff8] placeholder-neutral-500 focus:outline-none focus:ring-2 focus:ring-purple-500/60"
/>
{suggestions && (
<datalist id={`${key}-suggestions`}>
{suggestions.map((s) => (
<option key={s} value={s} />
))}
</datalist>
)}
</>
)}
</div>
),
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/test/mocks/handlers.js
Original file line number Diff line number Diff line change
Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
},
Expand Down Expand Up @@ -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",
},
Expand Down
32 changes: 32 additions & 0 deletions frontend/src/test/setup.js
Original file line number Diff line number Diff line change
Expand Up @@ -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) => ({
Expand All @@ -23,6 +54,7 @@ afterEach(() => {
server.resetHandlers();
resetMockState();
localStorage.clear();
sessionStorage.clear();
});

afterAll(() => server.close());
Loading