mitai-jinkendo/.claude/docs/technical/FRONTEND.md

924 lines
28 KiB
Markdown
Raw Normal View History

# 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)