mitai-jinkendo/.claude/docs/technical/FRONTEND.md
Lars 7940dc7560 docs: Struktur .claude/docs versionieren, working/, Gitea-Index, Regeln
- .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
2026-04-08 13:01:49 +02:00

924 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 375px600px (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)