mitai-jinkendo/CLAUDE.md
Lars 91c8a5332f
All checks were successful
Deploy Development / deploy (push) Successful in 55s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
docs: update v9c status and document known issue
Phase 2 Backend complete:
-  11 new tables (Feature-Registry Pattern)
-  Feature-access middleware
-  7 new routers, 30+ endpoints
-  Tested on dev, all endpoints functional

Known issue documented:
- Admin user creation missing email field (workaround available)

Phase 3 (Frontend UI) remains TODO.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-19 14:57:57 +01:00

1135 lines
36 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.

# Mitai Jinkendo Entwickler-Kontext für Claude Code
## Projekt-Übersicht
**Mitai Jinkendo** (身体 Jinkendo) ist eine selbst-gehostete PWA für Körper-Tracking (Gewicht, Körperfett, Umfänge, Ernährung, Aktivität) mit KI-Auswertung. Teil der **Jinkendo**-App-Familie (人拳道 Der menschliche Weg der Kampfkunst).
**Produktfamilie:** mitai · miken · ikigai · shinkan · kenkou (alle unter jinkendo.de)
## Tech-Stack
| Komponente | Technologie | Version |
|-----------|-------------|---------|
| Frontend | React 18 + Vite + PWA | Node 20 |
| Backend | FastAPI (Python) | Python 3.12 |
| Datenbank | PostgreSQL 16 (Alpine) | v9b |
| Container | Docker + Docker Compose | - |
| Webserver | nginx (Reverse Proxy) | Alpine |
| Auth | Token-basiert + bcrypt | - |
| KI | OpenRouter API (claude-sonnet-4) | - |
## Ports
| Service | Prod | Dev |
|---------|------|-----|
| Frontend | 3002 | 3099 |
| Backend | 8002 | 8099 |
## Verzeichnisstruktur
```
mitai-jinkendo/
├── backend/
│ ├── main.py # FastAPI App Setup + Router Registration (~75 Zeilen)
│ ├── db.py # PostgreSQL Connection Pool + Helpers
│ ├── auth.py # Auth Functions (hash, verify, sessions)
│ ├── models.py # Pydantic Models (11 Models)
│ ├── routers/ # Modular Endpoint Structure (14 Router)
│ │ ├── auth.py # Login, Logout, Password Reset (7 Endpoints)
│ │ ├── profiles.py # Profile CRUD + Current User (7 Endpoints)
│ │ ├── weight.py # Weight Tracking (5 Endpoints)
│ │ ├── circumference.py # Body Measurements (4 Endpoints)
│ │ ├── caliper.py # Skinfold Tracking (4 Endpoints)
│ │ ├── activity.py # Workout Logging + CSV Import (6 Endpoints)
│ │ ├── nutrition.py # Nutrition + FDDB Import (4 Endpoints)
│ │ ├── photos.py # Progress Photos (3 Endpoints)
│ │ ├── insights.py # AI Analysis + Pipeline (8 Endpoints)
│ │ ├── prompts.py # AI Prompt Management (2 Endpoints)
│ │ ├── admin.py # User Management (7 Endpoints)
│ │ ├── stats.py # Dashboard Stats (1 Endpoint)
│ │ ├── exportdata.py # CSV/JSON/ZIP Export (3 Endpoints)
│ │ └── importdata.py # ZIP Import (1 Endpoint)
│ ├── requirements.txt
│ └── Dockerfile
├── frontend/
│ ├── src/
│ │ ├── App.jsx # Root, Auth-Gates, Navigation
│ │ ├── app.css # Globale Styles, CSS-Variablen
│ │ ├── context/
│ │ │ ├── AuthContext.jsx # Session, Login, Logout
│ │ │ └── ProfileContext.jsx # Aktives Profil
│ │ ├── pages/ # Alle Screens
│ │ └── utils/
│ │ ├── api.js # Alle API-Calls (injiziert Token automatisch)
│ │ ├── calc.js # Körperfett-Formeln
│ │ ├── interpret.js # Regelbasierte Auswertung
│ │ ├── Markdown.jsx # Eigener MD-Renderer
│ │ └── guideData.js # Messanleitungen
│ └── public/ # Icons (Jinkendo Ensō-Logo)
├── .gitea/workflows/
│ ├── deploy-prod.yml # Auto-Deploy bei Push auf main
│ ├── deploy-dev.yml # Auto-Deploy bei Push auf develop
│ └── test.yml # Build-Test bei jedem Push
├── docker-compose.yml # Produktion (Ports 3002/8002)
├── docker-compose.dev-env.yml # Development (Ports 3099/8099)
└── CLAUDE.md # Diese Datei
```
## Aktuelle Version: v9b
### Was implementiert ist:
- ✅ Multi-User mit E-Mail + Passwort Login (bcrypt)
- ✅ Auth-Middleware auf ALLE Endpoints (60+ Endpoints geschützt)
- ✅ Rate Limiting (Login: 5/min, Reset: 3/min)
- ✅ CORS konfigurierbar via ALLOWED_ORIGINS in .env
- ✅ Admin/User Rollen, KI-Limits, Export-Berechtigungen
- ✅ Gewicht, Umfänge, Caliper (4 Formeln), Ernährung, Aktivität
- ✅ FDDB CSV-Import (Ernährung), Apple Health CSV-Import (Aktivität)
- ✅ KI-Analyse: 6 Einzel-Prompts + 3-stufige Pipeline (parallel)
- ✅ Konfigurierbare Prompts mit Template-Variablen (Admin kann bearbeiten)
- ✅ Verlauf mit 5 Tabs + Zeitraumfilter + KI pro Sektion
- ✅ Dashboard mit Kennzahlen, Zielfortschritt, Combo-Chart
- ✅ Assistent-Modus (Schritt-für-Schritt Messung)
- ✅ PWA (iPhone Home Screen), Jinkendo Ensō-Logo
- ✅ E-Mail (SMTP) für Password-Recovery
- ✅ Admin-Panel: User verwalten, KI-Limits, E-Mail-Test, PIN/Email setzen
- ✅ Multi-Environment: Prod (mitai.jinkendo.de) + Dev (dev.mitai.jinkendo.de)
- ✅ Gitea CI/CD mit Auto-Deploy auf Raspberry Pi 5
- ✅ PostgreSQL 16 Migration (vollständig von SQLite migriert)
- ✅ Export: CSV, JSON, ZIP (mit Fotos)
- ✅ Automatische SQLite→PostgreSQL Migration bei Container-Start
-**Modulare Backend-Architektur**: 14 Router-Module, main.py von 1878→75 Zeilen (-96%)
### Was in v9c kommt: Subscription & Coupon Management System
**Phase 1 (DB-Schema): ✅ DONE**
**Phase 2 (Backend API): ✅ DONE**
**Phase 3 (Frontend UI): 🔲 TODO**
**Core Features (Backend):**
- ✅ DB-Schema (11 neue Tabellen, Feature-Registry Pattern)
- ✅ Feature-Access Middleware (check_feature_access, increment_feature_usage)
- ✅ Flexibles Tier-System (free/basic/premium/selfhosted) - Admin-editierbar via API
-**Coupon-System** (3 Typen: single_use, period, wellpass)
- ✅ Coupon-Stacking-Logik (Pause + Resume bei Wellpass-Override)
- ✅ Access-Grant-System (zeitlich begrenzte Zugriffe mit Quelle-Tracking)
- ✅ User-Activity-Log (JSONB details)
- ✅ User-Stats (Aggregierte Statistiken)
- ✅ Individuelle User-Restrictions (Admin kann Limits pro User setzen)
- ✅ 7 neue Router, 30+ neue Endpoints (subscription, coupons, features, tiers, tier-limits, user-restrictions, access-grants)
**Frontend TODO (Phase 3):**
- 🔲 Selbst-Registrierung mit E-Mail-Verifizierung (Pflicht)
- 🔲 Trial-System UI (Dauer konfigurierbar, auto-start nach E-Mail-Verifikation)
- 🔲 Admin Matrix-Editor (Tier x Feature Limits)
- 🔲 Admin Coupon-Manager (CRUD, Redemption-Historie)
- 🔲 Admin User-Restrictions UI
- 🔲 User Subscription-Info Page
- 🔲 User Coupon-Einlösung UI
- 🔲 App-Settings Admin-Panel (globale Konfiguration)
**E-Mail Templates (v9c):**
- 🔲 Registrierung + E-Mail-Verifizierung
- 🔲 Einladungslink
- 🔲 Passwort-Reset (bereits vorhanden)
**Spätere Features (v9d/v9e):**
- 🔲 Bonus-System (Login-Streaks → Punkte → Geschenk-Coupons)
- 🔲 Trial-Reminder-E-Mails (3 Tage vor Ablauf)
- 🔲 Monatliches Nutzungs-Summary per E-Mail
- 🔲 Self-Service Upgrade (Stripe-Integration)
- 🔲 Partner-Verwaltung (Wellpass, Hansefit, etc.)
- 🔲 Admin-Benachrichtigungen (neue Registrierungen, etc.)
### Was in v9d kommt:
- 🔲 Bonus-System & Gamification (Streaks, Achievements)
- 🔲 Stripe-Integration (Self-Service Upgrade, Subscriptions)
- 🔲 OAuth2-Grundgerüst für Fitness-Connectoren
- 🔲 Strava Connector
- 🔲 Withings Connector (Waage)
- 🔲 Garmin Connector
---
## v9c Architektur-Details: Subscription & Coupon System
### Datenbank-Schema (Neue Tabellen)
#### **app_settings** - Globale Konfiguration
```sql
key, value, value_type, description, updated_at, updated_by
Beispiele:
- trial_days: 14
- trial_behavior: 'downgrade' | 'lock'
- allow_registration: true/false
- default_tier_trial: 'premium'
- gift_coupons_per_month: 3
```
#### **tiers** - Tier-Konfiguration (vereinfacht)
```sql
id, slug, name, description, sort_order, active
Initial Tiers:
- free, basic, premium, selfhosted
Limits sind jetzt in tier_limits Tabelle (siehe unten)!
```
#### **features** - Feature-Registry (alle limitierbaren Features)
```sql
id, slug, name, category, description, unit
default_limit (NULL = unbegrenzt)
reset_period ('monthly' | 'daily' | 'never')
visible_in_admin, sort_order, active
Initial Features:
- weight_entries: Gewichtseinträge, default: 30, never
- circumference_entries: Umfangsmessungen, default: 30, never
- caliper_entries: Caliper-Messungen, default: 30, never
- nutrition_entries: Ernährungseinträge, default: 30, never
- activity_entries: Aktivitäten, default: 30, never
- photos: Progress-Fotos, default: 5, never
- ai_calls: KI-Analysen, default: 0, monthly
- ai_pipeline: KI-Pipeline, default: 0, monthly
- csv_import: CSV-Importe, default: 0, monthly
- data_export: Daten-Exporte, default: 0, monthly
- fitness_connectors: Fitness-Connectoren, default: 0, never
Neue Features einfach per INSERT hinzufügen - kein Schema-Change!
```
#### **tier_limits** - Limits pro Tier + Feature
```sql
id, tier_slug, feature_slug, limit_value, enabled
Beispiel Free Tier:
- ('free', 'weight_entries', 30, true)
- ('free', 'ai_calls', 0, false) -- KI deaktiviert
- ('free', 'data_export', 0, false)
Beispiel Premium:
- ('premium', 'weight_entries', NULL, true) -- unbegrenzt
- ('premium', 'ai_calls', NULL, true) -- unbegrenzt
Admin kann in UI Matrix bearbeiten: Tier x Feature
```
#### **user_feature_restrictions** - Individuelle User-Limits
```sql
id, profile_id, feature_slug, limit_value, enabled
reason, set_by (admin_id)
Überschreibt Tier-Limits für spezifische User.
Admin kann jeden User individuell einschränken oder erweitern.
```
#### **user_feature_usage** - Nutzungs-Tracking
```sql
id, profile_id, feature_slug, period_start, usage_count, last_used
Für Features mit reset_period (z.B. ai_calls monthly).
Wird automatisch zurückgesetzt am Monatsanfang.
```
#### **coupons** - Coupon-Verwaltung
```sql
code, type ('single_use' | 'multi_use_period' | 'gift')
valid_from, valid_until, grants_tier, duration_days
max_redemptions, current_redemptions
created_by, created_for, notes, active
Beispiel Single-Use:
Code: FRIEND-GIFT-XYZ, 30 Tage Premium, max 1x
Beispiel Multi-Use Period:
Code: WELLPASS-2026-03, gültig 01.03-31.03, unbegrenzte Einlösungen
```
#### **coupon_redemptions** - Einlösungs-Historie
```sql
coupon_id, profile_id, redeemed_at, access_grant_id
UNIQUE(coupon_id, profile_id) - User kann denselben Coupon nur 1x einlösen
```
#### **access_grants** - Zeitlich begrenzte Zugriffe
```sql
profile_id, granted_tier, valid_from, valid_until
source ('coupon' | 'admin_grant' | 'trial')
active (false wenn pausiert durch Wellpass-Override)
paused_at, paused_by (access_grant_id das pausiert hat)
Stacking-Logik:
- Multi-Use Period Coupon (Wellpass): pausiert andere grants
- Single-Use Coupon: stackt zeitlich (Resume nach Ablauf)
```
#### **user_activity_log** - Aktivitäts-Tracking
```sql
profile_id, activity_type, details (JSONB), ip_address, user_agent, created
Activity Types:
- login, password_change, email_change, coupon_redeemed
- tier_change, export, ai_analysis, registration
```
#### **user_stats** - Aggregierte Statistiken
```sql
profile_id, first_login, last_login, total_logins
current_streak_days, longest_streak_days, last_streak_date
total_weight_entries, total_ai_analyses, total_exports
bonus_points (später), gift_coupons_available (später)
```
#### **profiles** - Erweiterte Spalten
```sql
tier, tier_locked (Admin kann Tier festnageln)
trial_ends_at, email_verified, email_verify_token
invited_by, contract_type, contract_valid_until
stripe_customer_id (vorbereitet für v9d)
```
---
### Backend-Erweiterungen
#### Neue Router (v9c):
```
routers/tiers.py - Tier-Verwaltung (List, Edit, Create)
routers/features.py - Feature-Registry (List, Add, Edit, Delete) ⭐ NEU
routers/tier_limits.py - Tier-Limits-Matrix (Admin bearbeitet Tier x Feature) ⭐ NEU
routers/coupons.py - Coupon-System (Redeem, Admin CRUD)
routers/access_grants.py - Zugriffs-Verwaltung (Current, Grant, Revoke)
routers/user_admin.py - Erweiterte User-Verwaltung (Activity, Stats, Feature-Restrictions)
routers/settings.py - App-Einstellungen (Admin)
routers/registration.py - Registrierung + E-Mail-Verifizierung
```
#### Neue Middleware:
```python
check_feature_access(profile_id, feature_slug, action='use')
"""
Zentrale Feature-Access-Prüfung.
Hierarchie:
1. User-Restriction (höchste Priorität)
2. Tier-Limit
3. Feature-Default
Returns: {'allowed': bool, 'limit': int, 'used': int, 'remaining': int, 'reason': str}
"""
increment_feature_usage(profile_id, feature_slug)
"""
Inkrementiert Nutzungszähler.
Berücksichtigt reset_period (monthly, daily, never).
"""
log_activity(profile_id, activity_type, details=None)
"""
Loggt User-Aktivitäten in user_activity_log.
"""
```
#### Hintergrund-Tasks (Cron):
```python
check_expired_access() # Täglich 00:00 - Trial/Coupon-Ablauf prüfen
reset_monthly_limits() # 1. jeden Monats - AI-Calls zurücksetzen
update_user_streaks() # Täglich 23:59 - Login-Streaks aktualisieren
```
---
### Zugriffs-Hierarchie
```
Effektiver Tier wird ermittelt durch (Priorität absteigend):
1. Admin-Override (tier_locked=true) → nutzt profiles.tier
2. Aktiver access_grant (nicht pausiert, valid_until > now)
3. Trial (trial_ends_at > now)
4. Base tier (profiles.tier)
Wellpass-Override-Logik:
- User hat Single-Use Coupon (20 Tage verbleibend)
- User löst Wellpass-Coupon ein (gültig bis 31.03)
- Single-Use access_grant wird pausiert (active=false, paused_by=wellpass_grant_id)
- Nach Wellpass-Ablauf: Single-Use wird reaktiviert (noch 20 Tage)
```
---
### Tier-Limits & Feature-Gates
**Daten-Sichtbarkeit bei Downgrade:**
- Frontend: Buttons/Features ausblenden (Export, KI, Import)
- Backend: API limitiert Rückgabe (z.B. nur letzte 30 Gewichtseinträge bei free)
- Daten bleiben erhalten, werden nur versteckt
- Bei Upgrade wieder sichtbar
**Feature-Checks:**
```python
# Beispiel: Gewicht-Eintrag erstellen
@check_feature_limit('weight', 'create')
def create_weight_entry():
# Prüft: Hat User max_weight_entries erreicht?
# Falls ja: HTTPException 403 "Limit erreicht - Upgrade erforderlich"
```
---
### Frontend-Erweiterungen
#### Neue Seiten:
```
RegisterPage.jsx - Registrierung (Name, E-Mail, Passwort)
VerifyEmailPage.jsx - E-Mail-Verifizierung (Token aus URL)
RedeemCouponPage.jsx - Coupon-Eingabe (oder Modal)
AdminCouponsPage.jsx - Coupon-Verwaltung (Admin)
AdminTiersPage.jsx - Tier-Verwaltung (CRUD) (Admin)
AdminFeaturesPage.jsx - Feature-Registry (List, Add, Edit) ⭐ NEU
AdminTierLimitsPage.jsx - Tier x Feature Matrix (bearbeiten) ⭐ NEU
AdminUserRestrictionsPage.jsx - User-spezifische Limits (bearbeiten) ⭐ NEU
AdminSettingsPage.jsx - App-Einstellungen (Admin)
```
#### Neue Komponenten:
```jsx
<TierBadge tier="premium" /> // Tier-Anzeige mit Icon
<FeatureGate feature="ai_calls">...</> // Feature-basierte Sichtbarkeit ⭐ GEÄNDERT
<AccessStatus /> // "Trial endet in 5 Tagen" Banner
<CouponInput onRedeem={...} /> // Coupon-Eingabefeld
<ActivityTimeline activities={...} /> // User-Activity-Log
<FeatureLimitBadge feature="ai_calls" /> // "5/10 verwendet" Anzeige ⭐ NEU
<TierLimitsMatrix tiers={...} features={...}/> // Matrix-Editor ⭐ NEU
<StreakCounter days={7} /> // Login-Streak (später)
```
#### Erweiterte Admin-Seiten:
```
AdminUsersPage.jsx erweitert um:
- Activity-Log Button → zeigt user_activity_log
- Stats Button → zeigt user_stats
- Access-Grants Button → zeigt aktive/abgelaufene Zugriffe
- Feature-Restrictions Button → individuelle Feature-Limits setzen ⭐ GEÄNDERT
- Grant Access Button → manuell Tier-Zugriff gewähren
- Usage-Overview → zeigt user_feature_usage für alle Features ⭐ NEU
```
#### Admin-Interface-Details:
**AdminFeaturesPage.jsx** - Feature-Registry verwalten
```jsx
// Alle Features auflisten + neue hinzufügen
<FeatureList>
{features.map(f => (
<FeatureRow>
<Name>{f.name}</Name>
<Category>{f.category}</Category>
<Unit>{f.unit}</Unit>
<ResetPeriod>{f.reset_period}</ResetPeriod>
<DefaultLimit>{f.default_limit ?? '∞'}</DefaultLimit>
<Actions>
<EditButton />
<DeleteButton />
</Actions>
</FeatureRow>
))}
</FeatureList>
<AddFeatureButton />
```
**AdminTierLimitsPage.jsx** - Matrix-Editor
```jsx
// Matrix-View: Tiers (Spalten) x Features (Zeilen)
<TierLimitsMatrix>
<thead>
<tr>
<th>Feature</th>
<th>Free</th>
<th>Basic</th>
<th>Premium</th>
<th>Selfhosted</th>
</tr>
</thead>
<tbody>
<tr>
<td>Gewichtseinträge</td>
<td><Input value="30" /></td>
<td><Input value="" placeholder="∞" /></td>
<td><Input value="" placeholder="∞" /></td>
<td><Input value="" placeholder="∞" /></td>
</tr>
<tr>
<td>KI-Analysen/Monat</td>
<td><Checkbox disabled /> 0</td>
<td><Checkbox checked /> <Input value="10" /></td>
<td><Checkbox checked /> </td>
<td><Checkbox checked /> </td>
</tr>
</tbody>
</TierLimitsMatrix>
```
**AdminUserRestrictionsPage.jsx** - Individuelle User-Limits
```jsx
<UserSelect onChange={loadUser} />
<CurrentTier badge={user.tier} />
<CurrentUsage>
{features.map(f => (
<FeatureUsageRow key={f.slug}>
<Name>{f.name}</Name>
<TierLimit>{getTierLimit(user.tier, f.slug)}</TierLimit>
<CurrentUsage>{getUsage(user.id, f.slug)}</CurrentUsage>
<Override>
<Input
value={getUserRestriction(user.id, f.slug)}
placeholder="Tier-Standard"
/>
</Override>
<SaveButton />
</FeatureUsageRow>
))}
</CurrentUsage>
```
---
### E-Mail Templates (v9c)
**1. Registrierung + E-Mail-Verifizierung:**
```
Betreff: Willkommen bei Mitai Jinkendo - E-Mail bestätigen
Hallo {name},
vielen Dank für deine Registrierung bei Mitai Jinkendo!
Bitte bestätige deine E-Mail-Adresse:
{app_url}/verify-email?token={token}
Nach der Bestätigung startet dein 14-Tage Premium Trial automatisch.
Viel Erfolg bei deinem Training!
Dein Mitai Jinkendo Team
```
**2. Einladungslink (Admin):**
```
Betreff: Du wurdest zu Mitai Jinkendo eingeladen
Hallo,
{admin_name} hat dich zu Mitai Jinkendo eingeladen!
Registriere dich jetzt:
{app_url}/register?invite={token}
Du erhältst {tier} Zugriff.
Dein Mitai Jinkendo Team
```
---
### Migrations-Reihenfolge (v9c)
```
Phase 1 - DB Schema:
1. app_settings Tabelle + Initialdaten
2. tiers Tabelle + 4 Standard-Tiers
3. coupons Tabelle
4. coupon_redemptions Tabelle
5. access_grants Tabelle
6. user_activity_log Tabelle
7. user_stats Tabelle
8. user_restrictions Tabelle
9. profiles Spalten erweitern
10. Bestehende Profile migrieren (Lars → tier='selfhosted', email_verified=true)
Phase 2 - Backend:
11. Tier-System Router + Middleware
12. Registrierungs-Flow
13. Coupon-System
14. Access-Grant-Logik
15. Activity-Logging
16. Erweiterte Admin-Endpoints
Phase 3 - Frontend:
17. Registrierungs-Seiten
18. Tier-System UI-Komponenten
19. Coupon-Eingabe
20. Erweiterte Admin-Panels
21. Feature-Gates in bestehende Seiten einbauen
Phase 4 - Cron-Jobs:
22. Expired-Access-Checker
23. Monthly-Reset
24. Streak-Updater
Phase 5 - Testing & Deployment:
25. Dev-Testing
26. Prod-Deployment
```
---
## Deployment
### Infrastruktur
```
Internet → privat.stommer.com (Fritz!Box DynDNS)
→ Synology NAS (Reverse Proxy + Let's Encrypt)
→ Raspberry Pi 5 (192.168.2.49, Docker)
```
### Git Workflow
```
develop branch → Auto-Deploy → dev.mitai.jinkendo.de (Port 3099/8099)
main branch → Auto-Deploy → mitai.jinkendo.de (Port 3002/8002)
```
### Deployment-Befehle (manuell falls nötig)
```bash
# Prod
cd /home/lars/docker/bodytrack
docker compose -f docker-compose.yml build --no-cache
docker compose -f docker-compose.yml up -d
# Dev
cd /home/lars/docker/bodytrack-dev
docker compose -f docker-compose.dev-env.yml build --no-cache
docker compose -f docker-compose.dev-env.yml up -d
```
## Datenbank-Schema (PostgreSQL 16, v9b)
### Wichtige Tabellen:
- `profiles` Nutzer (role, pin_hash/bcrypt, email, auth_type, ai_enabled, tier)
- `sessions` Auth-Tokens mit Ablaufdatum
- `weight_log` Gewichtseinträge (profile_id, date, weight)
- `circumference_log` 8 Umfangspunkte
- `caliper_log` Hautfaltenmessung, 4 Methoden
- `nutrition_log` Kalorien + Makros (aus FDDB-CSV)
- `activity_log` Training (aus Apple Health oder manuell)
- `photos` Progress Photos
- `ai_insights` KI-Auswertungen (scope = prompt-slug)
- `ai_prompts` Konfigurierbare Prompts mit Templates (11 Prompts)
- `ai_usage` KI-Calls pro Tag pro Profil
**Schema-Datei:** `backend/schema.sql` (vollständiges PostgreSQL-Schema)
**Migration-Script:** `backend/migrate_to_postgres.py` (SQLite→PostgreSQL, automatisch)
## Auth-Flow (v9b)
```
Login-Screen → E-Mail + Passwort → Token im localStorage
Token → X-Auth-Token Header → Backend require_auth()
Profile-Id → aus Session (nicht aus Header!)
SHA256 Passwörter → automatisch zu bcrypt migriert beim Login
```
## API-Konventionen
- Alle Endpoints: `/api/...`
- Auth-Header: `X-Auth-Token: <token>`
- Responses: immer JSON
- Fehler: `{"detail": "Fehlermeldung"}`
- Rate Limit überschritten: HTTP 429
## Umgebungsvariablen (.env)
```
# Database (PostgreSQL)
DB_HOST=postgres
DB_PORT=5432
DB_NAME=mitai_prod
DB_USER=mitai_prod
DB_PASSWORD= # REQUIRED
# AI
OPENROUTER_API_KEY= # KI-Calls (optional, alternativ ANTHROPIC_API_KEY)
OPENROUTER_MODEL=anthropic/claude-sonnet-4
ANTHROPIC_API_KEY= # Direkte Anthropic API (optional)
# Email
SMTP_HOST= # E-Mail (für Recovery)
SMTP_PORT=587
SMTP_USER=
SMTP_PASS=
SMTP_FROM=
# App
APP_URL=https://mitai.jinkendo.de
ALLOWED_ORIGINS=https://mitai.jinkendo.de
DATA_DIR=/app/data
PHOTOS_DIR=/app/photos
ENVIRONMENT=production
```
## Wichtige Hinweise für Claude Code
1. **Ports immer 3002/8002 (Prod) oder 3099/8099 (Dev)** nie ändern
2. **npm install** (nicht npm ci) kein package-lock.json vorhanden
3. **PostgreSQL-Migrations** Schema-Änderungen in `backend/schema.sql`, dann Container neu bauen
4. **Pipeline-Prompts** haben slug-Prefix `pipeline_` nie als Einzelanalyse zeigen
5. **dayjs.week()** braucht Plugin stattdessen native JS ISO-Wochenberechnung
6. **useNavigate()** nur in React-Komponenten, nicht in Helper-Functions
7. **api.js nutzen** für alle API-Calls injiziert Token automatisch
8. **bcrypt** für alle neuen Passwort-Operationen verwenden
9. **session=Depends(require_auth)** als separater Parameter nie in Header() einbetten
10. **RealDictCursor verwenden** `get_cursor(conn)` statt `conn.cursor()` für dict-like row access
## Code-Style
- React: Functional Components, Hooks
- CSS: Inline-Styles + globale CSS-Variablen (var(--accent), var(--text1), etc.)
- API-Calls: immer über `api.js` (injiziert Token automatisch)
- Kein TypeScript (bewusst, für Einfachheit)
- Python: keine Type-Hints Pflicht, aber bei neuen Funktionen erwünscht
## Design-System
### Farben (CSS-Variablen)
```css
--accent: #1D9E75 /* Jinkendo Grün Buttons, Links, Akzente */
--accent-dark: #085041 /* Dunkelgrün Icon-Hintergrund, Header */
--accent-light: #E1F5EE /* Hellgrün Hintergründe, Badges */
--bg: /* Seitenhintergrund (hell/dunkel auto) */
--surface: /* Card-Hintergrund */
--surface2: /* Sekundäre Fläche */
--border: /* Rahmen */
--text1: /* Primärer Text */
--text2: /* Sekundärer Text */
--text3: /* Muted Text, Labels */
--danger: #D85A30 /* Fehler, Warnungen */
```
### CSS-Klassen
```css
.card /* Weißer Container, border-radius 12px, box-shadow */
.btn /* Basis-Button */
.btn-primary /* Grüner Button (#1D9E75) */
.btn-secondary /* Grauer Button */
.btn-full /* 100% Breite */
.form-input /* Eingabefeld, volle Breite */
.form-label /* Feldbezeichnung, klein, uppercase */
.form-row /* Label + Input + Unit nebeneinander */
.form-unit /* Einheit rechts (kg, cm, etc.) */
.section-gap /* margin-bottom zwischen Sektionen */
.spinner /* Ladekreis, animiert */
```
### Abstände & Größen
```
Seiten-Padding: 16px seitlich
Card-Padding: 16-20px
Border-Radius: 12px (Cards), 8px (Buttons/Inputs), 50% (Avatare)
Icon-Größe: 16-20px inline, 24px standalone
Font-Größe: 12px (Labels), 14px (Body), 16-18px (Subtitel), 20-24px (Titel)
Font-Weight: 400 (normal), 600 (semi-bold), 700 (bold)
Bottom-Padding: 80px (für Mobile-Navigation)
```
### Komponenten-Muster
**Titelzeile einer Seite:**
```jsx
<div style={{display:'flex',alignItems:'center',
justifyContent:'space-between',marginBottom:20}}>
<div style={{fontSize:20,fontWeight:700,color:'var(--text1)'}}>
Seitentitel
</div>
<button className="btn btn-primary">Aktion</button>
</div>
```
**Ladezustand:**
```jsx
if (loading) return (
<div style={{display:'flex',justifyContent:'center',padding:40}}>
<div className="spinner"/>
</div>
)
```
**Fehlerzustand:**
```jsx
if (error) return (
<div style={{color:'var(--danger)',padding:16,textAlign:'center'}}>
{error}
</div>
)
```
**Leerer Zustand:**
```jsx
{items.length === 0 && (
<div style={{textAlign:'center',padding:40,color:'var(--text3)'}}>
<div style={{fontSize:32,marginBottom:8}}>📭</div>
<div>Noch keine Einträge</div>
</div>
)}
```
**Metric Card:**
```jsx
<div className="card" style={{padding:16,textAlign:'center'}}>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:4}}>LABEL</div>
<div style={{fontSize:24,fontWeight:700,color:'var(--accent)'}}>
{value}
</div>
<div style={{fontSize:12,color:'var(--text3)'}}>Einheit</div>
</div>
```
### Jinkendo Logo-System
```
Grundelement: Ensō-Kreis (offen, Lücke 4-5 Uhr)
Farbe Ensō: #1D9E75
Hintergrund: #085041 (dunkelgrün)
Kern-Symbol: #5DCAA5 (mintgrün)
Wortmarke: Jin(light) + ken(bold #1D9E75) + do(light)
```
### Verfügbare Custom Commands
```
/deploy → Commit + Push vorbereiten
/merge-to-prod → develop → main mergen
/test → Manuelle Tests durchführen
/new-feature → Neues Feature-Template
/ui-component → Neue Komponente erstellen
/ui-page → Neue Seite erstellen
/fix-bug → Bug analysieren und beheben
/add-endpoint → Neuen API-Endpoint hinzufügen
/db-add-column → Neue DB-Spalte hinzufügen
```
## Jinkendo App-Familie & Markenarchitektur
### Philosophie
**Jinkendo** (人拳道) = Jin (人 Mensch) + Ken (拳 Faust) + Do (道 Weg)
"Der menschliche Weg der Kampfkunst" ruhig aber kraftvoll, Selbstwahrnehmung, Meditation, Zielorientiert
### App-Familie (Subdomain-Architektur)
```
mitai.jinkendo.de → Körper-Tracker (身体 = eigener Körper) ← DIESE APP
miken.jinkendo.de → Meditation (眉間 = drittes Auge)
ikigai.jinkendo.de → Lebenssinn/Ziele (生き甲斐)
shinkan.jinkendo.de → Kampfsport (真観 = wahre Wahrnehmung)
kenkou.jinkendo.de → Gesundheit allgemein (健康) für später aufsparen
```
### Registrierte Domains
- jinkendo.de, jinkendo.com, jinkendo.life alle registriert bei Strato
## v9b Detailplan Freemium Tier-System
### Tier-Modell
```
free → Selbst-Registrierung, 14-Tage Trial, eingeschränkt
basic → Kernfunktionen (Abo Stufe 1)
premium → Alles inkl. KI und Connectoren (Abo Stufe 2)
selfhosted → Lars' Heimversion, keine Einschränkungen
```
### Geplante DB-Erweiterungen (profiles Tabelle)
```sql
tier TEXT DEFAULT 'free'
trial_ends_at TEXT -- ISO datetime
sub_valid_until TEXT -- ISO datetime
email_verified INTEGER DEFAULT 0
email_verify_token TEXT
invited_by TEXT -- profile_id FK
invitation_token TEXT
```
### Tier-Limits (geplant)
| Feature | free | basic | premium | selfhosted |
|---------|------|-------|---------|------------|
| Gewicht-Einträge | 30 | unbegrenzt | unbegrenzt | unbegrenzt |
| KI-Analysen/Monat | 0 | 3 | unbegrenzt | unbegrenzt |
| Ernährung Import | ❌ | ✅ | ✅ | ✅ |
| Export | ❌ | ✅ | ✅ | ✅ |
| Fitness-Connectoren | ❌ | ❌ | ✅ | ✅ |
### Registrierungs-Flow (geplant)
```
1. Selbst-Registrierung: Name + E-Mail + Passwort
2. Auto-Trial: tier='free', trial_ends_at=now+14d
3. E-Mail-Bestätigung → email_verified=1
4. Trial läuft ab → Upgrade-Prompt
5. Einladungslinks: Admin generiert Token → direkt basic-Tier
6. Stripe Integration: später (v9b ohne Stripe, nur Tier-Logik)
```
## Infrastruktur Details
### Heimnetzwerk
```
Internet
→ Fritz!Box 7530 AX (DynDNS: privat.stommer.com)
→ Synology NAS (192.168.2.63, Reverse Proxy + Let's Encrypt)
→ Raspberry Pi 5 (192.168.2.49, Docker)
→ MiniPC (192.168.2.144, Gitea auf Port 3000)
```
### Synology Reverse Proxy Regeln
```
mitai.jinkendo.de → HTTP 192.168.2.49:3002 (Prod Frontend)
dev.mitai.jinkendo.de → HTTP 192.168.2.49:3099 (Dev Frontend)
```
### AdGuard DNS Rewrites (für internes Routing)
```
mitai.jinkendo.de → 192.168.2.63
dev.mitai.jinkendo.de → 192.168.2.63
```
### Fritz!Box DNS-Rebind Ausnahmen
```
jinkendo.de
mitai.jinkendo.de
```
### Pi Verzeichnisstruktur
```
/home/lars/docker/
├── bodytrack/ → Prod (main branch, docker-compose.yml)
└── bodytrack-dev/ → Dev (develop branch, docker-compose.dev-env.yml)
```
### Gitea Runner
```
Runner: raspberry-pi (auf Pi installiert)
Service: /etc/systemd/system/gitea-runner.service
Binary: /home/lars/gitea-runner/act_runner
```
### Container Namen
```
Prod: mitai-api, mitai-ui
Dev: dev-mitai-api, dev-mitai-ui
```
## Bekannte Probleme & Lösungen
### Admin User-Erstellung Email fehlt (v9c TODO)
**Problem:** Bei Admin-CRUD `/api/profiles` (POST) wird keine Email-Adresse abgefragt.
**Impact:** Neue User können sich nicht einloggen (Login erfordert Email).
**Workaround:** Email manuell via `/api/admin/profiles/{pid}/email` setzen.
**Fix-TODO:** POST `/api/profiles` sollte Email als optionales Feld akzeptieren.
### dayjs.week() NIEMALS verwenden
```javascript
// ❌ Falsch:
const week = dayjs(date).week()
// ✅ Richtig (ISO 8601):
const weekNum = (() => {
const dt = new Date(date)
dt.setHours(0,0,0,0)
dt.setDate(dt.getDate()+4-(dt.getDay()||7))
const y = new Date(dt.getFullYear(),0,1)
return Math.ceil(((dt-y)/86400000+1)/7)
})()
```
### session=Depends(require_auth) Korrekte Platzierung
```python
# ❌ Falsch (führt zu NameError oder ungeschütztem Endpoint):
def endpoint(x_profile_id: Optional[str] = Header(default=None, session=Depends(require_auth))):
# ✅ Richtig (separater Parameter):
def endpoint(x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth)):
```
### Recharts Bar fill=function nicht unterstützt
```jsx
// ❌ Falsch:
<Bar fill={(entry) => entry.color}/>
// ✅ Richtig:
<Bar fill="#1D9E75"/>
```
### PostgreSQL Boolean-Syntax
```python
# ❌ Falsch (SQLite-Syntax):
cur.execute("SELECT * FROM ai_prompts WHERE active=1")
# ✅ Richtig (PostgreSQL):
cur.execute("SELECT * FROM ai_prompts WHERE active=true")
```
### RealDictCursor für dict-like row access
```python
# ❌ Falsch:
cur = conn.cursor()
cur.execute("SELECT COUNT(*) FROM weight_log")
count = cur.fetchone()[0] # Tuple index
# ✅ Richtig:
cur = get_cursor(conn) # Returns RealDictCursor
cur.execute("SELECT COUNT(*) as count FROM weight_log")
count = cur.fetchone()['count'] # Dict key
```
## v9b Migration Lessons Learned
### PostgreSQL Migration (SQLite → PostgreSQL)
**Problem:** Docker Build hing 30+ Minuten bei `apt-get install postgresql-client`
**Lösung:** Alle apt-get dependencies entfernt, reine Python-Lösung mit psycopg2-binary
**Problem:** Leere date-Strings (`''`) führten zu PostgreSQL-Fehlern
**Lösung:** Migration-Script konvertiert leere Strings zu NULL für DATE-Spalten
**Problem:** Boolean-Felder (SQLite INTEGER 0/1 vs PostgreSQL BOOLEAN)
**Lösung:** Migration konvertiert automatisch, Backend nutzt `active=true` statt `active=1`
### API Endpoint Consistency (März 2026)
**Problem:** 11 kritische Endpoint-Mismatches zwischen Frontend und Backend gefunden
**Gelöst:**
- AI-Endpoints konsistent: `/api/insights/run/{slug}`, `/api/insights/pipeline`
- Password-Reset: `/api/auth/forgot-password`, `/api/auth/reset-password`
- Admin-Endpoints: `/permissions`, `/email`, `/pin` Sub-Routes
- Export: JSON + ZIP Endpoints hinzugefügt
- Prompt-Bearbeitung: PUT-Endpoint für Admins
**Tool:** Vollständiger Audit via Explore-Agent empfohlen bei größeren Änderungen
## Export/Import Spezifikation (v9c)
### ZIP-Export Struktur
```
mitai-export-{name}-{YYYY-MM-DD}.zip
├── README.txt ← Erklärung des Formats + Versionsnummer
├── profile.json ← Profildaten (ohne Passwort-Hash)
├── data/
│ ├── weight.csv ← Gewichtsverlauf
│ ├── circumferences.csv ← Umfänge (8 Messpunkte)
│ ├── caliper.csv ← Caliper-Messungen
│ ├── nutrition.csv ← Ernährungsdaten
│ └── activity.csv ← Aktivitäten
├── insights/
│ └── ai_insights.json ← KI-Auswertungen (alle gespeicherten)
└── photos/
├── {date}_{id}.jpg ← Progress-Fotos
└── ...
```
### CSV Format (alle Dateien)
```
- Trennzeichen: Semikolon (;) Excel/LibreOffice kompatibel
- Encoding: UTF-8 mit BOM (für Windows Excel)
- Datumsformat: YYYY-MM-DD
- Dezimaltrennzeichen: Punkt (.)
- Erste Zeile: Header
- Nullwerte: leer (nicht "null" oder "NULL")
```
### weight.csv Spalten
```
id;date;weight;note;source;created
```
### circumferences.csv Spalten
```
id;date;waist;hip;chest;neck;upper_arm;thigh;calf;forearm;note;created
```
### caliper.csv Spalten
```
id;date;chest;abdomen;thigh;tricep;subscapular;suprailiac;midaxillary;method;bf_percent;note;created
```
### nutrition.csv Spalten
```
id;date;meal_name;kcal;protein;fat;carbs;fiber;note;source;created
```
### activity.csv Spalten
```
id;date;name;type;duration_min;kcal;heart_rate_avg;heart_rate_max;distance_km;note;source;created
```
### profile.json Struktur
```json
{
"export_version": "2",
"export_date": "2026-03-18",
"app": "Mitai Jinkendo",
"profile": {
"name": "Lars",
"email": "lars@stommer.com",
"sex": "m",
"height": 178,
"birth_year": 1980,
"goal_weight": 82,
"goal_bf_pct": 14,
"avatar_color": "#1D9E75",
"auth_type": "password",
"session_days": 30,
"ai_enabled": true,
"tier": "selfhosted"
},
"stats": {
"weight_entries": 150,
"nutrition_entries": 300,
"activity_entries": 45,
"photos": 12
}
}
```
### ai_insights.json Struktur
```json
[
{
"id": "uuid",
"scope": "gesamt",
"created": "2026-03-18T10:00:00",
"result": "KI-Analyse Text..."
}
]
```
### README.txt Inhalt
```
Mitai Jinkendo Datenexport
Version: 2
Exportiert am: YYYY-MM-DD
Profil: {name}
Inhalt:
- profile.json: Profildaten und Einstellungen
- data/*.csv: Messdaten (Semikolon-getrennt, UTF-8)
- insights/: KI-Auswertungen (JSON)
- photos/: Progress-Fotos (JPEG)
Import:
Dieser Export kann in Mitai Jinkendo unter
Einstellungen → Import → "Mitai Backup importieren"
wieder eingespielt werden.
Format-Version 2 (ab v9b):
Alle CSV-Dateien sind UTF-8 mit BOM kodiert.
Trennzeichen: Semikolon (;)
Datumsformat: YYYY-MM-DD
```
### Import-Funktion (zu implementieren)
**Endpoint:** `POST /api/import/zip`
**Verhalten:**
- Akzeptiert ZIP-Datei (multipart/form-data)
- Erkennt export_version aus profile.json
- Importiert nur fehlende Einträge (kein Duplikat)
- Fotos werden nicht überschrieben falls bereits vorhanden
- Gibt Zusammenfassung zurück: wie viele Einträge je Kategorie importiert
- Bei Fehler: vollständiger Rollback (alle oder nichts)
**Duplikat-Erkennung:**
```python
# INSERT ... ON CONFLICT (profile_id, date) DO NOTHING
# weight: UNIQUE (profile_id, date)
# nutrition: UNIQUE (profile_id, date, meal_name)
# activity: UNIQUE (profile_id, date, name)
# caliper: UNIQUE (profile_id, date)
# circumferences: UNIQUE (profile_id, date)
```
**Frontend:** Neuer Button in SettingsPage:
```
[ZIP exportieren] [JSON exportieren] [Backup importieren]
```