210 lines
9.2 KiB
Markdown
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*
|