Backend: - Add feature access checks to insights, export, import endpoints - Enforce ai_calls, ai_pipeline, data_export, csv_import limits - Return HTTP 403 (disabled) or 429 (limit exceeded) Frontend: - Create useFeatureAccess hook for feature checking - Create FeatureGate/FeatureBadge components - Gate KI-Analysen in Analysis page - Gate Export/Import in Settings page - Show usage counters (e.g. "3/10") Docs: - Update CLAUDE.md with implementation status Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1220 lines
40 KiB
Markdown
1220 lines
40 KiB
Markdown
# 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): ✅ DONE** (Feature Enforcement komplett, Self-Registration offen)
|
||
|
||
**Core Features (Backend):**
|
||
- ✅ DB-Schema (11 neue Tabellen, Feature-Registry Pattern)
|
||
- ✅ **Feature-Access Enforcement** - Limits werden jetzt korrekt durchgesetzt!
|
||
- ✅ 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 (Phase 3) - Status:**
|
||
- ✅ **AdminFeaturesPage** - Feature-Konfiguration (sortierung, reset_period, limits, visibility)
|
||
- ✅ **AdminTiersPage** - Tier-Verwaltung (CRUD, pricing monthly/yearly)
|
||
- ✅ **AdminTierLimitsPage** - Matrix-Editor (Tier x Feature, responsive mobile/desktop views)
|
||
- ✅ **AdminCouponsPage** - Coupon-Manager (CRUD, 3 Typen, auto-generate codes, redemption history)
|
||
- ✅ **AdminUserRestrictionsPage** - User-Override-System (effektive Werte, auto-remove redundant overrides)
|
||
- ✅ **SubscriptionPage** - User Subscription-Info + Coupon-Einlösung (tier badge, limits, usage progress bars)
|
||
- ✅ **Feature Enforcement (März 2026)** - Backend + Frontend Feature Gates komplett implementiert
|
||
- ✅ Alle Routes in App.jsx registriert
|
||
- 🔲 Selbst-Registrierung mit E-Mail-Verifizierung (Pflicht)
|
||
- 🔲 Trial-System UI (Countdown-Banner, auto-start nach E-Mail-Verifikation)
|
||
- 🔲 App-Settings Admin-Panel (globale Konfiguration: trial_days, allow_registration, etc.)
|
||
|
||
**✅ Feature Enforcement Implementation (20. März 2026):**
|
||
|
||
**Backend - Feature Checks in Endpoints:**
|
||
- ✅ **routers/insights.py** - KI-Analysen geschützt
|
||
- `analyze_with_prompt()` - prüft 'ai_calls' feature, wirft HTTP 403/429 bei Limit
|
||
- `run_pipeline()` - prüft 'ai_pipeline' feature
|
||
- Beide inkrementieren usage nach erfolgreicher Ausführung
|
||
- ✅ **routers/exportdata.py** - Alle Export-Endpoints geschützt (CSV/JSON/ZIP)
|
||
- Prüft 'data_export' feature, wirft HTTP 403 bei deaktiviert/429 bei Limit
|
||
- Inkrementiert usage nach Export
|
||
- ✅ **routers/importdata.py** - ZIP-Import geschützt
|
||
- Prüft 'csv_import' feature vor Import
|
||
- ✅ **routers/nutrition.py** - FDDB CSV-Import geschützt
|
||
- Prüft 'csv_import' feature
|
||
- ✅ **routers/activity.py** - Apple Health CSV-Import geschützt
|
||
- Prüft 'csv_import' feature
|
||
|
||
**Frontend - Feature Gate System:**
|
||
- ✅ **hooks/useFeatureAccess.js** - Custom Hook für Feature-Access-Prüfung
|
||
- Ruft `/api/features/{slug}/check-access` auf
|
||
- Liefert: canUse, limit, used, remaining, reason, loading, error
|
||
- Optimistic default (canUse=true) für bessere UX
|
||
- ✅ **components/FeatureGate.jsx** - Reusable Feature Gate Component
|
||
- `<FeatureGate>` - versteckt children wenn Feature nicht verfügbar
|
||
- `<FeatureBadge>` - zeigt Usage-Counter (z.B. "3/10") mit Farb-Codierung
|
||
- Upgrade-Prompt optional anzeigbar
|
||
- ✅ **pages/Analysis.jsx** - KI-Analysen Feature-Gates
|
||
- Pipeline-Button wrapped in `<FeatureGate feature="ai_pipeline">`
|
||
- Einzelanalysen wrapped in `<FeatureGate feature="ai_calls">`
|
||
- Usage-Counter in beiden Sektionen
|
||
- ✅ **pages/SettingsPage.jsx** - Export/Import Feature-Gates
|
||
- Export-Buttons wrapped in `<FeatureGate feature="data_export">`
|
||
- Import-Button wrapped in `<FeatureGate feature="csv_import">`
|
||
- Usage-Counter bei beiden
|
||
- ✅ **utils/api.js** - API-Funktion hinzugefügt
|
||
- `checkFeatureAccess(featureSlug)` für Frontend-Checks
|
||
|
||
**Verhalten:**
|
||
- Backend wirft HTTP 403 "Feature nicht verfügbar" bei deaktiviertem Feature
|
||
- Backend wirft HTTP 429 "Limit erreicht" bei überschrittenem Limit
|
||
- Frontend blendet Feature aus oder zeigt Upgrade-Prompt
|
||
- Usage-Counter zeigt aktuelle Nutzung (z.B. "5/10 AI-Calls")
|
||
- Farb-Codierung: Grün (0-70%), Orange (70-90%), Rot (90-100%)
|
||
|
||
**Noch NICHT geschützt (optional für v9d):**
|
||
- 🔲 **photos.py** - Feature-Check für Progress-Fotos Upload
|
||
- 🔲 **weight.py, circumference.py, caliper.py** - Entry-Limits prüfen (verhindert neue Einträge wenn Limit erreicht)
|
||
- 🔲 **nutrition.py, activity.py** - Entry-Limits prüfen (analog zu weight)
|
||
|
||
**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.
|
||
|
||
### AdminUserRestrictionsPage – Effektive Werte anzeigen (v9c GELÖST)
|
||
**Problem:** Ursprüngliches Design zeigte leere Felder wenn kein Override existierte.
|
||
**Issues:**
|
||
- User konnte nicht sehen welcher Wert aktuell gilt (Override vs Tier-Standard)
|
||
- "unlimited" konnte nicht eingegeben/gespeichert werden (nur Platzhalter)
|
||
- Redundante Overrides (Wert = Tier-Standard) wurden nicht verhindert
|
||
- Tier-Limits verwendeten falschen Fallback (default_limit statt null)
|
||
|
||
**Lösung (März 2026):**
|
||
```javascript
|
||
// getDisplayValue() zeigt effektiven Wert (Override ODER Tier-Limit)
|
||
const restriction = restrictions.find(r => r.feature_id === featureId)
|
||
if (restriction) return formatValue(restriction.limit_value)
|
||
return formatValue(tierLimits[featureId]) // Tier-Standard als Fallback
|
||
|
||
// formatValue() konvertiert NULL zu "unlimited" (statt leer)
|
||
if (val === null) return 'unlimited'
|
||
|
||
// handleChange() entfernt Override wenn Wert = Tier-Standard
|
||
if (parsedValue === tierLimit) {
|
||
newChanges[featureId] = { action: 'remove' } // Redundanter Override
|
||
}
|
||
|
||
// Tier-Limits-Fallback wie TierLimitsPage
|
||
limits[feature.id] = limitsMatrix.limits[key] ?? null // nicht default_limit!
|
||
```
|
||
|
||
**Ergebnis:**
|
||
- ✅ User sieht immer den aktuellen effektiven Wert
|
||
- ✅ "unlimited" kann getippt und gespeichert werden (grün gefärbt)
|
||
- ✅ Redundante Overrides werden automatisch entfernt
|
||
- ✅ Selfhosted-Tier zeigt korrekt "unlimited" statt "0"
|
||
|
||
### 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]
|
||
```
|