diff --git a/docs/superpowers/plans/2026-05-09-mvp-login.md b/docs/superpowers/plans/2026-05-09-mvp-login.md new file mode 100644 index 0000000..4be4907 --- /dev/null +++ b/docs/superpowers/plans/2026-05-09-mvp-login.md @@ -0,0 +1,326 @@ +# MVP Login + Family Seed Implementation Plan + +> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development to implement this plan task-by-task. + +**Goal:** Make the UI actually usable — login page, auth gating, family profile seed, and vote-link base URL fix. + +**Architecture:** Three changes: (1) React Login page + 401 interceptor in the frontend so unauthenticated users are redirected; (2) Python seed script to insert the first `family_profile` + `family_member` rows (no API endpoint exists to create them); (3) `APP_BASE_URL` added to `.env.test` so vote links in emails point to the right host. + +**Tech Stack:** React 18 + TypeScript + Vite + Tailwind, axios, FastAPI, SQLAlchemy 2.0, Postgres 15. + +--- + +## File map + +| File | Action | +|---|---| +| `frontend/src/pages/Login.tsx` | **Create** — password form, calls `auth.login()`, redirects to `/` | +| `frontend/src/App.tsx` | **Modify** — add `/login` route, logout button in nav | +| `frontend/src/api/index.ts` | **Modify** — add 401 response interceptor → redirect to `/login` | +| `scripts/seed_family.py` | **Create** — inserts `family_profile` + `family_member` rows via SQLAlchemy | + +--- + +## Task 1: Login page + 401 interceptor + nav logout + +**Worktree:** `/home/peter/Projects/MealPlanner/.worktrees/feature-mvp-login` + +**Files:** +- Create: `frontend/src/pages/Login.tsx` +- Modify: `frontend/src/App.tsx` +- Modify: `frontend/src/api/index.ts` + +### Step 1: Create `frontend/src/pages/Login.tsx` + +```tsx +import { useState } from 'react' +import { mealPlannerApi } from '../api' + +export default function Login() { + const [password, setPassword] = useState('') + const [error, setError] = useState('') + const [loading, setLoading] = useState(false) + + const handleSubmit = async (e: React.FormEvent) => { + e.preventDefault() + setError('') + setLoading(true) + try { + await mealPlannerApi.auth.login(password) + window.location.href = '/' + } catch { + setError('Incorrect password. Try again.') + } finally { + setLoading(false) + } + } + + return ( +
+
+

MealPlanner

+

Family login

+
+
+ + setPassword(e.target.value)} + className="w-full border border-gray-300 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-blue-500" + autoFocus + required + /> +
+ {error &&

{error}

} + +
+
+
+ ) +} +``` + +### Step 2: Modify `frontend/src/App.tsx` + +Add `import Login from './pages/Login'` to the imports block. + +Add a logout button to the nav (right side of the flex): + +```tsx +
+ +
+``` + +Add the Login route inside ``: + +```tsx +} /> +``` + +The full updated `App.tsx`: + +```tsx +import { BrowserRouter, Routes, Route, Link } from 'react-router-dom' +import { QueryClient, QueryClientProvider } from '@tanstack/react-query' +import Dashboard from './pages/Dashboard' +import MealDetail from './pages/MealDetail' +import Pantry from './pages/Pantry' +import ShoppingList from './pages/ShoppingList' +import Login from './pages/Login' +import { mealPlannerApi } from './api' + +const queryClient = new QueryClient() + +function App() { + return ( + + +
+ +
+ + } /> + } /> + } /> + } /> + } /> + +
+
+
+
+ ) +} + +export default App +``` + +### Step 3: Add 401 interceptor to `frontend/src/api/index.ts` + +After the `api` axios instance is created (after the `const api = axios.create(...)` block), add: + +```typescript +api.interceptors.response.use( + response => response, + error => { + if (error.response?.status === 401 && window.location.pathname !== '/login') { + window.location.href = '/login' + } + return Promise.reject(error) + } +) +``` + +### Step 4: Build and verify + +```bash +cd /home/peter/Projects/MealPlanner/.worktrees/feature-mvp-login +npm --prefix frontend ci +npm --prefix frontend run build +# Expected: no TypeScript errors, build succeeds +``` + +### Step 5: Commit + +```bash +cd /home/peter/Projects/MealPlanner/.worktrees/feature-mvp-login +git add frontend/src/pages/Login.tsx \ + frontend/src/App.tsx \ + frontend/src/api/index.ts +git commit -m "feat: login page, 401 interceptor, nav sign-out" +``` + +--- + +## Task 2: Family profile seed script + +**File:** Create `scripts/seed_family.py` + +This script inserts a `family_profile` row and `family_member` rows directly via SQLAlchemy. Run once after the containers are up. + +```python +#!/usr/bin/env python3 +""" +One-time seed: creates the Woolery family profile + members. + +Run inside the backend container: + docker compose --env-file .env.test exec backend python /app/seed_family.py + +Or copy and run: + docker cp scripts/seed_family.py mealplanner-backend-1:/app/seed_family.py + docker compose --env-file .env.test exec backend python /app/seed_family.py +""" +import os +import sys + +# ── Edit these before running ──────────────────────────────────────────────── +FAMILY_CALORIE_TARGET = 500 # per serving, per meal +MEMBERS = [ + {"name": "Peter", "email": "peter@research.bike", "role": "admin", "likes_mushrooms": True}, + {"name": "Wife", "email": "", "role": "member", "likes_mushrooms": False}, + {"name": "Kid 1", "email": "", "role": "member", "likes_mushrooms": False}, + {"name": "Kid 2", "email": "", "role": "member", "likes_mushrooms": False}, +] +# ───────────────────────────────────────────────────────────────────────────── + +sys.path.insert(0, "/app") +os.environ.setdefault("DATABASE_URL", os.environ["DATABASE_URL"]) + +from app.database import SessionLocal +from app.models import FamilyProfile, FamilyMember, FamilyMemberRole + +db = SessionLocal() +try: + existing = db.query(FamilyProfile).first() + if existing: + print(f"Family profile already exists (id={existing.id}). Nothing to do.") + sys.exit(0) + + profile = FamilyProfile(calorie_target=FAMILY_CALORIE_TARGET) + db.add(profile) + db.flush() + + for m in MEMBERS: + if not m["email"]: + print(f" Skipping {m['name']} — no email set") + continue + member = FamilyMember( + family_profile_id=profile.id, + name=m["name"], + email=m["email"], + role=FamilyMemberRole[m["role"].upper()], + likes_mushrooms=m["likes_mushrooms"], + ) + db.add(member) + print(f" Added member: {m['name']} <{m['email']}>") + + db.commit() + print(f"Done. Family profile id={profile.id}") +finally: + db.close() +``` + +### Step: Commit + +```bash +cd /home/peter/Projects/MealPlanner/.worktrees/feature-mvp-login +git add scripts/seed_family.py +git commit -m "feat: one-time family profile seed script" +``` + +--- + +## Post-merge steps (run by operator after merge) + +### 1. Add APP_BASE_URL to `.env.test` + +Edit `.env.test` on the host and add: +``` +APP_BASE_URL=http://100.108.224.12:8081 +``` + +This makes vote links in Friday emails point to the right host. + +### 2. Rebuild frontend container and restart + +```bash +docker compose --env-file .env.test build frontend +docker compose --env-file .env.test up -d +``` + +### 3. Run the seed script + +```bash +# Edit scripts/seed_family.py to fill in real names/emails first, then: +docker cp scripts/seed_family.py mealplanner-backend-1:/app/seed_family.py +docker compose --env-file .env.test exec backend python /app/seed_family.py +``` + +### 4. Verify login works + +Open `http://100.108.224.12:8081` — should redirect to `/login`. Enter `SESSION_PASSWORD`. Should land on Dashboard showing "No meal plan generated yet."