447 lines
14 KiB
Markdown
447 lines
14 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 | 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: <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
|
||
<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
|
||
|
||
### 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"/>
|
||
```
|
||
|
||
### SQLite neue Spalten hinzufügen
|
||
```python
|
||
# In _safe_alters Liste hinzufügen (NICHT direkt ALTER TABLE):
|
||
_safe_alters = [
|
||
("profiles", "neue_spalte TEXT DEFAULT NULL"),
|
||
]
|
||
```
|