mitai-jinkendo/CLAUDE.md
Lars ef8008a75d
All checks were successful
Deploy Development / deploy (push) Successful in 33s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 12s
docs: update CLAUDE.md and add comprehensive membership system documentation
Updates:
- CLAUDE.md: Reflect current v9c-dev status (enforcement disabled, history working)
- CLAUDE.md: Document simple AI limit system currently active
- CLAUDE.md: Update implementation status (admin UI complete, enforcement rolled back)

New Documentation:
- docs/MEMBERSHIP_SYSTEM.md: Complete v9c architecture documentation
  - Design decisions and rationale
  - Complete database schema (11 tables)
  - Backend API overview (7 routers, 30+ endpoints)
  - Frontend components (6 admin pages)
  - Feature enforcement rollback analysis
  - Lessons learned and next steps
  - Testing strategy
  - Deployment notes
  - Troubleshooting guide

The new doc provides complete reference for:
- Feature-Registry-Pattern implementation
- Tier system architecture
- Coupon system (3 types with stacking logic)
- User-Override system
- Access-Grant mechanics
- What went wrong with enforcement attempt
- Roadmap for v9d/v9e

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:44:29 +01:00

39 KiB
Raw Blame History

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: v9c-dev (März 2026)

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 (simple daily 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)
  • KI-Analyse Historisierung: Alle Analysen werden gespeichert (nicht überschrieben)
  • Pipeline korrekt: Speichert unter scope='pipeline', erscheint nur 1x in UI
  • 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%)

Aktuelles KI-Limit-System (Simple):

-- Tägliche Limits pro Profil
profiles.ai_enabled BOOLEAN          -- KI an/aus
profiles.ai_limit_day INTEGER        -- Tägliches Limit (NULL = unbegrenzt)
ai_usage (profile_id, date, count)  -- Täglicher Counter

-- Funktionen in routers/insights.py:
check_ai_limit(pid)  -- Prüft Limit vor KI-Call
inc_ai_usage(pid)    -- Inkrementiert Counter nach Call

Was in v9c kommt: Subscription & Coupon Management System

Phase 1 (DB-Schema): DONE Phase 2 (Backend API): DONE Phase 3 (Admin Frontend): DONE Phase 4 (Feature Enforcement): ⚠️ DEAKTIVIERT (Rollback am 20.03.2026 - Bugs)

Core Features (Backend & Admin-UI komplett):

  • DB-Schema (11 neue Tabellen, Feature-Registry Pattern)
  • 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)

Admin-Frontend (vollständig implementiert):

  • 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)
  • Alle Routes in App.jsx registriert

⚠️ Feature Enforcement - ROLLBACK (20.03.2026):

  • Initial implementation broke core functionality (analysis history, export visibility, counters)
  • Complete rollback to working state (commit 4fcde4a)
  • Simple AI limit system (ai_enabled, ai_limit_day) now active
  • v9c backend/admin UI remains functional but NOT enforcing limits
  • Needs complete reimplementation with proper testing before re-enabling

Noch NICHT implementiert:

  • 🔲 Feature-Enforcement-System (needs redesign)
  • 🔲 Selbst-Registrierung mit E-Mail-Verifizierung
  • 🔲 Trial-System UI (Countdown-Banner)
  • 🔲 App-Settings Admin-Panel (globale Konfiguration)

📚 Vollständige v9c Dokumentation: Siehe /docs/MEMBERSHIP_SYSTEM.md für:

  • Vollständige Architektur-Dokumentation
  • Datenbank-Schema Details
  • API-Endpoints Übersicht
  • Design-Entscheidungen und Rationale
  • Lessons Learned vom Feature-Enforcement-Rollback

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

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)

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)

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

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

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

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

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

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

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

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

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

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:

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

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:

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

<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

// 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

// 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

<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)

# 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)

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

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

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

if (loading) return (
  <div style={{display:'flex',justifyContent:'center',padding:40}}>
    <div className="spinner"/>
  </div>
)

Fehlerzustand:

if (error) return (
  <div style={{color:'var(--danger)',padding:16,textAlign:'center'}}>
    {error}
  </div>
)

Leerer Zustand:

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

<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)

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

// 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

// ❌ 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

# ❌ 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

// ❌ Falsch:
<Bar fill={(entry) => entry.color}/>

// ✅ Richtig:
<Bar fill="#1D9E75"/>

PostgreSQL Boolean-Syntax

# ❌ 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

# ❌ 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

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

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

# 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]