MAESTRO: Initialize frontend routing with 8 placeholder page components and vitest test suite

Add SetupPage, LoginPage, DashboardPage, ProjectsPage, ExperimentPage, LivePage,
ComparePage, and AdminPage as placeholder components. Wire up react-router-dom routing
in App.tsx with BrowserRouter in main.tsx. Unknown routes redirect to dashboard.
Install vitest + @testing-library/react and add 9 routing tests. Build passes cleanly.
This commit is contained in:
John Lightner 2026-04-07 02:03:48 -05:00
parent 267091bbce
commit 4cd0b8a1c8
17 changed files with 4120 additions and 9 deletions

View file

@ -35,9 +35,11 @@ Set up the PromptLooper repository, Docker infrastructure, and basic project ske
- [x] Create backend/auth.py implementing JWT token generation/verification, API key validation, and the first-boot setup flow. The setup endpoint should check if any users exist — if not, accept username + password to create the admin account. Include a dependency function for route-level auth that supports both JWT and API key.
> Created backend/auth.py with: bcrypt password hashing via passlib, JWT token creation/verification (HS256, 24h expiry) using python-jose, first-boot `needs_setup()` + `create_admin()` flow (409 if admin exists), `authenticate_user()` for login, and `get_current_user` FastAPI dependency supporting both JWT Bearer tokens and X-Api-Key header (API key grants first admin user). UUID string-to-UUID conversion for SQLite compatibility. 21 tests in tests/test_auth.py covering hashing, JWT lifecycle, setup flow, login, and all auth dependency paths. All 95 backend tests pass.
- [ ] Scaffold all router files in backend/routers/ as stubs: auth.py, projects.py, experiments.py, runs.py, endpoints.py, export.py, webhooks.py, admin.py. Each should have the correct APIRouter prefix and tags, with placeholder endpoints that return 501 Not Implemented.
- [x] Scaffold all router files in backend/routers/ as stubs: auth.py, projects.py, experiments.py, runs.py, endpoints.py, export.py, webhooks.py, admin.py. Each should have the correct APIRouter prefix and tags, with placeholder endpoints that return 501 Not Implemented.
> Created all 8 router stubs with APIRouter instances, mounted via main.py's _mount_routers(). Endpoints match the spec: auth (3 endpoints), projects (5), experiments (9 incl. sweep/pause/resume/stop), runs (5 incl. leaderboard), endpoints (5 incl. test), export (4 formats), webhooks (3), admin (3). All return 501 Not Implemented. 37 tests in tests/test_routers.py verify every route is mounted and returns 501. All 132 backend tests pass.
- [ ] Initialize the frontend: run npm create vite@latest with React + TypeScript template. Install Tailwind CSS and configure it. Install react-router-dom for routing. Create the basic App.tsx with routes for Setup, Login, Dashboard, Projects, Experiment, Live, Compare, and Admin pages (all as placeholder components). Verify it builds cleanly.
- [x] Initialize the frontend: run npm create vite@latest with React + TypeScript template. Install Tailwind CSS and configure it. Install react-router-dom for routing. Create the basic App.tsx with routes for Setup, Login, Dashboard, Projects, Experiment, Live, Compare, and Admin pages (all as placeholder components). Verify it builds cleanly.
> Frontend was already scaffolded with Vite + React + TypeScript + Tailwind + react-router-dom from the Dockerfile task. Added 8 placeholder page components (SetupPage, LoginPage, DashboardPage, ProjectsPage, ExperimentPage, LivePage, ComparePage, AdminPage) in frontend/src/pages/. Updated App.tsx with react-router-dom Routes and main.tsx with BrowserRouter. Unknown routes redirect to dashboard. Installed vitest + @testing-library/react for testing. 9 routing tests in App.test.tsx all passing. Build completes cleanly. All 132 backend tests still pass.
- [ ] Create frontend/src/api/client.ts with a typed API client using fetch. Include JWT token management (stored in memory, not localStorage), request/response interceptors for auth headers, and typed wrapper functions for each API endpoint group. Include WebSocket connection helper.

3948
frontend/package-lock.json generated Normal file

File diff suppressed because it is too large Load diff

View file

@ -6,7 +6,8 @@
"scripts": {
"dev": "vite",
"build": "tsc && vite build",
"preview": "vite preview"
"preview": "vite preview",
"test": "vitest run"
},
"dependencies": {
"react": "^18.3.1",
@ -14,13 +15,17 @@
"react-router-dom": "^6.28.0"
},
"devDependencies": {
"@testing-library/jest-dom": "^6.9.1",
"@testing-library/react": "^16.3.2",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@vitejs/plugin-react": "^4.3.4",
"autoprefixer": "^10.4.20",
"jsdom": "^29.0.2",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.15",
"typescript": "^5.6.3",
"vite": "^6.0.0"
"vite": "^6.0.0",
"vitest": "^4.1.2"
}
}

59
frontend/src/App.test.tsx Normal file
View file

@ -0,0 +1,59 @@
import { render, screen } from "@testing-library/react";
import { MemoryRouter } from "react-router-dom";
import { describe, it, expect } from "vitest";
import App from "./App";
function renderWithRouter(route: string) {
return render(
<MemoryRouter initialEntries={[route]}>
<App />
</MemoryRouter>,
);
}
describe("App routing", () => {
it("renders SetupPage at /setup", () => {
renderWithRouter("/setup");
expect(screen.getByText("PromptLooper Setup")).toBeInTheDocument();
});
it("renders LoginPage at /login", () => {
renderWithRouter("/login");
expect(screen.getByText("Sign In")).toBeInTheDocument();
});
it("renders DashboardPage at /", () => {
renderWithRouter("/");
expect(screen.getByText("Dashboard")).toBeInTheDocument();
});
it("renders ProjectsPage at /projects", () => {
renderWithRouter("/projects");
expect(screen.getByText("Projects")).toBeInTheDocument();
});
it("renders ExperimentPage at /experiments/:id", () => {
renderWithRouter("/experiments/abc-123");
expect(screen.getByText("Experiment")).toBeInTheDocument();
});
it("renders LivePage at /live/:id", () => {
renderWithRouter("/live/abc-123");
expect(screen.getByText("Live")).toBeInTheDocument();
});
it("renders ComparePage at /compare", () => {
renderWithRouter("/compare");
expect(screen.getByText("Compare")).toBeInTheDocument();
});
it("renders AdminPage at /admin", () => {
renderWithRouter("/admin");
expect(screen.getByText("Admin")).toBeInTheDocument();
});
it("redirects unknown routes to dashboard", () => {
renderWithRouter("/nonexistent");
expect(screen.getByText("Dashboard")).toBeInTheDocument();
});
});

View file

@ -1,5 +1,25 @@
function App() {
return <div>PromptLooper</div>;
}
import { Routes, Route, Navigate } from "react-router-dom";
import SetupPage from "./pages/SetupPage";
import LoginPage from "./pages/LoginPage";
import DashboardPage from "./pages/DashboardPage";
import ProjectsPage from "./pages/ProjectsPage";
import ExperimentPage from "./pages/ExperimentPage";
import LivePage from "./pages/LivePage";
import ComparePage from "./pages/ComparePage";
import AdminPage from "./pages/AdminPage";
export default App;
export default function App() {
return (
<Routes>
<Route path="/setup" element={<SetupPage />} />
<Route path="/login" element={<LoginPage />} />
<Route path="/" element={<DashboardPage />} />
<Route path="/projects" element={<ProjectsPage />} />
<Route path="/experiments/:id" element={<ExperimentPage />} />
<Route path="/live/:id" element={<LivePage />} />
<Route path="/compare" element={<ComparePage />} />
<Route path="/admin" element={<AdminPage />} />
<Route path="*" element={<Navigate to="/" replace />} />
</Routes>
);
}

View file

@ -1,10 +1,13 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { BrowserRouter } from "react-router-dom";
import App from "./App";
import "./index.css";
ReactDOM.createRoot(document.getElementById("root")!).render(
<React.StrictMode>
<BrowserRouter>
<App />
</BrowserRouter>
</React.StrictMode>,
);

View file

@ -0,0 +1,8 @@
export default function AdminPage() {
return (
<div className="p-8">
<h1 className="mb-4 text-2xl font-bold">Admin</h1>
<p className="text-gray-600">System administration and user management.</p>
</div>
);
}

View file

@ -0,0 +1,8 @@
export default function ComparePage() {
return (
<div className="p-8">
<h1 className="mb-4 text-2xl font-bold">Compare</h1>
<p className="text-gray-600">Compare results across runs and experiments.</p>
</div>
);
}

View file

@ -0,0 +1,8 @@
export default function DashboardPage() {
return (
<div className="p-8">
<h1 className="mb-4 text-2xl font-bold">Dashboard</h1>
<p className="text-gray-600">Overview of recent experiments and runs.</p>
</div>
);
}

View file

@ -0,0 +1,8 @@
export default function ExperimentPage() {
return (
<div className="p-8">
<h1 className="mb-4 text-2xl font-bold">Experiment</h1>
<p className="text-gray-600">Configure and run prompt experiments.</p>
</div>
);
}

View file

@ -0,0 +1,8 @@
export default function LivePage() {
return (
<div className="p-8">
<h1 className="mb-4 text-2xl font-bold">Live</h1>
<p className="text-gray-600">Real-time experiment progress and results.</p>
</div>
);
}

View file

@ -0,0 +1,10 @@
export default function LoginPage() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow">
<h1 className="mb-4 text-2xl font-bold">Sign In</h1>
<p className="text-gray-600">Log in to PromptLooper.</p>
</div>
</div>
);
}

View file

@ -0,0 +1,8 @@
export default function ProjectsPage() {
return (
<div className="p-8">
<h1 className="mb-4 text-2xl font-bold">Projects</h1>
<p className="text-gray-600">Manage your prompt tuning projects.</p>
</div>
);
}

View file

@ -0,0 +1,10 @@
export default function SetupPage() {
return (
<div className="flex min-h-screen items-center justify-center bg-gray-50">
<div className="w-full max-w-md rounded-lg bg-white p-8 shadow">
<h1 className="mb-4 text-2xl font-bold">PromptLooper Setup</h1>
<p className="text-gray-600">Create your admin account to get started.</p>
</div>
</div>
);
}

View file

@ -0,0 +1 @@
import "@testing-library/jest-dom/vitest";

View file

@ -17,4 +17,9 @@ export default defineConfig({
"/health": "http://localhost:8000",
},
},
test: {
environment: "jsdom",
globals: true,
setupFiles: ["./src/test-setup.ts"],
},
});