- .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
28 KiB
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-CheckStatCard– Statistik-Karte mit Delta-AnzeigePill– Status-Pill mit Tooltip (WHR, WHtR, KF, Protein Ø7T)
SettingsPage.jsx:
ProfileForm– Formular für Profil-Bearbeitung
NutritionPage.jsx:
EntryTab– Manuelle Eingabe + CSV-ImportImportHistoryTab– Import-Historie mit GruppierungChartsTab– Korrelationen + Wochendaten
VitalsPage.jsx:
BaselineTab– Morgenmessungen (RHR, HRV, VO2 Max, SpO2)BloodPressureTab– Blutdruck mehrfach täglich + Context-TaggingImportTab– CSV-Import (Omron Deutsch, Apple Health)
Context / State Management
1. AuthContext (frontend/src/context/AuthContext.jsx)
Verantwortlichkeit: Session-Management + Login/Logout
State:
{
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öschungsetAuthFromToken(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 !== 0canExport–session.profile.export_enabled !== 0
Storage:
localStorage.bodytrack_token– Auth-TokenlocalStorage.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:
{
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-TokenHeader) - Automatisches Profile-ID-Injection (
X-Profile-IdHeader, derzeit deprecated) - Einheitliche Fehlerbehandlung (parst
{detail: "..."}aus Backend) - Typed-like API (alle Methoden dokumentiert)
Beispiel:
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:
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:
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-Punktdurnin– Durnin-Womersley 4-Punktparrillo– 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)
- WHR (Waist-Hip-Ratio):
- 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:
{ 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:
<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:
: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:
--nav-h: 64px; /* Bottom Navigation Höhe */
--header-h: 52px; /* App-Header Höhe */
Utility Classes
Cards:
.card /* Standard-Card (white background, border, rounded) */
.card-title /* Card-Überschrift (uppercase, small, muted) */
Stats:
.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:
.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:
.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:
.tabs /* Tab-Container (segmented control) */
.tab /* Einzelner Tab */
.tab.active /* Aktiver Tab (white background, shadow) */
Misc:
.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:
.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):
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:
// frontend/src/main.jsx
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/service-worker.js')
}
Manifest
Location: frontend/public/manifest.json
Wichtige Felder:
{
"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:
<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:
<UsageBadge feature="weight_entries" />
// Rendert: "5/10" (grün) oder "10/10 🔒" (rot)
Logik:
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:
<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
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:
if (!session) return <LoginScreen/>
Admin-Schutz:
// In AdminPanel-Seiten:
const { isAdmin } = useAuth()
if (!isAdmin) return <div>Nur für Admins</div>
Setup-Check:
if (needsSetup) return <SetupScreen/>
Performance-Optimierungen
1. Code Splitting
React.lazy() für Admin-Seiten:
const AdminPanel = React.lazy(() => import('./pages/AdminPanel'))
Vorteil: Admin-Code nicht im Initial Bundle (~80 KB gespart)
2. Memoization
useMemo für teure Berechnungen:
const stats = useMemo(() => {
return calculateStats(data)
}, [data])
Verwendung: Chart-Daten-Transformation, Aggregationen
3. Lazy Loading
Images:
<img src={photoUrl} loading="lazy" />
Charts:
- Nur sichtbare Charts rendern (Intersection Observer in Planung)
4. API-Call-Batching
Parallel-Loading:
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:
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:
{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:
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:
if (!weight || weight < 20 || weight > 300) {
setError('Gewicht zwischen 20 und 300 kg')
return
}
Server-Side:
- Backend wirft
HTTPException(400, detail="...")→ Frontend zeigtdetail
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:
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)