7.0 KiB
Grademaxxing - AI Coding Agent Instructions
Project Architecture
Fullstack SPA Architecture:
- Frontend: React Router 7 SPA (client-side only, no SSR) built with Vite + TypeScript + Tailwind CSS v4
- Backend: FastAPI (Python) serves both API (
/api/*) and built React SPA (build/client/) - Database: MongoDB with Motor async driver
- Deployment: Docker Compose with 3 services (mongodb, frontend-build, backend)
Critical Architectural Decision: Backend serves the compiled React SPA from build/client/ directory. All non-/api/* routes fall through to index.html for client-side routing. Frontend must be built before backend serves it.
Essential Workflows
Development:
# Docker (recommended) - starts MongoDB + builds frontend + runs backend
docker-compose up --build
# Local development (3 terminals)
# Terminal 1: cd backend && uvicorn main:app --reload
# Terminal 2: pnpm dev
# Terminal 3: mongod --dbpath ./data
Build & Deploy:
pnpm build # Builds React SPA to build/client/
# Backend automatically serves from build/client/ when running
Quick start scripts: start.bat (Windows) or ./start.sh (Linux/Mac) wrap docker-compose up --build
Data Model & Business Logic
Flexible Grading System (Not German-specific):
- Each
Subjecthas customgrading_categories(e.g., "Written" 60%, "Oral" 40%) - Categories must sum to 100% weight (validated in backend/models.py)
- Each
Gradehasgrade/max_grade(e.g., 85/100) andweight_in_categoryfor multiple tests - Supports any grading scale (percentage, German 1-6, US letters, etc.)
Grade Calculation Pipeline (app/utils/gradeCalculations.ts):
- Normalize each grade to percentage:
(grade / max_grade) * 100 - Calculate category average: weighted sum of grades in category
- Calculate subject average: weighted sum of category averages
- Calculate overall GPA: simple average of all subject averages
Report Periods: Filter grades by date range for semester/quarter calculations (see calculateReportCard)
Code Conventions
Backend (FastAPI + MongoDB):
- All routes require auth via
Depends(get_current_user)from backend/routes/auth_routes.py - MongoDB ObjectId handling: Convert to string for JSON responses, convert to
ObjectId()for queries - Model pattern:
{Entity}Base→{Entity}Create/Update→{Entity}InDB→{Entity}Response(see backend/models.py) - Route structure: Each entity gets its own router in
backend/routes/, imported in main.py - Always use
awaitwith Motor async operations:await db.collection.find().to_list()
Frontend (React Router 7 SPA):
- Route definitions in app/routes.ts, files in
app/routes/(use.tsxextension) - All authenticated pages wrap children in
<ProtectedRoute>component (app/components/ProtectedRoute.tsx) - API client singleton:
import { api } from "~/api/client"- handles auth headers automatically - Auth token stored in localStorage (
access_token), set on login/register, removed on logout - Component pattern: Small reusable components in
app/components/, import as~/components/{Name} - Tailwind CSS only - no CSS modules or styled-components
TypeScript Type Safety:
- Backend types defined in backend/models.py (Pydantic)
- Frontend types in app/types/api.ts - manually sync with backend models
- Use
_id(alias) for MongoDB ObjectId in responses, map toidfield in TypeScript when needed
Authentication & Authorization
Flow:
- Login/register → Backend returns JWT + user data (backend/routes/auth_routes.py)
- Frontend stores token in localStorage (app/api/client.ts)
- All protected API calls include
Authorization: Bearer {token}header - Backend validates token with
get_current_userdependency - Frontend uses
ProtectedRoutewrapper to redirect unauthenticated users
User Isolation: All database queries filter by user_id (from token) - users only see their own data
Common Patterns
Adding a New Entity:
- Define Pydantic models in backend/models.py:
{Entity}Base,{Entity}Create,{Entity}InDB,{Entity}Response - Create router in
backend/routes/{entity}_routes.pywith CRUD endpoints - Import and include router in backend/main.py
- Add TypeScript types to app/types/api.ts
- Add API client methods to app/api/client.ts
- Create routes/components in
app/
Adding a New Route:
- Create
app/routes/{name}.tsxwith default export component - Add route to app/routes.ts config array
- Wrap in
<ProtectedRoute>if auth required
MongoDB Query Pattern:
db = get_database()
cursor = db.collection.find({"user_id": current_user.id})
items = await cursor.to_list(length=None)
for item in items:
item["_id"] = str(item["_id"]) # Convert ObjectId to string
return [ResponseModel(**item) for item in items]
Critical Files
- backend/main.py - FastAPI app initialization, SPA serving, route inclusion
- app/api/client.ts - Centralized API client with auth
- app/utils/gradeCalculations.ts - Core business logic for grade calculations
- backend/models.py - Pydantic models with validation (category weights must sum to 100%)
- docker-compose.yml - Multi-stage build: frontend-build → backend serves result
Known Issues & Gotchas
- Frontend build must complete before backend serves (
frontend-buildcontainer in Docker) - MongoDB ObjectId serialization: Always convert to string for JSON responses
- CORS: Backend allows origins from
CORS_ORIGINSenv var (comma-separated) - Category weights validation: Must sum to 100% (enforced in Pydantic model)
- Date handling: Python uses
datetime.utcnow(), frontend converts ISO strings - No SSR: React Router in SPA mode only, backend serves static build
Testing
Manual testing: See DEVELOPMENT.md checklist (register → create period → add subject → add grades → view dashboard)
API Testing: FastAPI auto-docs at http://localhost:8000/docs (Swagger UI)
Environment Setup
Required Environment Variables:
MONGODB_URL- MongoDB connection string (default:mongodb://localhost:27017)SECRET_KEY- JWT signing key (generate withpython -c "import secrets; print(secrets.token_urlsafe(64))")DATABASE_NAME- MongoDB database name (default:grademaxxing)CORS_ORIGINS- Comma-separated allowed origins (default:http://localhost:5173,http://localhost:8000)ACCESS_TOKEN_EXPIRE_MINUTES- JWT expiration (default: 10080 = 7 days)
Frontend uses VITE_API_URL for API base URL (defaults to http://localhost:8000/api)