# 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 | SQLite (v9a) → PostgreSQL (v9b geplant) | - | | 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, alle Endpoints (~2000 Zeilen) │ ├── 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: v9a ### Was implementiert ist: - ✅ Multi-User mit E-Mail + Passwort Login (bcrypt) - ✅ Auth-Middleware auf ALLE Endpoints (44 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 - ✅ 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 - ✅ Multi-Environment: Prod (mitai.jinkendo.de) + Dev (dev.mitai.jinkendo.de) - ✅ Gitea CI/CD mit Auto-Deploy auf Raspberry Pi 5 ### Was in v9b kommt: - 🔲 PostgreSQL Migration (aktuell noch SQLite) - 🔲 Selbst-Registrierung mit E-Mail-Bestätigung - 🔲 Freemium Tier-System (free/basic/premium/selfhosted) - 🔲 14-Tage Trial automatisch - 🔲 Einladungslinks für Beta-Nutzer - 🔲 Admin kann Tiers manuell setzen ### Was in v9c kommt: - 🔲 OAuth2-Grundgerüst für Fitness-Connectoren - 🔲 Strava Connector - 🔲 Withings Connector (Waage) - 🔲 Garmin Connector ## 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 (SQLite, v9a) ### Wichtige Tabellen: - `profiles` – Nutzer (role, pin_hash/bcrypt, email, auth_type, ai_enabled) - `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) - `ai_insights` – KI-Auswertungen (scope = prompt-slug) - `ai_prompts` – Konfigurierbare Prompts mit Templates (11 Prompts) - `ai_usage` – KI-Calls pro Tag pro Profil ## Auth-Flow (v9a) ``` 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: ` - Responses: immer JSON - Fehler: `{"detail": "Fehlermeldung"}` - Rate Limit überschritten: HTTP 429 ## Umgebungsvariablen (.env) ``` OPENROUTER_API_KEY= # KI-Calls OPENROUTER_MODEL=anthropic/claude-sonnet-4 SMTP_HOST= # E-Mail SMTP_PORT=587 SMTP_USER= SMTP_PASS= SMTP_FROM= APP_URL=https://mitai.jinkendo.de ALLOWED_ORIGINS=https://mitai.jinkendo.de DATA_DIR=/app/data PHOTOS_DIR=/app/photos ``` ## 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. **SQLite safe_alters** – neue Spalten immer via safe_alters Liste 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 ## 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
Seitentitel
``` **Ladezustand:** ```jsx if (loading) return (
) ``` **Fehlerzustand:** ```jsx if (error) return (
{error}
) ``` **Leerer Zustand:** ```jsx {items.length === 0 && (
📭
Noch keine Einträge
)} ``` **Metric Card:** ```jsx
LABEL
{value}
Einheit
``` ### 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 ### 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: entry.color}/> // ✅ Richtig: ``` ### SQLite neue Spalten hinzufügen ```python # In _safe_alters Liste hinzufügen (NICHT direkt ALTER TABLE): _safe_alters = [ ("profiles", "neue_spalte TEXT DEFAULT NULL"), ] ```