- .gitignore: .claude/docs, rules, commands tracken; settings.local weiter ignorieren - DOCUMENTATION.md: verbindliche Ablage functional/technical/working/issues - .claude/README.md: Agent-Einstieg; GITEA_ISSUES_INDEX aus MCP (Stand 2026-04-08) - Arbeitspapiere von docs/ nach .claude/docs/working/ verschoben - docs/MEMBERSHIP_SYSTEM.md als Stub; kanonisch technical/MEMBERSHIP_SYSTEM.md - CLAUDE.md Pflichtlektüre und Links angepasst; docs/README.md vereinfacht Made-with: Cursor
924 lines
28 KiB
Markdown
924 lines
28 KiB
Markdown
# Frontend-Dokumentation
|
||
|
||
## Übersicht
|
||
|
||
Das Frontend ist eine **Progressive Web App (PWA)** gebaut mit React 18, Vite und React Router. Die Architektur folgt einem **Component-based Pattern** mit Context-basiertem State Management (kein Redux).
|
||
|
||
**Technologien:**
|
||
- React 18 (ohne TypeScript)
|
||
- Vite (Build Tool + Dev Server)
|
||
- React Router v6 (Client-side Routing)
|
||
- Recharts (Chart-Bibliothek)
|
||
- Lucide React (Icon Library)
|
||
- Day.js (Datum-Handling)
|
||
|
||
**Bundle-Größe:** ~450 KB (gzip), PWA-Cache für Offline-Nutzung
|
||
|
||
---
|
||
|
||
## Seiten-Übersicht
|
||
|
||
| Seite | Route | Beschreibung | Auth | Admin |
|
||
|-------|-------|--------------|------|-------|
|
||
| **LoginScreen** | `/` (ohne Auth) | E-Mail + Passwort Login, SHA256→bcrypt Auto-Migration | ❌ | ❌ |
|
||
| **Register** | `/register` | Selbst-Registrierung + E-Mail-Verifizierung | ❌ | ❌ |
|
||
| **Verify** | `/verify?token=...` | E-Mail-Verifizierung nach Registrierung | ❌ | ❌ |
|
||
| **SetupScreen** | `/` (First Run) | Initiales Setup (erster Admin-Account) | ❌ | ❌ |
|
||
| **Dashboard** | `/` | Übersicht: Quick Weight, Stats, Charts, Widgets | ✅ | ❌ |
|
||
| **CaptureHub** | `/capture` | Quick-Entry-Auswahl (Gewicht/Umfänge/Caliper/Fotos/Aktivität/Schlaf) | ✅ | ❌ |
|
||
| **WeightScreen** | `/weight` | Gewichts-Tracking mit Inline-Edit | ✅ | ❌ |
|
||
| **CircumScreen** | `/circum` | Umfänge (8 Punkte) | ✅ | ❌ |
|
||
| **CaliperScreen** | `/caliper` | Hautfaltenmessungen (4 Methoden) | ✅ | ❌ |
|
||
| **MeasureWizard** | `/wizard` | Geführte Messung (Schritt-für-Schritt) | ✅ | ❌ |
|
||
| **ActivityPage** | `/activity` | Training + Trainingstypen (v9d) | ✅ | ❌ |
|
||
| **NutritionPage** | `/nutrition` | 3-Tab Layout: Entry / Import / Charts | ✅ | ❌ |
|
||
| **SleepPage** | `/sleep` | Schlaf-Tracking + Phasen + Apple Health Import | ✅ | ❌ |
|
||
| **RestDaysPage** | `/rest-days` | Ruhetage (Kraft/Cardio/Entspannung) | ✅ | ❌ |
|
||
| **VitalsPage** | `/vitals` | 3-Tab: Baseline / Blutdruck / Import | ✅ | ❌ |
|
||
| **History** | `/history` | Verlauf mit Charts (Gewicht, KF%, Umfänge, etc.) | ✅ | ❌ |
|
||
| **Analysis** | `/analysis` | KI-Auswertung + Pipeline | ✅ | ❌ |
|
||
| **SettingsPage** | `/settings` | Profil, PIN-Change, Export, Feature-Usage-Übersicht | ✅ | ❌ |
|
||
| **SubscriptionPage** | `/subscription` | Membership-Status (v9c) | ✅ | ❌ |
|
||
| **GuidePage** | `/guide` | Anleitungen (Caliper, Umfänge) | ✅ | ❌ |
|
||
| **AdminPanel** | `/admin/*` (in Settings) | Admin-Übersicht | ✅ | ✅ |
|
||
| **AdminTierLimitsPage** | `/admin/tier-limits` | Tier-Limits Matrix (v9c) | ✅ | ✅ |
|
||
| **AdminFeaturesPage** | `/admin/features` | Feature-Verwaltung (v9c) | ✅ | ✅ |
|
||
| **AdminTiersPage** | `/admin/tiers` | Tier-Verwaltung (v9c) | ✅ | ✅ |
|
||
| **AdminCouponsPage** | `/admin/coupons` | Coupon-System (v9c) | ✅ | ✅ |
|
||
| **AdminUserRestrictionsPage** | `/admin/user-restrictions` | User-spezifische Limits (v9c) | ✅ | ✅ |
|
||
| **AdminTrainingTypesPage** | `/admin/training-types` | Trainingstypen-CRUD (v9d) | ✅ | ✅ |
|
||
| **AdminActivityMappingsPage** | `/admin/activity-mappings` | Activity Mapping-Verwaltung (v9d) | ✅ | ✅ |
|
||
| **AdminTrainingProfiles** | `/admin/training-profiles` | Training Type Profiling (v9d #15) | ✅ | ✅ |
|
||
|
||
**Gesamt:** 31 Seiten (22 User-facing, 9 Admin)
|
||
|
||
---
|
||
|
||
## Komponenten
|
||
|
||
### Wiederverwendbare Komponenten
|
||
|
||
| Komponente | Props | Beschreibung |
|
||
|-----------|-------|--------------|
|
||
| **Avatar** | `profile, size` | Runder Avatar mit Initialen + Farbe |
|
||
| **Markdown** | `text` | Lightweight Markdown-Renderer (## Headings, **bold**, Listen) |
|
||
| **TrialBanner** | – | Trial-Countdown-Banner (3 Urgency-Level) |
|
||
| **EmailVerificationBanner** | – | E-Mail-Verifizierungs-Hinweis |
|
||
| **FeatureUsageOverview** | – | Tabelle mit allen Feature-Limits + Usage (v9c Phase 3) |
|
||
| **UsageBadge** | `feature` | Inline-Badge mit Limit-Status (z.B. "3/10") |
|
||
| **TrainingTypeDistribution** | `days` | Pie-Chart für Trainingstypen-Verteilung |
|
||
| **SleepWidget** | `days` | Dashboard-Widget mit Schlaf-Stats |
|
||
| **RestDaysWidget** | `weeks` | Dashboard-Widget mit aktuellen Ruhetagen |
|
||
|
||
**Location:** `frontend/src/components/`
|
||
|
||
### Inline-Komponenten (in Seiten definiert)
|
||
|
||
**Dashboard.jsx:**
|
||
- `QuickWeight` – Schnelle Gewichts-Eingabe mit Feature-Limit-Check
|
||
- `StatCard` – Statistik-Karte mit Delta-Anzeige
|
||
- `Pill` – Status-Pill mit Tooltip (WHR, WHtR, KF, Protein Ø7T)
|
||
|
||
**SettingsPage.jsx:**
|
||
- `ProfileForm` – Formular für Profil-Bearbeitung
|
||
|
||
**NutritionPage.jsx:**
|
||
- `EntryTab` – Manuelle Eingabe + CSV-Import
|
||
- `ImportHistoryTab` – Import-Historie mit Gruppierung
|
||
- `ChartsTab` – Korrelationen + Wochendaten
|
||
|
||
**VitalsPage.jsx:**
|
||
- `BaselineTab` – Morgenmessungen (RHR, HRV, VO2 Max, SpO2)
|
||
- `BloodPressureTab` – Blutdruck mehrfach täglich + Context-Tagging
|
||
- `ImportTab` – CSV-Import (Omron Deutsch, Apple Health)
|
||
|
||
---
|
||
|
||
## Context / State Management
|
||
|
||
### 1. AuthContext (`frontend/src/context/AuthContext.jsx`)
|
||
|
||
**Verantwortlichkeit:** Session-Management + Login/Logout
|
||
|
||
**State:**
|
||
```javascript
|
||
{
|
||
session: {
|
||
token: string,
|
||
profile_id: string,
|
||
role: 'user' | 'admin',
|
||
profile: { id, name, email, tier, ... }
|
||
},
|
||
loading: boolean,
|
||
needsSetup: boolean, // First-run detection
|
||
}
|
||
```
|
||
|
||
**Methods:**
|
||
- `login(credentials)` – Login mit E-Mail + Passwort (oder Legacy profile_id + PIN)
|
||
- `setup(formData)` – Initial Setup (First Run)
|
||
- `logout()` – Logout + Token-Löschung
|
||
- `setAuthFromToken(token, profile)` – Direkt-Login (für E-Mail-Verifizierung)
|
||
- `checkStatus()` – Auth-Status prüfen (beim App-Start)
|
||
|
||
**Computed:**
|
||
- `isAdmin` – `session.role === 'admin'`
|
||
- `canUseAI` – `session.profile.ai_enabled !== 0`
|
||
- `canExport` – `session.profile.export_enabled !== 0`
|
||
|
||
**Storage:**
|
||
- `localStorage.bodytrack_token` – Auth-Token
|
||
- `localStorage.bodytrack_active_profile` – Aktive Profile-ID
|
||
|
||
**Flow:**
|
||
```
|
||
App-Start → checkStatus()
|
||
↓
|
||
GET /api/auth/status → {needs_setup: true/false}
|
||
↓ (wenn needs_setup = false)
|
||
GET /api/auth/me (mit Token aus localStorage)
|
||
↓
|
||
Session gesetzt → App.jsx zeigt Dashboard
|
||
```
|
||
|
||
### 2. ProfileContext (`frontend/src/context/ProfileContext.jsx`)
|
||
|
||
**Verantwortlichkeit:** Aktives Profil + Profil-Liste
|
||
|
||
**State:**
|
||
```javascript
|
||
{
|
||
profiles: Array<Profile>, // Alle Profile
|
||
activeProfile: Profile, // Aktuelles Profil
|
||
loading: boolean,
|
||
}
|
||
```
|
||
|
||
**Methods:**
|
||
- `setActiveProfile(profile)` – Profil wechseln (speichert in localStorage)
|
||
- `refreshProfiles()` – Profile neu laden (nach Update)
|
||
|
||
**Flow:**
|
||
```
|
||
session.profile_id ändert sich
|
||
↓
|
||
GET /api/profiles (mit X-Auth-Token)
|
||
↓
|
||
profiles gesetzt, activeProfile = match(session.profile_id)
|
||
```
|
||
|
||
**Hinweis:** Profile-Wechsel ist derzeit Single-User-optimiert (Multi-User-Support in Planung).
|
||
|
||
---
|
||
|
||
## API-Integration (`frontend/src/utils/api.js`)
|
||
|
||
**Zweck:** Zentrale API-Schnittstelle – **ALLE** API-Calls gehen über `api.js`
|
||
|
||
**Features:**
|
||
- Automatisches Token-Injection (`X-Auth-Token` Header)
|
||
- Automatisches Profile-ID-Injection (`X-Profile-Id` Header, derzeit deprecated)
|
||
- Einheitliche Fehlerbehandlung (parst `{detail: "..."}` aus Backend)
|
||
- Typed-like API (alle Methoden dokumentiert)
|
||
|
||
**Beispiel:**
|
||
```javascript
|
||
import { api } from '../utils/api'
|
||
|
||
// GET-Request
|
||
const weights = await api.listWeight(365) // limit=365
|
||
|
||
// POST-Request
|
||
await api.upsertWeight('2026-03-23', 75.5, 'Morgens nüchtern')
|
||
|
||
// DELETE-Request
|
||
await api.deleteWeight(entryId)
|
||
|
||
// File-Upload
|
||
const result = await api.importCsv(file)
|
||
```
|
||
|
||
**Headers-Injection:**
|
||
```javascript
|
||
function hdrs(extra={}) {
|
||
const h = {...extra}
|
||
if (_profileId) h['X-Profile-Id'] = _profileId // Deprecated, bleibt für Legacy
|
||
const token = getToken()
|
||
if (token) h['X-Auth-Token'] = token
|
||
return h
|
||
}
|
||
```
|
||
|
||
**Error-Handling:**
|
||
```javascript
|
||
if (!res.ok) {
|
||
const err = await res.text()
|
||
try {
|
||
const parsed = JSON.parse(err)
|
||
throw new Error(parsed.detail || err)
|
||
} catch {
|
||
throw new Error(err)
|
||
}
|
||
}
|
||
```
|
||
|
||
**API-Methoden (285 Zeilen):**
|
||
- **Profiles:** `getActiveProfile, listProfiles, createProfile, updateProfile, deleteProfile`
|
||
- **Weight:** `listWeight, upsertWeight, updateWeight, deleteWeight, weightStats`
|
||
- **Circumferences:** `listCirc, upsertCirc, updateCirc, deleteCirc`
|
||
- **Caliper:** `listCaliper, upsertCaliper, updateCaliper, deleteCaliper`
|
||
- **Activity:** `listActivity, createActivity, updateActivity, deleteActivity, activityStats, bulkCategorizeActivities, importActivityCsv`
|
||
- **Nutrition:** `importCsv, listNutrition, nutritionCorrelations, nutritionWeekly, nutritionImportHistory, createNutrition, updateNutrition, deleteNutrition`
|
||
- **Photos:** `uploadPhoto, listPhotos, photoUrl`
|
||
- **AI:** `insightTrend, listPrompts, runInsight, insightPipeline, listInsights, latestInsights`
|
||
- **Export:** `exportZip, exportJson, exportCsv` (Download-Handling inkludiert)
|
||
- **Admin:** `adminListProfiles, adminCreateProfile, adminDeleteProfile, adminSetPermissions, changePin`
|
||
- **Auth:** `register, verifyEmail, resendVerification`
|
||
- **Subscription (v9c):** `getMySubscription, getMyUsage, getMyLimits, redeemCoupon, getFeatureUsage`
|
||
- **Admin Features (v9c):** `listFeatures, createFeature, updateFeature, deleteFeature`
|
||
- **Admin Tiers (v9c):** `listTiers, createTier, updateTier, deleteTier, getTierLimitsMatrix, updateTierLimit, updateTierLimitsBatch`
|
||
- **Admin User Restrictions (v9c):** `listUserRestrictions, createUserRestriction, updateUserRestriction, deleteUserRestriction`
|
||
- **Admin Coupons (v9c):** `listCoupons, createCoupon, updateCoupon, deleteCoupon, getCouponRedemptions`
|
||
- **Admin Access Grants (v9c):** `listAccessGrants, createAccessGrant, updateAccessGrant, revokeAccessGrant`
|
||
- **Training Types (v9d):** `listTrainingTypes, listTrainingTypesFlat, getTrainingCategories, adminListTrainingTypes, adminCreateTrainingType, adminUpdateTrainingType, adminDeleteTrainingType, getAbilitiesTaxonomy`
|
||
- **Training Profiles (v9d #15):** `getProfileStats, getProfileTemplates, getProfileTemplate, applyProfileTemplate, getTrainingParameters, batchEvaluateActivities`
|
||
- **Activity Mappings (v9d):** `adminListActivityMappings, adminCreateActivityMapping, adminUpdateActivityMapping, adminDeleteActivityMapping, adminGetMappingCoverage`
|
||
- **Sleep (v9d):** `listSleep, getSleepByDate, createSleep, updateSleep, deleteSleep, getSleepStats, getSleepDebt, getSleepTrend, getSleepPhases, importAppleHealthSleep`
|
||
- **Rest Days (v9d):** `listRestDays, createRestDay, getRestDay, updateRestDay, deleteRestDay, getRestDaysStats, validateActivity`
|
||
- **Vitals Baseline (v9d):** `listBaseline, getBaselineByDate, createBaseline, updateBaseline, deleteBaseline, getBaselineStats, importBaselineAppleHealth`
|
||
- **Blood Pressure (v9d):** `listBloodPressure, getBPByDate, createBloodPressure, updateBloodPressure, deleteBloodPressure, getBPStats, importBPOmron`
|
||
|
||
---
|
||
|
||
## Berechnungs-Utils
|
||
|
||
### 1. calc.js (`frontend/src/utils/calc.js`)
|
||
|
||
**Zweck:** Körperfett-Berechnungen + Derived Metrics
|
||
|
||
**Funktionen:**
|
||
|
||
**`calcBodyFat(method, skinfolds, sex, age)`**
|
||
- Berechnet Körperfett-% nach 4 Methoden:
|
||
- `jackson3` – Jackson-Pollock 3-Punkt (Standard)
|
||
- `jackson7` – Jackson-Pollock 7-Punkt
|
||
- `durnin` – Durnin-Womersley 4-Punkt
|
||
- `parrillo` – Parrillo 9-Punkt (linear)
|
||
- Nutzt Siri-Formel: `BF% = (495 / D) - 450`
|
||
- Parameter:
|
||
- `method`: String ('jackson3', 'jackson7', 'durnin', 'parrillo')
|
||
- `skinfolds`: Object mit Hautfalten in mm (z.B. `{chest: 12, abdomen: 24, thigh: 18}`)
|
||
- `sex`: 'm' | 'f'
|
||
- `age`: Number
|
||
|
||
**`getBfCategory(pct, sex)`**
|
||
- Kategorisiert Körperfett-% in Bereiche:
|
||
- Männer: Essenziell (<6%), Athletisch (6-14%), Fit (14-18%), Durchschnitt (18-25%), Übergewicht (>25%)
|
||
- Frauen: Essenziell (<14%), Athletisch (14-21%), Fit (21-25%), Durchschnitt (25-32%), Übergewicht (>32%)
|
||
- Returns: `{max, label, color, desc}`
|
||
|
||
**`calcDerived(measurement, height)`**
|
||
- Berechnet abgeleitete Metriken:
|
||
- **WHR** (Waist-Hip-Ratio): `waist / hip` (Ziel: <0.90 M / <0.85 F)
|
||
- **WHtR** (Waist-to-Height-Ratio): `waist / height` (Ziel: <0.50)
|
||
- **FFMI** (Fat-Free Mass Index): `lean_mass / (height_m²)` (Natural Limit: ~25 M / ~22 F)
|
||
- Returns: `{whr, whtr, ffmi}`
|
||
|
||
**`getRuleBasedAssessment(current, previous, profile)`**
|
||
- Generiert automatische Interpretationen basierend auf:
|
||
- Körperfett-Kategorie
|
||
- Änderungen seit letzter Messung
|
||
- FFMI (Muskel-Index)
|
||
- WHR / WHtR (Fettverteilung)
|
||
- Taillenumfang (WHO-Grenzwerte)
|
||
- Returns: `{findings: Array, summary: string, summaryType: 'good'|'warn'|'bad'}`
|
||
|
||
**Guide-Daten:**
|
||
- `CIRCUMFERENCE_GUIDE` – Messanleitung für 8 Umfangspunkte (wo, wie, Posture, Tipps)
|
||
- `CALIPER_GUIDE` – Messanleitung für Hautfalten-Punkte
|
||
|
||
### 2. interpret.js (`frontend/src/utils/interpret.js`)
|
||
|
||
**Zweck:** Interpretation von Messwerten
|
||
|
||
**`getInterpretation(measurement, profile, prevMeasurement)`**
|
||
- Analysiert Messung und generiert strukturierte Interpretation:
|
||
- Körperfett-Status (mit Kategorie + Farbe)
|
||
- WHR-Status
|
||
- WHtR-Status
|
||
- FFMI-Status
|
||
- BMI-Status
|
||
- Vergleich zur letzten Messung (Deltas)
|
||
- Returns: Array von Interpretations-Objects:
|
||
```javascript
|
||
{
|
||
category: 'Körperfett',
|
||
icon: '🫧',
|
||
status: 'good' | 'warn' | 'bad',
|
||
title: 'Athletischer Körperfettanteil',
|
||
detail: 'Ausgezeichnet. Typisch für aktive Sportler...',
|
||
value: '12.5%',
|
||
badge: 'Athletisch',
|
||
color: '#1D9E75',
|
||
}
|
||
```
|
||
|
||
**`getStatusColor(status)`** – Farbe für Status ('good'→Grün, 'warn'→Orange, 'bad'→Rot)
|
||
|
||
**`getStatusBg(status)`** – Background-Farbe für Status
|
||
|
||
### 3. Markdown.jsx (`frontend/src/utils/Markdown.jsx`)
|
||
|
||
**Zweck:** Leichtgewichtiger Markdown-Renderer für KI-Texte
|
||
|
||
**Unterstützte Syntax:**
|
||
- `# Heading 1`, `## Heading 2`, `### Heading 3`
|
||
- `**bold**`, `*italic*`
|
||
- `- Bullet List`, `1. Numbered List`
|
||
- `---` (Horizontal Rule)
|
||
- Line Breaks
|
||
|
||
**Verwendung:**
|
||
```jsx
|
||
<Markdown text={aiInsight.content} />
|
||
```
|
||
|
||
**Vorteil:** Kein remark/rehype-Dependency – nur 134 Zeilen pures React
|
||
|
||
---
|
||
|
||
## CSS-System (`frontend/src/app.css`)
|
||
|
||
### CSS-Variablen (Light + Dark Mode)
|
||
|
||
**Farben:**
|
||
```css
|
||
:root {
|
||
--bg: #f4f3ef; /* Hintergrund */
|
||
--surface: #ffffff; /* Cards */
|
||
--surface2: #f9f8f5; /* Inputs, Secondary */
|
||
--border: rgba(0,0,0,0.09); /* Standard-Border */
|
||
--border2: rgba(0,0,0,0.16);/* Input-Border */
|
||
--text1: #1c1b18; /* Primär-Text */
|
||
--text2: #5a5955; /* Sekundär-Text */
|
||
--text3: #9a9892; /* Muted */
|
||
--accent: #1D9E75; /* Primär-Farbe */
|
||
--accent-light: #E1F5EE; /* Accent-Background */
|
||
--accent-dark: #0a5c43; /* Hover */
|
||
--danger: #D85A30; /* Fehler/Löschen */
|
||
--warn: #EF9F27; /* Warnung */
|
||
}
|
||
|
||
@media (prefers-color-scheme: dark) {
|
||
:root {
|
||
--bg: #181816;
|
||
--surface: #222220;
|
||
--surface2: #1e1e1c;
|
||
--border: rgba(255,255,255,0.08);
|
||
--text1: #eeecea;
|
||
--text2: #aaa9a4;
|
||
--text3: #686762;
|
||
--accent-light: #04342C;
|
||
--accent-dark: #5DCAA5;
|
||
}
|
||
}
|
||
```
|
||
|
||
**Layout:**
|
||
```css
|
||
--nav-h: 64px; /* Bottom Navigation Höhe */
|
||
--header-h: 52px; /* App-Header Höhe */
|
||
```
|
||
|
||
### Utility Classes
|
||
|
||
**Cards:**
|
||
```css
|
||
.card /* Standard-Card (white background, border, rounded) */
|
||
.card-title /* Card-Überschrift (uppercase, small, muted) */
|
||
```
|
||
|
||
**Stats:**
|
||
```css
|
||
.stats-grid /* 2-Column Grid für Stats */
|
||
.stat-card /* Einzelne Stat-Card */
|
||
.stat-val /* Wert (groß, bold) */
|
||
.stat-label /* Label (klein, muted) */
|
||
.stat-delta /* Delta (z.B. "+2.5 kg") */
|
||
.delta-pos /* Positive Änderung (grün) */
|
||
.delta-neg /* Negative Änderung (rot) */
|
||
```
|
||
|
||
**Forms:**
|
||
```css
|
||
.form-section /* Formular-Sektion mit Abstand */
|
||
.form-section-title /* Sektions-Titel (uppercase, border-bottom) */
|
||
.form-row /* Zeile mit Label + Input + Unit */
|
||
.form-label /* Label (links, flex:1) */
|
||
.form-input /* Input (90px breit, text-align:right) */
|
||
.form-unit /* Einheit (z.B. "kg", 24px breit) */
|
||
.form-select /* Select-Dropdown */
|
||
.form-sub /* Sub-Label (klein, muted) */
|
||
```
|
||
|
||
**Buttons:**
|
||
```css
|
||
.btn /* Base Button */
|
||
.btn-primary /* Primär-Button (accent) */
|
||
.btn-secondary /* Sekundär-Button (grau) */
|
||
.btn-danger /* Löschen-Button (rot) */
|
||
.btn-full /* Full-Width Button */
|
||
```
|
||
|
||
**Tabs:**
|
||
```css
|
||
.tabs /* Tab-Container (segmented control) */
|
||
.tab /* Einzelner Tab */
|
||
.tab.active /* Aktiver Tab (white background, shadow) */
|
||
```
|
||
|
||
**Misc:**
|
||
```css
|
||
.badge /* Inline-Badge (klein, rounded) */
|
||
.spinner /* Loading-Spinner (CSS-Animation) */
|
||
.empty-state /* Leerer Zustand (zentriert, muted) */
|
||
.muted /* Muted-Text (text3) */
|
||
```
|
||
|
||
### Responsive Design
|
||
|
||
**Mobile-First Approach:**
|
||
- Standard-Layout für 375px–600px (Mobile)
|
||
- Max-Width: 600px (zentriert auf Desktop)
|
||
- Bottom-Navigation für Mobile (64px hoch)
|
||
- Touch-optimierte Button-Größen (min 44px)
|
||
|
||
**Bottom Navigation:**
|
||
```css
|
||
.bottom-nav {
|
||
position: fixed;
|
||
bottom: 0;
|
||
left: 50%;
|
||
transform: translateX(-50%);
|
||
width: 100%;
|
||
max-width: 600px;
|
||
height: var(--nav-h);
|
||
z-index: 20;
|
||
}
|
||
```
|
||
|
||
**Safe Area (iPhone):**
|
||
```css
|
||
padding-bottom: env(safe-area-inset-bottom, 0); /* Notch-Handling */
|
||
```
|
||
|
||
**Desktop-Optimierung:**
|
||
- App zentriert mit max-width: 600px
|
||
- Kein responsives Layout für >600px (bewusst Mobile-optimiert)
|
||
|
||
---
|
||
|
||
## PWA-Konfiguration
|
||
|
||
### Service Worker
|
||
|
||
**Location:** `frontend/public/service-worker.js`
|
||
|
||
**Cache-Strategie:**
|
||
- **Static Assets:** Cache-First (HTML, CSS, JS, Icons)
|
||
- **API-Calls:** Network-First mit Fallback
|
||
- **Photos:** Cache-First mit Expiry
|
||
|
||
**Registrierung:**
|
||
```javascript
|
||
// frontend/src/main.jsx
|
||
if ('serviceWorker' in navigator) {
|
||
navigator.serviceWorker.register('/service-worker.js')
|
||
}
|
||
```
|
||
|
||
### Manifest
|
||
|
||
**Location:** `frontend/public/manifest.json`
|
||
|
||
**Wichtige Felder:**
|
||
```json
|
||
{
|
||
"name": "Mitai Jinkendo",
|
||
"short_name": "Mitai",
|
||
"theme_color": "#1D9E75",
|
||
"background_color": "#f4f3ef",
|
||
"display": "standalone",
|
||
"icons": [
|
||
{ "src": "/icon-192.png", "sizes": "192x192", "type": "image/png" },
|
||
{ "src": "/icon-512.png", "sizes": "512x512", "type": "image/png" }
|
||
]
|
||
}
|
||
```
|
||
|
||
**Installation:**
|
||
- iOS: "Zum Home-Bildschirm"
|
||
- Android: "Installieren"-Prompt
|
||
- Desktop: Chrome/Edge Install-Button
|
||
|
||
---
|
||
|
||
## Chart-Bibliothek (Recharts)
|
||
|
||
**Verwendete Charts:**
|
||
|
||
| Chart-Typ | Verwendung | Seite |
|
||
|-----------|-----------|-------|
|
||
| **LineChart** | Gewicht, Körperfett, Umfänge, Vitalwerte | Dashboard, History |
|
||
| **BarChart** | Wöchentliche Ernährung, Aktivität | NutritionPage, History |
|
||
| **PieChart** | Trainingstypen-Verteilung | Dashboard, ActivityPage |
|
||
| **ScatterChart** | Korrelationen (Gewicht vs. Kalorien) | NutritionPage |
|
||
| **ComposedChart** | Multi-Axis (Gewicht + KF% kombiniert) | History |
|
||
|
||
**Standard-Konfiguration:**
|
||
```jsx
|
||
<ResponsiveContainer width="100%" height={240}>
|
||
<LineChart data={data}>
|
||
<CartesianGrid strokeDasharray="3 3" stroke="var(--border)" />
|
||
<XAxis dataKey="date" tick={{ fontSize: 11 }} stroke="var(--text3)" />
|
||
<YAxis tick={{ fontSize: 11 }} stroke="var(--text3)" />
|
||
<Tooltip contentStyle={{ background: 'var(--surface)', border: '1px solid var(--border)' }} />
|
||
<Line type="monotone" dataKey="weight" stroke="var(--accent)" strokeWidth={2} dot={false} />
|
||
</LineChart>
|
||
</ResponsiveContainer>
|
||
```
|
||
|
||
**Farb-Schema:**
|
||
- Gewicht: `var(--accent)` (#1D9E75)
|
||
- Körperfett: `#D85A30` (Danger)
|
||
- Umfänge: Individual Colors (siehe `CIRCUMFERENCE_GUIDE`)
|
||
- 7-Tage-Durchschnitt: `var(--accent-dark)` (#0a5c43, gestrichelt)
|
||
|
||
---
|
||
|
||
## Feature Usage Badges (v9c Phase 3)
|
||
|
||
**Zweck:** Sichtbarkeit der Feature-Limits direkt in der UI
|
||
|
||
**Komponenten:**
|
||
|
||
### 1. UsageBadge (`components/UsageBadge.jsx`)
|
||
|
||
**Inline-Badge mit Limit-Status:**
|
||
```jsx
|
||
<UsageBadge feature="weight_entries" />
|
||
// Rendert: "5/10" (grün) oder "10/10 🔒" (rot)
|
||
```
|
||
|
||
**Logik:**
|
||
```javascript
|
||
const { allowed, used, limit, remaining } = await api.getFeatureUsage()
|
||
const color = allowed ? 'var(--accent)' : 'var(--danger)'
|
||
const text = limit === null ? '∞' : `${used}/${limit}`
|
||
```
|
||
|
||
**Verwendung:** In Buttons (z.B. "Speichern 5/10")
|
||
|
||
### 2. FeatureUsageOverview (`components/FeatureUsageOverview.jsx`)
|
||
|
||
**Tabelle mit allen Features:**
|
||
```jsx
|
||
<FeatureUsageOverview />
|
||
```
|
||
|
||
**Darstellung:**
|
||
| Feature | Genutzt | Limit | Verbleibend | Status |
|
||
|---------|---------|-------|-------------|--------|
|
||
| Gewichtseinträge | 45 | 100 | 55 | ✓ OK |
|
||
| KI-Aufrufe | 10 | 10 | 0 | 🔒 Limit erreicht |
|
||
| Daten-Export | 1 | 5 | 4 | ✓ OK |
|
||
|
||
**Farbcodierung:**
|
||
- Grün: `allowed === true`
|
||
- Rot: `allowed === false`
|
||
- Gelb: `remaining < 10% && allowed`
|
||
|
||
**Location:** Settings-Seite (Tab "Quota")
|
||
|
||
---
|
||
|
||
## Routing-Architektur
|
||
|
||
### App-Struktur (`App.jsx`)
|
||
|
||
```
|
||
AuthProvider
|
||
↓
|
||
ProfileProvider
|
||
↓
|
||
BrowserRouter
|
||
↓
|
||
AppShell
|
||
├── Public Routes (ohne Auth)
|
||
│ ├── /register → Register
|
||
│ ├── /verify?token=... → Verify
|
||
│ └── /reset-password?token=... → ResetPassword
|
||
│
|
||
├── Auth Gates
|
||
│ ├── authLoading → Spinner
|
||
│ ├── needsSetup → SetupScreen
|
||
│ └── !session → LoginScreen
|
||
│
|
||
└── Authenticated Routes
|
||
├── Header (Logo + Logout + Avatar)
|
||
├── Main (Scrollable Content)
|
||
│ └── Routes (31 Seiten)
|
||
└── Nav (Bottom Navigation, 5 Items)
|
||
```
|
||
|
||
### Navigation-Items
|
||
|
||
```javascript
|
||
const links = [
|
||
{ to: '/', icon: <LayoutDashboard/>, label: 'Übersicht' },
|
||
{ to: '/capture', icon: <PlusSquare/>, label: 'Erfassen' },
|
||
{ to: '/history', icon: <TrendingUp/>, label: 'Verlauf' },
|
||
{ to: '/analysis', icon: <BarChart2/>, label: 'Analyse' },
|
||
{ to: '/settings', icon: <Settings/>, label: 'Einst.' },
|
||
]
|
||
```
|
||
|
||
**Besonderheit:** Active-State via React Router `NavLink` (`isActive` prop)
|
||
|
||
### Route-Guards
|
||
|
||
**Auth-Schutz:**
|
||
```jsx
|
||
if (!session) return <LoginScreen/>
|
||
```
|
||
|
||
**Admin-Schutz:**
|
||
```jsx
|
||
// In AdminPanel-Seiten:
|
||
const { isAdmin } = useAuth()
|
||
if (!isAdmin) return <div>Nur für Admins</div>
|
||
```
|
||
|
||
**Setup-Check:**
|
||
```jsx
|
||
if (needsSetup) return <SetupScreen/>
|
||
```
|
||
|
||
---
|
||
|
||
## Performance-Optimierungen
|
||
|
||
### 1. Code Splitting
|
||
|
||
**React.lazy() für Admin-Seiten:**
|
||
```javascript
|
||
const AdminPanel = React.lazy(() => import('./pages/AdminPanel'))
|
||
```
|
||
|
||
**Vorteil:** Admin-Code nicht im Initial Bundle (~80 KB gespart)
|
||
|
||
### 2. Memoization
|
||
|
||
**useMemo für teure Berechnungen:**
|
||
```javascript
|
||
const stats = useMemo(() => {
|
||
return calculateStats(data)
|
||
}, [data])
|
||
```
|
||
|
||
**Verwendung:** Chart-Daten-Transformation, Aggregationen
|
||
|
||
### 3. Lazy Loading
|
||
|
||
**Images:**
|
||
```jsx
|
||
<img src={photoUrl} loading="lazy" />
|
||
```
|
||
|
||
**Charts:**
|
||
- Nur sichtbare Charts rendern (Intersection Observer in Planung)
|
||
|
||
### 4. API-Call-Batching
|
||
|
||
**Parallel-Loading:**
|
||
```javascript
|
||
const [stats, insights, weights] = await Promise.all([
|
||
api.getStats(),
|
||
api.latestInsights(),
|
||
api.listWeight(30),
|
||
])
|
||
```
|
||
|
||
**Verwendung:** Dashboard initial load
|
||
|
||
---
|
||
|
||
## Error-Handling
|
||
|
||
### 1. API-Fehler
|
||
|
||
**Pattern in allen Seiten:**
|
||
```javascript
|
||
const [error, setError] = useState(null)
|
||
|
||
try {
|
||
const data = await api.someEndpoint()
|
||
setData(data)
|
||
} catch(e) {
|
||
setError(e.message) // api.js parsed bereits {detail: "..."}
|
||
} finally {
|
||
setLoading(false)
|
||
}
|
||
```
|
||
|
||
**Anzeige:**
|
||
```jsx
|
||
{error && (
|
||
<div style={{
|
||
background: 'rgba(216,90,48,0.1)',
|
||
color: 'var(--danger)',
|
||
padding: '10px 14px',
|
||
borderRadius: 8,
|
||
border: '1px solid rgba(216,90,48,0.2)'
|
||
}}>
|
||
{error}
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
### 2. Network-Fehler
|
||
|
||
**Offline-Detection:**
|
||
```javascript
|
||
useEffect(() => {
|
||
const handleOnline = () => setOnline(true)
|
||
const handleOffline = () => setOnline(false)
|
||
window.addEventListener('online', handleOnline)
|
||
window.addEventListener('offline', handleOffline)
|
||
return () => {
|
||
window.removeEventListener('online', handleOnline)
|
||
window.removeEventListener('offline', handleOffline)
|
||
}
|
||
}, [])
|
||
```
|
||
|
||
**Anzeige:** Banner "Keine Internetverbindung – Änderungen werden gespeichert sobald Online"
|
||
|
||
### 3. Form-Validierung
|
||
|
||
**Client-Side:**
|
||
```javascript
|
||
if (!weight || weight < 20 || weight > 300) {
|
||
setError('Gewicht zwischen 20 und 300 kg')
|
||
return
|
||
}
|
||
```
|
||
|
||
**Server-Side:**
|
||
- Backend wirft `HTTPException(400, detail="...")` → Frontend zeigt `detail`
|
||
|
||
---
|
||
|
||
## Besonderheiten & Design-Entscheidungen
|
||
|
||
### 1. Warum kein TypeScript?
|
||
|
||
**Entscheidung:** Bewusst auf TypeScript verzichtet
|
||
|
||
**Gründe:**
|
||
- Schnellere Prototyping-Geschwindigkeit
|
||
- Weniger Build-Komplexität
|
||
- JSDoc-Kommentare für Dokumentation ausreichend
|
||
- Type Safety durch Backend (Pydantic validiert alle Inputs)
|
||
|
||
### 2. Warum Context statt Redux?
|
||
|
||
**Entscheidung:** Context API ausreichend für diesen Use-Case
|
||
|
||
**Gründe:**
|
||
- Nur 2 globale States (Auth + Profile)
|
||
- Kein komplexes State-Update-Pattern nötig
|
||
- Weniger Boilerplate
|
||
- Performance ausreichend (keine häufigen Re-Renders)
|
||
|
||
**Hinweis:** Bei >5 Contexts würde Redux Sinn machen
|
||
|
||
### 3. Warum Custom Markdown statt remark/rehype?
|
||
|
||
**Entscheidung:** Eigener Markdown-Renderer (134 Zeilen)
|
||
|
||
**Gründe:**
|
||
- Nur Subset von Markdown benötigt (Headings, Bold, Listen)
|
||
- remark + rehype + plugins = ~200 KB Bundle-Size
|
||
- Custom-Renderer = 0 Dependencies
|
||
- Full Control über Styling
|
||
|
||
### 4. Warum Recharts statt Chart.js?
|
||
|
||
**Entscheidung:** Recharts für alle Charts
|
||
|
||
**Gründe:**
|
||
- React-native (kein Canvas, sondern SVG)
|
||
- Declarative API passt zu React
|
||
- Responsive by default
|
||
- Kleineres Bundle als Chart.js
|
||
|
||
### 5. Inline-Editing statt Modal-Forms
|
||
|
||
**Entscheidung:** Inline-Edit für alle Listen (Gewicht, Ernährung, Vitalwerte)
|
||
|
||
**Pattern:**
|
||
```javascript
|
||
const [editingId, setEditingId] = useState(null)
|
||
|
||
{entries.map(e => (
|
||
editingId === e.id
|
||
? <EditForm entry={e} onSave={...} onCancel={...} />
|
||
: <ViewRow entry={e} onEdit={() => setEditingId(e.id)} />
|
||
))}
|
||
```
|
||
|
||
**Vorteil:**
|
||
- Keine Modal-Komponente nötig
|
||
- Besserer Mobile-UX (kein Overlay)
|
||
- Schnelleres Editing (kein Dialog öffnen)
|
||
|
||
---
|
||
|
||
## Bekannte Limitationen
|
||
|
||
### 1. Desktop-Optimierung
|
||
|
||
**Problem:** App ist Mobile-First, Desktop-Layout nicht optimiert
|
||
|
||
**Aktuell:** Max-Width 600px, zentriert auf Desktop
|
||
|
||
**Geplant (v10+):** Responsive Grid-Layout für Desktop (Sidebar + Multi-Column)
|
||
|
||
### 2. Offline-Modus
|
||
|
||
**Problem:** Service Worker cached nur Static Assets, nicht API-Responses
|
||
|
||
**Aktuell:** Offline = Keine Daten-Eingabe möglich
|
||
|
||
**Geplant (v10+):** IndexedDB für Offline-Queue
|
||
|
||
### 3. Multi-Profil-Support
|
||
|
||
**Problem:** Profile-Wechsel funktioniert, aber Session ist Single-User
|
||
|
||
**Aktuell:** Logout + Login für Profil-Wechsel
|
||
|
||
**Geplant (v9f+):** Multi-Session-Support (Switch ohne Logout)
|
||
|
||
### 4. Accessibility
|
||
|
||
**Problem:** ARIA-Labels fehlen, Keyboard-Navigation unvollständig
|
||
|
||
**Aktuell:** Maus/Touch-optimiert
|
||
|
||
**Geplant (v10+):** WCAG 2.1 AA Compliance
|
||
|
||
---
|
||
|
||
## Testing
|
||
|
||
**Aktueller Stand:** Kein automatisiertes Testing implementiert
|
||
|
||
**Geplant (v10+):**
|
||
- Unit-Tests: Vitest für utils (calc.js, interpret.js)
|
||
- Component-Tests: React Testing Library
|
||
- E2E-Tests: Playwright für kritische Flows (Login, Gewicht-Eingabe, KI-Analyse)
|
||
|
||
**Manual Testing:**
|
||
- Alle Features manuell auf iOS Safari, Android Chrome, Desktop Firefox getestet
|
||
- Regression-Tests bei jedem Deploy
|
||
|
||
---
|
||
|
||
## Zusammenfassung
|
||
|
||
**Architektur-Highlights:**
|
||
- ✅ 31 Seiten (22 User, 9 Admin)
|
||
- ✅ Context-basiertes State Management (Auth + Profile)
|
||
- ✅ Zentrale API-Schnittstelle (api.js)
|
||
- ✅ Berechnungs-Utils für Body-Metrics (calc.js, interpret.js)
|
||
- ✅ CSS-Variablen für Light/Dark Mode
|
||
- ✅ PWA mit Service Worker
|
||
- ✅ Recharts für alle Charts
|
||
- ✅ Feature Usage Badges (v9c Phase 3)
|
||
- ✅ Mobile-First Design (max-width 600px)
|
||
- ✅ Inline-Editing statt Modals
|
||
- ✅ Custom Markdown-Renderer (0 Dependencies)
|
||
|
||
**Performance:**
|
||
- Initial Bundle: ~450 KB (gzip)
|
||
- Code Splitting: Admin-Seiten lazy-loaded
|
||
- API-Call-Batching: Parallel-Loading für Dashboard
|
||
|
||
**Nächste Schritte:**
|
||
- Testing (Vitest + React Testing Library)
|
||
- Desktop-Responsive-Layout
|
||
- Offline-Modus (IndexedDB)
|
||
- Accessibility (WCAG 2.1 AA)
|