11 KiB
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
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 (
<div className="min-h-screen bg-gray-50 flex items-center justify-center">
<div className="bg-white rounded-lg shadow p-8 w-full max-w-sm">
<h1 className="text-2xl font-bold text-gray-900 mb-2 text-center">MealPlanner</h1>
<p className="text-sm text-gray-500 text-center mb-6">Family login</p>
<form onSubmit={handleSubmit} className="space-y-4">
<div>
<label className="block text-sm font-medium text-gray-700 mb-1">
Password
</label>
<input
type="password"
value={password}
onChange={e => 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
/>
</div>
{error && <p className="text-red-600 text-sm">{error}</p>}
<button
type="submit"
disabled={loading}
className="w-full bg-blue-600 text-white py-2 rounded-lg hover:bg-blue-700 disabled:opacity-50 font-medium"
>
{loading ? 'Signing in…' : 'Sign in'}
</button>
</form>
</div>
</div>
)
}
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):
<div className="flex items-center">
<button
onClick={async () => {
await mealPlannerApi.auth.logout().catch(() => {})
window.location.href = '/login'
}}
className="text-sm text-gray-500 hover:text-gray-900"
>
Sign out
</button>
</div>
Add the Login route inside <Routes>:
<Route path="/login" element={<Login />} />
The full updated App.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 (
<QueryClientProvider client={queryClient}>
<BrowserRouter>
<div className="min-h-screen bg-gray-50">
<nav className="bg-white shadow-sm">
<div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
<div className="flex justify-between h-16">
<div className="flex space-x-8">
<Link to="/" className="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-900">
MealPlanner
</Link>
<Link to="/pantry" className="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-500 hover:text-gray-900">
Pantry
</Link>
<Link to="/shopping-list" className="inline-flex items-center px-1 pt-1 text-sm font-medium text-gray-500 hover:text-gray-900">
Shopping List
</Link>
</div>
<div className="flex items-center">
<button
onClick={async () => {
await mealPlannerApi.auth.logout().catch(() => {})
window.location.href = '/login'
}}
className="text-sm text-gray-500 hover:text-gray-900"
>
Sign out
</button>
</div>
</div>
</div>
</nav>
<main className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-8">
<Routes>
<Route path="/" element={<Dashboard />} />
<Route path="/meals/:id" element={<MealDetail />} />
<Route path="/pantry" element={<Pantry />} />
<Route path="/shopping-list" element={<ShoppingList />} />
<Route path="/login" element={<Login />} />
</Routes>
</main>
</div>
</BrowserRouter>
</QueryClientProvider>
)
}
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:
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
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
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.
#!/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
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
docker compose --env-file .env.test build frontend
docker compose --env-file .env.test up -d
3. Run the seed script
# 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."