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

28 KiB
Raw Permalink Blame 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:

{
  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:

{
  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:

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-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:
    {
      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 375px600px (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 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:

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)