- .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
19 KiB
Architektur-Übersicht – Mitai Jinkendo
System-Überblick
Mitai Jinkendo ist eine selbst-gehostete Progressive Web App (PWA) für Körper-Tracking mit KI-gestützter Auswertung. Die Architektur folgt einem klassischen 3-Tier-Modell mit klarer Trennung von Präsentation, Business-Logik und Datenhaltung.
Internet
↓
Fritz!Box (privat.stommer.com)
↓
Synology NAS
↓
Raspberry Pi 5 (192.168.2.49)
↓
┌────────────────────────────────────┐
│ Docker Compose Environment │
├────────────────────────────────────┤
│ Frontend (React + Vite) │ Port 3002 (Prod) / 3099 (Dev)
│ Backend (FastAPI + Python) │ Port 8002 (Prod) / 8099 (Dev)
│ Database (PostgreSQL 16 Alpine) │ Port 5432 (internal)
└────────────────────────────────────┘
Tech-Stack
| Komponente | Technologie | Version | Zweck |
|---|---|---|---|
| Frontend | React | 18 | UI Framework |
| Vite | Latest | Build Tool + Dev Server | |
| React Router | 6 | Client-side Routing | |
| Lucide React | Latest | Icon Library | |
| Backend | FastAPI | Latest | REST API Framework |
| Python | 3.12 | Programmiersprache | |
| psycopg2-binary | Latest | PostgreSQL Driver | |
| bcrypt | Latest | Passwort-Hashing | |
| slowapi | Latest | Rate Limiting | |
| Database | PostgreSQL | 16 Alpine | Primäre Datenbank |
| Container | Docker | Latest | Container Runtime |
| Docker Compose | v2 | Multi-Container Orchestration | |
| KI | OpenRouter API | - | Claude Sonnet 4 |
| Anthropic API | - | Direkt-Integration (optional) | |
| SMTP | - | E-Mail-Versand (Verifikation, Reset) | |
| Infrastruktur | Raspberry Pi 5 | - | Host-System |
| Gitea | 3000 | Git Server + CI/CD | |
| Synology NAS | - | Reverse Proxy |
Deployment-Architektur
Umgebungen
| Umgebung | Domain | Branch | Ports | Deployment |
|---|---|---|---|---|
| Production | mitai.jinkendo.de | main |
3002/8002 | Auto-Deploy via Gitea |
| Development | dev.mitai.jinkendo.de | develop |
3099/8099 | Auto-Deploy via Gitea |
Git-Workflow
develop → Push → Gitea Webhook → Runner → docker-compose.dev-env.yml → dev.mitai.jinkendo.de
main → Push → Gitea Webhook → Runner → docker-compose.yml → mitai.jinkendo.de
Runner-Details:
- Läuft auf Raspberry Pi 5 (
/home/lars/gitea-runner/) - Watchtower für automatische Updates
- Docker Compose Build + Restart bei neuen Commits
Verzeichnisstruktur auf dem Server
/home/lars/docker/
├── bodytrack/ # Production
│ ├── docker-compose.yml
│ └── .env
└── bodytrack-dev/ # Development
├── docker-compose.dev-env.yml
└── .env
Externe Volumes:
bodytrack_bodytrack-data→/app/data(Backend JSON-Daten, Legacy)bodytrack_bodytrack-photos→/app/photos(Progress-Fotos)mitai_postgres_data→ PostgreSQL Datenbank
Komponenten-Übersicht
Backend-Module (26 Router)
| Router | Endpunkte | Beschreibung |
|---|---|---|
| auth | /api/auth/* |
Login, Register, Verify, Password Reset |
| profiles | /api/profiles/*, /api/profile |
Nutzerverwaltung |
| weight | /api/weight/* |
Gewichts-Tracking |
| circumference | /api/circumferences/* |
Umfangsmessungen (8 Punkte) |
| caliper | /api/caliper/* |
Hautfaltenmessungen (4 Methoden) |
| activity | /api/activity/* |
Training & Aktivitäten |
| nutrition | /api/nutrition/* |
Ernährungsdaten (Kalorien + Makros) |
| photos | /api/photos/* |
Progress-Fotos |
| insights | /api/insights/*, /api/ai/* |
KI-Auswertungen |
| prompts | /api/prompts/* |
Konfigurierbare KI-Prompts |
| admin | /api/admin/* |
Admin-Panel (Profile, Permissions) |
| stats | /api/stats |
Statistiken für Dashboard |
| exportdata | /api/export/* |
CSV/JSON/ZIP Export |
| importdata | /api/import/* |
CSV Import |
| subscription | /api/subscription/* |
Abo-Status (v9c) |
| coupons | /api/coupons/* |
Coupon-System (v9c) |
| features | /api/features/* |
Feature-Verwaltung (v9c) |
| tiers_mgmt | /api/tiers/* |
Tier-Verwaltung (v9c) |
| tier_limits | /api/tier-limits/* |
Tier-Limits Matrix (v9c) |
| user_restrictions | /api/user-restrictions/* |
User-spezifische Limits (v9c) |
| access_grants | /api/access-grants/* |
Zeitlich begrenzte Zugriffe (v9c) |
| training_types | /api/training-types/* |
Trainingstypen (v9d) |
| admin_training_types | /api/admin/training-types/* |
Trainingstypen-Admin (v9d) |
| admin_activity_mappings | /api/admin/activity-mappings/* |
Activity Mapping Admin (v9d) |
| sleep | /api/sleep/* |
Schlaf-Modul (v9d Phase 2b) |
| rest_days | /api/rest-days/* |
Ruhetage (v9d Phase 2a) |
| vitals_baseline | /api/vitals/baseline/* |
Morgenmessungen (RHR, HRV, VO2 Max) |
| blood_pressure | /api/blood-pressure/* |
Blutdruck (mehrfach täglich) |
| evaluation | /api/evaluation/* |
Training Type Profiling (v9d Phase 2 #15) |
Registrierung in main.py:
app.include_router(auth.router)
app.include_router(profiles.router)
# ... alle 26 Router
Frontend-Struktur
frontend/src/
├── App.jsx # Root-Komponente + Routing
├── app.css # Globales Design-System
├── context/
│ ├── AuthContext.jsx # Session-Management
│ └── ProfileContext.jsx # Aktives Profil
├── pages/ # 30+ Seiten
│ ├── LoginScreen.jsx # Login + Auto-Migration SHA256→bcrypt
│ ├── Register.jsx # Selbst-Registrierung (v9c)
│ ├── Verify.jsx # E-Mail-Verifizierung
│ ├── Dashboard.jsx # Übersicht mit Stats + Charts
│ ├── CaptureHub.jsx # Quick-Entry-Auswahl
│ ├── WeightScreen.jsx # Gewichts-Erfassung
│ ├── CircumScreen.jsx # Umfänge
│ ├── CaliperScreen.jsx # Caliper
│ ├── ActivityPage.jsx # Training (mit Trainingstypen v9d)
│ ├── NutritionPage.jsx # Ernährung (3-Tab: Entry/Import/Charts)
│ ├── SleepPage.jsx # Schlaf (v9d Phase 2b)
│ ├── RestDaysPage.jsx # Ruhetage (v9d Phase 2a)
│ ├── VitalsPage.jsx # Vitalwerte (3-Tab: Baseline/BP/Import)
│ ├── History.jsx # Verlauf-Charts
│ ├── Analysis.jsx # KI-Analyse + Pipeline
│ ├── SettingsPage.jsx # Einstellungen
│ ├── SubscriptionPage.jsx # Membership-UI (v9c)
│ └── Admin*.jsx # 9 Admin-Seiten
└── utils/
├── api.js # ALLE API-Calls (285 Zeilen)
├── calc.js # Body-Fat-Berechnungen
├── interpret.js # Daten-Interpretation
├── Markdown.jsx # Markdown-Renderer
└── guideData.js # Guide-Inhalte
Datenfluss
1. User Request → Auth → API → Database
User Action (Frontend)
↓
api.js (Token injiziert via hdrs())
↓
FastAPI Endpoint
↓
require_auth() Dependency (auth.py)
↓ (validiert X-Auth-Token → session)
↓
get_session() → SELECT FROM sessions JOIN profiles
↓ (returns session dict mit profile_id, role, ai_enabled, ...)
↓
Router-Funktion
↓
db.py → get_db() Context Manager
↓
psycopg2 → PostgreSQL Query (RealDictCursor)
↓
Response (JSON)
2. Feature Access Control (v9c)
Endpoint (z.B. POST /api/insights/run)
↓
check_feature_access(profile_id, 'ai_calls')
↓
Prüf-Hierarchie:
1. user_feature_restrictions (user-spezifisch)
2. tier_limits (Tier-basiert)
3. features.default_limit (Fallback)
↓
get_effective_tier(profile_id)
→ access_grants (Coupon/Trial) ODER profiles.tier
↓
user_feature_usage (aktueller Zähler)
↓
Ergebnis: {allowed: bool, limit: int, used: int, remaining: int, reason: str}
↓
Falls allowed == false → HTTP 403
Falls allowed == true → increment_feature_usage() + execute
3. KI-Pipeline (3-stufig)
User: "Analyse starten"
↓
POST /api/insights/pipeline
↓
check_feature_access('ai_pipeline')
↓
Stufe 1: Gewicht-Trend → claude-sonnet-4 (OpenRouter)
Stufe 2: Ernährung → claude-sonnet-4
Stufe 3: Gesamtanalyse → claude-sonnet-4 (mit Ergebnissen aus 1+2)
↓
Jeder Call: increment_feature_usage('ai_calls')
↓
INSERT INTO ai_insights (scope='pipeline', content=...)
↓
Return: {content: "...", usage: {...}}
Sicherheitsarchitektur
1. Passwort-Sicherheit
Hash-Verfahren:
- Aktuell: bcrypt (Salting + Work Factor)
- Legacy: SHA256 (wird beim Login automatisch zu bcrypt migriert)
Passwort-Hashing (auth.py):
def hash_pin(pin: str) -> str:
return bcrypt.hashpw(pin.encode(), bcrypt.gensalt()).decode()
def verify_pin(pin: str, stored_hash: str) -> bool:
if stored_hash.startswith('$2'): # bcrypt
return bcrypt.checkpw(pin.encode(), stored_hash.encode())
# Legacy SHA256 → auto-upgrade beim nächsten Login
return stored_hash == hashlib.sha256(pin.encode()).hexdigest()
2. Session-Management
Token-Format:
secrets.token_urlsafe(32)→ 43 Zeichen Base64-URL-safe- Gespeichert in
sessionsTabelle mitexpires_at - Standard-Lebensdauer: 30 Tage (konfigurierbar pro Profil)
Auth-Flow:
# Backend
@router.post("/api/auth/login")
def login(email, password):
profile = SELECT ... WHERE email=...
verify_pin(password, profile.pin_hash) # ✓
token = make_token()
INSERT INTO sessions (token, profile_id, expires_at)
return {"token": token, "profile": {...}}
# Frontend (AuthContext.jsx)
localStorage.setItem('mitai-jinkendo_token', token)
Auth-Middleware (auth.py):
def require_auth(x_auth_token: Optional[str] = Header(default=None)):
session = get_session(x_auth_token)
if not session:
raise HTTPException(401, "Nicht eingeloggt")
return session # dict mit profile_id, role, ai_enabled, ...
3. CORS-Konfiguration
Produktion (docker-compose.yml):
ALLOWED_ORIGINS: https://mitai.jinkendo.de
Development:
ALLOWED_ORIGINS: https://dev.mitai.jinkendo.de,http://localhost:3099
FastAPI Setup (main.py):
app.add_middleware(
CORSMiddleware,
allow_origins=os.getenv("ALLOWED_ORIGINS", "*").split(","),
allow_credentials=True,
allow_methods=["GET","POST","PUT","DELETE","OPTIONS"],
allow_headers=["*"],
)
4. Rate Limiting
Library: slowapi (Redis-freie In-Memory Rate Limiting)
Anwendung:
from slowapi import Limiter
from slowapi.util import get_remote_address
limiter = Limiter(key_func=get_remote_address)
app.state.limiter = limiter
@router.post("/auth/login")
@limiter.limit("5/minute") # Max 5 Login-Versuche pro Minute
def login(request: Request, ...):
...
Geschützte Endpoints:
/api/auth/login→ 5/minute/api/auth/register→ 3/minute- KI-Endpoints → via Feature-Limits (nicht Rate Limiting)
5. SQL Injection Protection
Parameter-Binding:
# ✅ Sicher (psycopg2 escaped automatisch):
cur.execute("SELECT * FROM profiles WHERE id = %s", (profile_id,))
# ❌ NIEMALS:
cur.execute(f"SELECT * FROM profiles WHERE id = '{profile_id}'")
RealDictCursor:
- Automatisches Escaping
- Keine String-Konkatenation
Versions-Historie
v9c (Komplett) – Production seit 21.03.2026 ✅
Features:
- Membership-System (5 Tiers: free/basic/premium/selfhosted)
- Coupon-System + Trial (14 Tage)
- Feature-Enforcement (4 Phasen):
- Phase 1: Cleanup ✅
- Phase 2: Monitoring ✅
- Phase 3: Frontend Display ✅
- Phase 4: Enforcement (HTTP 403) ✅
- Selbst-Registrierung + E-Mail-Verifizierung
- Export: CSV/JSON/ZIP
- Ernährungs-Modul erweitert (3-Tab Layout)
v9d – Phase 1b ✅
Trainingstypen-System:
- 29 Trainingstypen in 7 Kategorien
- Lernendes Mapping-System (DB-basiert)
- Apple Health Import (Deutsch + English)
- Bulk-Kategorisierung (selbstlernend)
v9d – Phase 2 ✅ (Deployed 23.03.2026)
Vitalwerte & Erholung:
- Schlaf-Modul (v9d Phase 2b):
- Tabelle
sleep_logmit JSONB sleep_segments - Schlafphasen (Deep, REM, Light, Awake)
- Apple Health CSV Import
- Tabelle
- Ruhetage (v9d Phase 2a):
- Multi-dimensionale Ruhetage (Kraft/Cardio/Entspannung)
- Quick Mode Presets + Custom Entry
- Vitalwerte erweitert (v9d Phase 2d):
- 3-Tab Architektur: Baseline (morgens) / Blutdruck (mehrfach täglich) / Import
- Baseline Vitals: Ruhepuls, HRV, VO2 Max, SpO2, Atemfrequenz
- Blutdruck: Systolisch/Diastolisch + Puls, WHO/ISH-Klassifizierung
- Context-Tagging: 8 Kontexte (nüchtern, nach Essen, Training, Stress, etc.)
- Inline-Editing: Alle Messungen direkt in der Liste bearbeitbar
- CSV Import: Omron (Deutsch) + Apple Health (Deutsch/Englisch)
v9b (PostgreSQL-Migration)
- Migration SQLite → PostgreSQL 16
- Connection Pooling (psycopg2)
- UUID statt Integer Primary Keys
- Trigger für auto-update timestamps
v9a (Basis)
- Login + Auth-Middleware
- Gewicht, Umfänge, Caliper, Ernährung, Aktivität
- KI-Analyse (6 Prompts + 3-stufige Pipeline)
- PWA + Dashboard
- Admin-Panel
Design-Entscheidungen
Warum PostgreSQL statt SQLite?
Vorteile:
- Concurrent Writes (SQLite locked bei Writes)
- Native UUID Support
- JSONB für flexible Datenstrukturen (z.B. sleep_segments)
- Trigger + Stored Procedures
- Production-ready für Self-Hosting
Migration:
- Automatisches Migrations-System (
db_init.py) - Rollback-fähig via SQL-Dateien
- Tracking in
schema_migrationsTabelle
Warum FastAPI statt Flask/Django?
Vorteile:
- Async Support (für zukünftige WebSocket-Features)
- Automatic OpenAPI Docs
- Type Hints → automatische Validierung
- Dependency Injection (z.B.
require_auth) - Performance (ASGI statt WSGI)
Warum React statt Vue/Svelte?
Entscheidung:
- Bekanntes Ecosystem
- Context API ausreichend (kein Redux nötig)
- Gute PWA-Integration
- React Router maturity
Warum kein TypeScript?
Entscheidung (bewusst):
- Schnellere Prototyping-Geschwindigkeit
- Weniger Build-Komplexität
- Dokumentation via JSDoc-Kommentare
- Type Safety durch Backend (Pydantic)
Warum Connection Pooling?
Problem ohne Pooling:
- Jeder API-Call → neue DB-Verbindung → Overhead
- PostgreSQL max_connections erreicht bei vielen gleichzeitigen Requests
Lösung:
_pool = psycopg2.pool.SimpleConnectionPool(minconn=1, maxconn=10)
@contextmanager
def get_db():
conn = _pool.getconn()
try:
yield conn
conn.commit()
except:
conn.rollback()
raise
finally:
_pool.putconn(conn)
Vorteil:
- Max 10 gleichzeitige Verbindungen
- Automatisches Recycling
- Auto-Commit/Rollback
Bekannte Limitationen & Workarounds
1. Docker Build auf Raspberry Pi
Problem: apt-get install postgresql-client hängt 30+ Minuten
Lösung: Reine Python-Lösung mit psycopg2-binary (keine System-Dependencies)
2. Apple Health CSV-Import
Problem: Dezimalwerte werden als String exportiert ("65.0" statt 65)
Lösung:
def safe_float(val):
try:
return float(val) if val else None
except:
return None
3. Bun Crash nach langen Claude Code Sessions
Problem: Bun (JS Runtime für Claude Code CLI) crashed bei >30 Min Sessions
Workaround: Bei komplexen Tasks früher committen + neue Session starten
4. dayjs.week() ohne Plugin
Problem: dayjs().week() existiert nicht ohne isoWeek-Plugin
Lösung: Native ISO-Wochenberechnung:
const isoWeek = (d => Math.ceil(((new Date(d.setDate(d.getDate()+4-(d.getDay()||7)))-
new Date(d.getFullYear(),0,1))/86400000+1)/7))(new Date(date))
Performance-Optimierungen
1. Frontend
- Code Splitting: React.lazy() für Admin-Seiten (nicht initial geladen)
- Image Loading:
loading="lazy"für Photos - CSS: Inline Critical CSS, kein CSS-in-JS Overhead
- PWA Cache: Service Worker cached statische Assets
2. Backend
- Connection Pooling: Max 10 Connections statt unlimited
- Query Optimization: Indizes auf häufig abgefragte Spalten
- Lazy Loading: KI-Analysen nur auf Abruf, nicht im Dashboard
3. Database
Indizes:
CREATE INDEX idx_weight_log_profile_date ON weight_log(profile_id, date DESC);
CREATE INDEX idx_sessions_expires_at ON sessions(expires_at);
CREATE INDEX idx_ai_insights_profile_scope ON ai_insights(profile_id, scope, created DESC);
Unique Constraints:
CREATE UNIQUE INDEX idx_weight_log_profile_date_unique ON weight_log(profile_id, date);
Monitoring & Logging
1. Feature Usage Logging (v9c Phase 2)
Format: JSON Lines (JSONL)
Speicherort: /app/logs/feature-usage.log
Beispiel:
{"timestamp":"2026-03-23T10:15:30Z","profile_id":"uuid-here","feature":"ai_calls","allowed":true,"limit":10,"used":3,"remaining":7}
{"timestamp":"2026-03-23T10:16:00Z","profile_id":"uuid-here","feature":"data_export","allowed":false,"limit":0,"used":0,"remaining":0,"reason":"limit_exceeded"}
Auswertung:
cat feature-usage.log | jq -s 'group_by(.feature) | map({feature: .[0].feature, total: length})'
2. Health Checks
Database:
healthcheck:
test: ["CMD-SHELL", "pg_isready -U mitai_prod"]
interval: 10s
API:
@app.get("/")
def root():
return {"status": "ok", "service": "mitai-jinkendo", "version": "v9c-dev"}
3. Error Handling
Einheitliches Format:
raise HTTPException(status_code=404, detail="Eintrag nicht gefunden")
# → {"detail": "Eintrag nicht gefunden"}
Frontend (api.js):
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)
}
}
Zusammenfassung
Architektur-Highlights:
- ✅ Modulare Router-Struktur (26 Module, single responsibility)
- ✅ Connection Pooling (max 10 concurrent DB connections)
- ✅ Token-basiertes Auth + bcrypt
- ✅ Feature Access Control mit Tier-System
- ✅ Docker Compose für einfaches Deployment
- ✅ Auto-Deploy via Gitea Webhooks
- ✅ PostgreSQL mit Migrations-System
- ✅ PWA mit Service Worker
- ✅ API-First Prinzip (Backend = Single Source of Truth)
Nächste Schritte (v9e+):
- Ziele-Modul (Goal Tracking)
- HF-Zonen + Erholungsstatus
- Stripe-Integration
- Connectoren (Garmin, Withings, etc.)
- Meditation + Selbstwahrnehmung