Files
gravl/.planning/codebase/ARCHITECTURE.md
T
2026-02-15 21:49:31 +01:00

210 lines
9.2 KiB
Markdown

# Architecture
**Analysis Date:** 2026-02-15
## Pattern Overview
**Overall:** Monolithic multi-tier architecture with separated frontend and backend services.
**Key Characteristics:**
- Frontend: Single-Page Application (SPA) with React + React Router
- Backend: Express.js REST API with direct database queries
- Database: PostgreSQL with relational schema
- State Management: React Context API for authentication, local component state for page-level data
- Communication: HTTP/JSON via Fetch API with Bearer token authentication
- Deployment: Containerized (Docker) with Traefik reverse proxy routing
## Layers
**Presentation Layer (Frontend):**
- Purpose: Render UI, handle user interactions, manage local state and navigation
- Location: `/workspace/gravl/frontend/src/`
- Contains: React pages, components, context providers, CSS styling
- Depends on: React, React Router, AuthContext, backend API endpoints
- Used by: Browser clients
**Application/Page Layer (Frontend):**
- Purpose: Manage view logic, fetch data, orchestrate navigation between different views
- Location: `/workspace/gravl/frontend/src/pages/`
- Contains: Full page components (Dashboard, WorkoutPage, ProfilePage, ProgressPage, LoginPage, RegisterPage, OnboardingWizard, WorkoutSelectPage)
- Depends on: AuthContext, Icon components, API calls via fetch
- Used by: App.jsx routing logic
**Context Layer (Frontend):**
- Purpose: Provide global authentication state and user session management
- Location: `/workspace/gravl/frontend/src/context/AuthContext.jsx`
- Contains: Auth state, login/register/logout functions, token management, localStorage integration
- Depends on: React hooks, backend authentication endpoints
- Used by: All protected pages and components
**API/REST Layer (Backend):**
- Purpose: Handle HTTP requests, validate input, manage authentication, route requests to data layer
- Location: `/workspace/gravl/backend/src/index.js`
- Contains: Express routes for auth, user profile, programs, exercises, logs, progression
- Depends on: PostgreSQL connection, JWT verification, bcrypt password hashing
- Used by: Frontend via HTTP requests
**Data Layer (Backend):**
- Purpose: Execute queries against PostgreSQL database
- Location: Database queries within `/workspace/gravl/backend/src/index.js` using pg Pool
- Contains: User management, program/day/exercise definitions, workout logs, measurements, strength records
- Depends on: PostgreSQL driver (pg)
- Used by: API layer for all data operations
**Database Layer:**
- Purpose: Persist application data
- Location: `/workspace/gravl/db/init.sql` (schema definition)
- Contains: 7 tables (users, programs, program_days, exercises, program_exercises, workout_logs, user_measurements, user_strength)
- Depends on: PostgreSQL engine
- Used by: Backend data layer
## Data Flow
**User Registration/Login Flow:**
1. User enters credentials on RegisterPage or LoginPage
2. Page calls `useAuth().register()` or `useAuth().login()` from AuthContext
3. AuthContext makes POST to `/api/auth/register` or `/api/auth/login`
4. Backend validates credentials (register: email uniqueness + hash password; login: password verification)
5. Backend returns JWT token and user object
6. AuthContext stores token in localStorage and sets user state
7. Navigation redirects to `/onboarding` (incomplete) or `/` (complete)
**Onboarding Flow:**
1. User completes OnboardingWizard with profile data (gender, age, experience, goal, measurements, strength)
2. Wizard calls `useAuth().updateProfile()` with profile data
3. Backend updates users table and related measurement/strength tables
4. Sets `onboarding_complete = true`
5. User navigated to Dashboard
**Workout/Exercise Flow:**
1. Dashboard displays program days and selected day's workout
2. User clicks workout day, `onStartWorkout()` called
3. App.jsx calls `fetchProgram()` to load program with all days/exercises
4. App.jsx calls `fetchLogs()` to fetch existing workout logs for that day
5. WorkoutPage displayed with exercises and weight/rep input fields
6. User enters weight/reps and clicks "Log Set"
7. `logSet()` calls POST `/api/logs` with exercise_id, weight, reps, date, set_number
8. Backend checks if log exists for that set (update) or creates new (insert)
9. Response updates local logs state
10. WorkoutPage re-renders with updated data
**Progression Calculation Flow:**
1. WorkoutPage calls `fetchProgression()` for each exercise
2. Backend fetches last workout for that exercise (last 10 logs, completed only)
3. Analyzes if all sets hit max_reps
4. Returns suggestedWeight (same weight or +2.5kg if maxed out)
5. Frontend displays suggestion in workout interface
**Profile/Measurements Flow:**
1. User navigates to ProfilePage
2. Page calls parallel fetches: `/api/user/profile`, `/api/user/measurements`, `/api/user/strength`
3. Backend joins latest measurements and strength records with user profile
4. Page displays current profile and can add new measurements or strength records
5. User saves changes → updates user profile state in AuthContext
**State Management:**
- **Global state:** User session, authentication token (AuthContext in localStorage)
- **Page-level state:** Program, logs, current view, selected day (App.jsx state)
- **Component-level state:** Form inputs, editing mode, expanded sections (individual page components)
- **No shared state management library:** Direct React Context + local useState
## Key Abstractions
**AuthContext:**
- Purpose: Centralized authentication and user session management
- Examples: `useAuth()` hook returns { user, token, loading, register, login, logout, updateProfile, refreshProfile }
- Pattern: React Context + custom hook for easy access from any component
**Page Components:**
- Purpose: Encapsulate view logic, form handling, and local data fetching
- Examples: `Dashboard.jsx`, `WorkoutPage.jsx`, `ProfilePage.jsx`
- Pattern: Functional components with useState/useEffect, direct API calls via fetch
**Program/Exercise Model:**
- Purpose: Represent training structure in database and API
- Structure: Program > Days > Exercises (program_exercises join table) > Logs (user workout records)
- Pattern: Nested JSON responses from `/api/programs/:id` endpoint
**Workout Log:**
- Purpose: Record individual set performance (weight, reps, completion status)
- Examples: `workout_logs` table with user_id, program_exercise_id, date, set_number, weight, reps, completed
- Pattern: Upsert logic (update if exists, insert if new)
## Entry Points
**Frontend Entry:**
- Location: `frontend/index.html``src/main.jsx``src/App.jsx`
- Triggers: Browser loads gravl.homelab.local
- Responsibilities:
1. Bootstrap React app with BrowserRouter and AuthProvider
2. Define route structure (auth routes vs. protected routes)
3. Initialize token from localStorage and verify session
4. Render main App component
**Backend Entry:**
- Location: `backend/src/index.js`
- Triggers: Docker container startup (`npm start``node src/index.js`)
- Responsibilities:
1. Initialize Express app and PostgreSQL connection pool
2. Mount CORS and JSON middleware
3. Define all API routes with request/response handling
4. Listen on port 3001
5. Database queries executed inline within route handlers
**Auth-Protected Routes:**
- ProtectedRoute wrapper checks user existence and onboarding status
- Redirects to `/login` if unauthenticated
- Redirects to `/onboarding` if authenticated but onboarding incomplete
- Routes: `/`, `/profile`, `/progress`, `/select-workout`, `/workout`
**Auth Routes:**
- AuthRoute wrapper redirects to `/` or `/onboarding` if already authenticated
- Routes: `/login`, `/register`
## Error Handling
**Strategy:** Try-catch in Express routes returns JSON errors; frontend logs errors and may show error UI
**Patterns:**
- Backend: Catch database/auth errors, return 400/401/500 with JSON error message
- Frontend: Catch fetch errors in async functions, log to console, optionally show in component error state
- Validation: Frontend form validation (required, minLength); backend re-validates email uniqueness
- Auth failures: Return 401 Unauthorized, AuthContext logs user out
- Database errors: Return 500 with generic message (details in server logs only)
## Cross-Cutting Concerns
**Logging:**
- Backend: `console.error()` for exceptions; logs visible in Docker container stdout
- Frontend: `console.error()` for network failures and state issues
**Validation:**
- Frontend: HTML5 form validation (type="email", minLength, required)
- Backend: Email lowercase normalization, null coercion for numeric fields, JWT signature verification
**Authentication:**
- Method: JWT Bearer token in Authorization header
- Token storage: localStorage on browser
- Token validation: `authMiddleware` function checks header and verifies signature
- Token lifetime: 30 days expiration
- Session management: AuthContext refresh on mount, logout clears localStorage and state
**CORS:**
- Enabled globally with `cors()` middleware on all routes
- Frontend proxy configured in Vite for `/api` calls during development
- Docker network configured for service-to-service communication
**Data Integrity:**
- Foreign key constraints in database schema (ON DELETE CASCADE)
- Unique email constraint in users table
- Indexes on frequently queried columns (user_id, date, program_exercise_id)
---
*Architecture analysis: 2026-02-15*