feat: Complete MVP setup - Docker, Frontend, Migrations, CI/CD
Some checks failed
Deploy Development / deploy (push) Failing after 4s

Docker & Deployment:
- docker-compose.yml (Prod: Port 3003/8003)
- docker-compose.dev-env.yml (Dev: Port 3098/8098)
- Backend Dockerfile (Python 3.12-slim)
- Frontend Dockerfile (Node 20 + Nginx)
- Gitea Actions (deploy-dev.yml, deploy-prod.yml)

Frontend:
- React 18 + Vite setup
- package.json, vite.config.js, index.html
- App.jsx (minimal with version display)
- api.js (complete API client)
- app.css + AuthContext from Mitai
- main.jsx entry point

Backend Migrations:
- 001_auth_membership.sql (Auth + Features + Tier Limits)
- 002_organization.sql (Clubs, Divisions, Training Groups)
- 003_catalogs.sql (Skills + Methods with sample data)

Documentation:
- .claude/rules/ (ARCHITECTURE, CODING_RULES, etc.)
- SHINKAN_PROJECT_SETUP.md (technical setup guide)

Server:
- Directories created on Pi: /home/lars/docker/shinkan[-dev]
- Gitea Runner configured and running

Ready for first deployment to dev.shinkan.jinkendo.de

version: 0.1.0
date: 2026-04-21
This commit is contained in:
Lars 2026-04-21 14:36:52 +02:00
parent a426c03598
commit b2bc8590c4
25 changed files with 4972 additions and 0 deletions

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,470 @@
# Architektur-Regeln Mitai Jinkendo
> **PFLICHTLEKTÜRE für Claude Code vor jeder Implementierung.**
> Diese Regeln sind verbindlich und dürfen nicht ohne explizite
> Genehmigung des Nutzers abgeändert werden.
>
> **Dokumentationsablage:** siehe **`DOCUMENTATION.md`** (gleicher Ordner) fachlich/technisch/working/issues.
---
## 1. Router-Architektur
### 1.1 Ein Modul = Ein Router
Jedes fachliche Modul hat genau eine Router-Datei in `backend/routers/`.
```
backend/routers/
├── auth.py # Authentifizierung
├── profiles.py # Nutzerprofile
├── weight.py # Gewichts-Tracking
├── sleep.py # Schlaf-Modul
├── training_types.py # Trainingstypen + HF
└── ... # je neues Modul = neue Datei
```
**Regeln:**
- Kein Endpoint darf außerhalb seines thematischen Routers definiert werden
- Neue Module immer als neue Router-Datei anlegen, nie in bestehende einfügen
- Router in `main.py` registrieren: `app.include_router(modul.router, prefix="/api")`
- Router-Datei-Name = Modul-Name in `version.py` MODULE_VERSIONS
### 1.2 API-First Prinzip
Jede Funktion ist zuerst als API-Endpoint implementiert die UI nutzt ausschließlich
diese Endpoints über `api.js`. Keine Business-Logik im Frontend.
```python
# ✅ Richtig: Logik im Backend-Endpoint
@router.get("/sleep/stats")
def get_sleep_stats(session=Depends(require_auth)):
# Berechnung hier
return {"avg_duration": ..., "sleep_debt": ...}
# ❌ Falsch: Berechnung im Frontend
const sleepDebt = entries.reduce((sum, e) => sum + (goal - e.duration), 0)
```
### 1.3 Einheitliche Fehlerbehandlung
```python
# ✅ Immer dieses Format:
raise HTTPException(status_code=404, detail="Eintrag nicht gefunden")
# Response: {"detail": "Eintrag nicht gefunden"}
# ❌ Nie eigene Formate:
return {"error": "not found"}
return {"message": "Fehler", "success": False}
```
---
## 2. Versionskontrollsystem
### 2.1 Versionierungsschema
**Semantic Versioning: `MAJOR.MINOR.PATCH`**
| Typ | Wann | Beispiel |
|-----|------|---------|
| MAJOR | Breaking Change, DB-Migration inkompatibel | 9.0.0 → 10.0.0 |
| MINOR | Neues Feature, neues Modul | 9.2.0 → 9.3.0 |
| PATCH | Bugfix, kleine Änderung, Refactor | 9.3.0 → 9.3.1 |
### 2.2 Versions-Dateien
**Backend: `backend/version.py`**
```python
APP_VERSION = "9.3.0"
BUILD_DATE = "2026-03-22"
MODULE_VERSIONS = {
"auth": "1.2.0",
"profiles": "1.1.0",
"weight": "1.0.3",
"circumference": "1.0.1",
"caliper": "1.0.1",
"activity": "1.1.0",
"nutrition": "1.0.2",
"photos": "1.0.0",
"insights": "1.3.0",
"prompts": "1.1.0",
"admin": "1.2.0",
"stats": "1.0.1",
"exportdata": "1.1.0",
"importdata": "1.0.0",
"membership": "2.1.0",
}
CHANGELOG = [
{
"version": "9.3.0",
"date": "2026-03-22",
"changes": [
"Feature: Sleep Module (sleep_log, JSONB-Segmente)",
"Feature: Vitalwerte-Seite in Navigation",
"Feature: Trainingstypen-Kategorisierung",
]
},
{
"version": "9.2.1",
"date": "2026-03-20",
"changes": [
"Fix: Feature-Enforcement Rollback",
"Fix: Erholungsstatus-Gewichtung korrigiert",
]
},
]
```
**Frontend: `frontend/src/version.js`**
```javascript
export const APP_VERSION = "9.3.0"
export const BUILD_DATE = "2026-03-22"
export const PAGE_VERSIONS = {
Dashboard: "1.3.0",
LoginScreen: "1.1.0",
WeightPage: "1.0.3",
ActivityPage: "1.2.0",
NutritionPage: "1.1.0",
AnalysisPage: "1.3.0",
SettingsPage: "1.4.0",
AdminPanel: "1.2.0",
SubscriptionPage: "1.0.0",
// Neue Seiten hier eintragen
}
```
### 2.3 Versions-Endpoint
**`GET /api/version`** öffentlich (kein Auth erforderlich)
```json
{
"app_version": "9.3.0",
"build_date": "2026-03-22",
"backend_version": "9.3.0",
"modules": {
"auth": "1.2.0",
"sleep": "1.0.0"
},
"db_schema_version": "20260322",
"environment": "production"
}
```
Dieser Endpoint wird in `backend/routers/version.py` implementiert und liest
direkt aus `version.py`.
### 2.4 Versions-Anzeige in der App
**Settings-Seite Versions-Panel:**
```
System-Versionen
─────────────────────────────────────
App (gesamt) 9.3.0
Backend 9.3.0 ✓ erreichbar
Frontend 9.3.0 ✓ geladen
DB-Schema 20260322
Umgebung production
─────────────────────────────────────
Module
auth 1.2.0
sleep 1.0.0
membership 2.1.0
[alle Module...]
─────────────────────────────────────
[Changelog] [Cache leeren]
```
Frontend ruft beim Laden der Settings-Seite `/api/version` ab und vergleicht
mit der eigenen `APP_VERSION` aus `version.js`. Bei Abweichung: Warnung anzeigen.
### 2.5 Pflicht-Regel: Versions-Bump bei jedem Commit
**Jede Code-Änderung erfordert:**
1. Versions-Bump in `backend/version.py` (APP_VERSION + betroffenes MODULE_VERSION)
2. Versions-Bump in `frontend/src/version.js` (APP_VERSION + betroffene PAGE_VERSION)
3. Changelog-Eintrag in `backend/version.py` CHANGELOG
**Claude Code prüft das im `/deploy` Command automatisch.**
Kein Commit ohne Versions-Bump keine Ausnahme.
### 2.6 DB-Schema-Version
Format: `YYYYMMDD` (Datum der letzten Migration)
Gespeichert in `backend/version.py`:
```python
DB_SCHEMA_VERSION = "20260322"
```
Bei jeder Schema-Änderung (ALTER TABLE, neue Tabelle) → DB_SCHEMA_VERSION aktualisieren.
---
## 3. Datenbankregeln
### 3.1 Pflichtfelder für neue Tabellen
```sql
-- Jede neue Tabelle braucht:
id SERIAL PRIMARY KEY,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
```
### 3.2 Source-Tracking bei Import-Daten
Tabellen die Daten aus externen Quellen empfangen brauchen:
```sql
source VARCHAR(50) DEFAULT 'manual'
-- Werte u. a.: 'manual' | 'apple_health' | 'garmin' | 'withings' | 'csv'
```
Importe über den **Universal CSV**-Pfad setzen `source = 'csv'`, sofern die Tabelle ein `source`-Feld hat; CHECK-Constraints und Migrationen müssen diesen Wert erlauben.
**Agent-Pflicht bei neuen Import-Zielen oder Executor-Änderungen:** `.claude/docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md`
Manuelle Einträge (`source = 'manual'`) haben IMMER Vorrang bei Reimport:
```sql
-- Reimport überschreibt nur nicht-manuelle Einträge:
INSERT INTO sleep_log (...) ON CONFLICT (profile_id, date)
DO UPDATE SET ... WHERE sleep_log.source != 'manual'
```
### 3.3 Profile-ID Isolation
Jede Tabelle mit Nutzerdaten hat `profile_id` als Foreign Key.
Kein Endpoint gibt Daten eines anderen Profils zurück.
Profile-ID kommt IMMER aus der Session, nie aus Request-Parametern.
### 3.4 Boolean-Werte
```sql
-- PostgreSQL Boolean (nicht SQLite 0/1):
WHERE active = true ✓
WHERE active = 1 ✗
```
---
## 4. Frontend-Regeln
### 4.1 Alle API-Calls über api.js
```javascript
// ✅ Richtig:
import { api } from '../utils/api'
const data = await api.listSleep()
// ❌ Falsch:
const r = await fetch('/api/sleep')
```
### 4.2 Neue Seite = Eintrag in PAGE_VERSIONS
Jede neue Seite in `frontend/src/version.js` registrieren.
### 4.3 CSS-Variablen statt Hardcoded-Farben
```javascript
// ✅ Richtig:
style={{color: 'var(--accent)'}}
// ❌ Falsch:
style={{color: '#1D9E75'}}
```
### 4.4 Fehlerbehandlung in allen async Funktionen
```javascript
try {
const data = await api.meinEndpoint()
setData(data)
} catch(e) {
setError(e.message)
} finally {
setLoading(false)
}
```
---
## 5. Git & Deployment-Regeln
### 5.1 Nie direkt auf main pushen
Immer über Pull Request in Gitea: develop → main.
develop Branch niemals löschen.
### 5.2 Commit-Message Format
```
feat: neues Feature oder Modul
fix: Bugfix
refactor: Umbau ohne Funktionsänderung
docs: Dokumentation
version: Versions-Bump
ci: CI/CD Änderungen
chore: Maintenance
```
### 5.3 Versions-Bump im Commit
```
feat: Sleep Module v1.0.0
- sleep_log Tabelle mit JSONB-Segmenten
- Import aus Apple Health CSV
- Korrelationen Schlaf <-> Ruhepuls
version: 9.3.0 (backend + frontend)
module: sleep 1.0.0
```
---
## 6. Dokumentations-Regeln
### 6.1 Neue Module dokumentieren
Bei jedem neuen Modul:
1. Fachliche Spec: `.claude/docs/functional/MODUL_NAME.md`
2. Technische Spec: `.claude/docs/technical/MODUL_NAME.md`
3. Nach Fertigstellung: `.claude/library/` aktualisieren
### 6.2 CLAUDE.md aktuell halten
Nach größeren Änderungen CLAUDE.md Versions-Tabelle aktualisieren.
### 6.3 Lessons Learned dokumentieren
Jeder Rollback oder schwerer Bug → Eintrag in `.claude/rules/LESSONS_LEARNED.md`
---
## Zusammenfassung: Checkliste vor jedem Commit
```
[ ] Versions-Bump in backend/version.py (APP_VERSION + MODULE)
[ ] Versions-Bump in frontend/src/version.js (APP_VERSION + PAGE)
[ ] Changelog-Eintrag in backend/version.py
[ ] DB_SCHEMA_VERSION aktualisiert (wenn Schema geändert)
[ ] Neues Modul in PAGE_VERSIONS / MODULE_VERSIONS eingetragen
[ ] Auth auf alle neuen Endpoints (require_auth)
[ ] Fehlerformat einheitlich (HTTPException mit detail)
[ ] Neue Tabellen haben created_at + updated_at
[ ] Import-Tabellen haben source-Feld
[ ] api.js für alle Frontend API-Calls
```
---
## 7. Prod-Schutz & Dev-Zugriff
### 7.1 Absoluter Prod-Schutz
Claude Code darf auf dem Prod-System (mitai.jinkendo.de) NIEMALS:
- Container neustarten (`docker restart mitai-*`)
- Schreibend in Container ausführen (`docker exec mitai-api ...`)
- Dateien direkt ändern (`/home/lars/docker/bodytrack/`)
- Prod-Datenbank schreiben (nur SELECT erlaubt)
**Prod-Änderungen ausschließlich über:**
```
git push origin develop → Gitea PR → Merge → deploy-prod.yml
```
### 7.2 Dev-System voller Zugriff erlaubt
Claude Code darf auf dem Dev-System (dev.mitai.jinkendo.de):
- Container neustarten (`docker restart dev-mitai-*`)
- Logs lesen und filtern
- DB lesen und schreiben (für Tests)
- Container neu bauen
### 7.3 Test-Umgebung
- API-Tests: gegen http://dev.mitai.jinkendo.de
- Playwright-Tests: gegen https://dev.mitai.jinkendo.de
- Screenshots: in `screenshots/` Ordner (in .gitignore)
- Test-Credentials: in Umgebungsvariablen (TEST_EMAIL, TEST_PASSWORD)
- NIEMALS Test-Credentials in Code committen
### 7.4 Erkennungsmerkmale Prod vs. Dev
```
Prod-Container: mitai-api, mitai-ui, mitai-db-prod
Dev-Container: dev-mitai-api, dev-mitai-ui, dev-mitai-postgres
Prod-Ports: 8002 (Backend), 3002 (Frontend)
Dev-Ports: 8099 (Backend), 3099 (Frontend)
Prod-URL: mitai.jinkendo.de
Dev-URL: dev.mitai.jinkendo.de
```
---
## 8. CSV-Import vs. Data Layer (Issue #53)
### 8.1 Leitlinie: Wo Interpretation stattfindet
| Schicht | Erlaubt | Nicht Sinn der Schicht |
|--------|---------|-------------------------|
| **Import (Ingest)** | Zuordnung CSV→Speicherfeld, **Typ-/Einheits-Konvertierung** (`type_conversions`), Duplikat-/Constraint-Logik | Fachliche **Interpretation**, Aggregation von „Bedeutung“, Metriken für Auswertung |
| **Data Layer (Issue #53, Layer 1+)** | Daten lesen, aufbereiten, ableiten, für Charts/KI/Prompts bereitstellen | — |
Verbindlich: **Semantik und Auswertung** nicht dauerhaft im Import verstecken; neue Features werden an dieser Grenze geprüft.
**Detail & Zielbild (Multi-Layer, Single Source of Truth):** `docs/issues/issue-53-phase-0c-multi-layer-architecture.md`
**Umsetzung Schlaf-Import (Refactoring, Offen):** Gitea http://192.168.2.144:3000/Lars/mitai-jinkendo/issues/69
### 8.2 Ist-Einordnung Import-Pfade (Übergang)
Bis sukzessive auf das Zielbild umgestellt ist, gilt:
| Pfad | Einordnung |
|------|----------------|
| Universal-CSV (`csv_parser`, `routers/csv_import.py`, Executor für u. a. Gewicht/Ernährung/Blutdruck/Aktivität/Vitals) | **Zielrichtung:** Mapping + Typkonvertierung |
| Apple-Schlaf-Aggregat (`csv_parser/sleep_apple_import.py`, `import_mode: apple_sleep_aggregate`) | **Legacy-Adapter** (quellenspezifische Aufbereitung) Austausch gegen mapping-nah + Layer 1 geplant |
| Dedizierte Import-Endpoints (z. B. `/api/activity/import-csv`, Vitals Apple) | **Legacy/Parallel** neue Quellen bevorzugt über Universal-Pfad + Vorlagen |
Änderungen an Import-Pfaden: Legacy nur erweitern mit **expliziter** Issue-/Review-Begründung; kein neues „wir rechnen Auswertung beim Insert“ ohne Data-Layer-Bezug.
---
## 9. Test-Regeln
### 9.1 Tests schreiben ist Pflicht
Jedes neue Feature bekommt mindestens einen Playwright-Test in
`tests/dev-smoke-test.spec.js`.
### 9.2 Reihenfolge: Test vor Commit
```
Implementieren → Tests schreiben → Tests grün → Committen
NIEMALS: Implementieren → Committen → Tests später
```
### 9.3 Claude Code schreibt Tests selbst
Nach jeder Implementierung:
1. Passende Tests in dev-smoke-test.spec.js ergänzen
2. `npx playwright test` ausführen
3. Fehler korrigieren bis alle Tests grün
4. Erst dann committen
### 9.4 Test-Kategorien
```javascript
// UI-Test (Playwright)
test('FEATURE: Beschreibung', async ({ page }) => { ... })
// API-Test (Playwright request)
test('API: Endpoint', async ({ request }) => { ... })
```
### 9.5 Screenshots bei Fehlern
Fehlgeschlagene Tests erzeugen automatisch Screenshots in:
`test-results/TESTNAME/test-failed-1.png`
→ Immer ansehen bevor Code geändert wird
### 9.6 Prod nie testen
Tests laufen IMMER gegen dev.mitai.jinkendo.de
NIEMALS gegen mitai.jinkendo.de
---
## 10. Dashboard-Lab-Widgets und Feature-System
**Kontext:** Dashboard-Widgets (`backend/widget_catalog.py`, Lab unter `/api/app/...`) und das **Subscription-/Feature-Modell** (`features`, `tier_limits`, `check_feature_access` in `backend/auth.py`) sind **getrennte Schichten**, müssen aber bei tariffrelevanten Widgets **verknüpft** werden.
**Bindend:**
1. **Keine fest codierten Tier-Namen** für Widget-Rechte Tiers und Limits kommen aus der DB.
2. **Komplexität** (Module aus, Unter-Stufen, KI vs. Standard) liegt in der **Feature-/Subscription-Logik**, nicht verteilt in Widget-Komponenten.
3. **Nutzer-Konfigurator** (z.B. Dashboard-Lab): Widgets **ohne** passende Berechtigung **nicht anzeigen**; alle erlaubten Widgets bleiben verfügbar.
4. **Backend** liefert die effektive Erlaubnis (z.B. über erweiterten Katalog oder Entitlements), und **validiert beim Speichern** des Layouts, dass keine unerlaubten Widget-IDs persistiert werden (Policy: ablehnen oder strippen einheitlich halten).
5. **Daten/API:** Zusätzlich zur UI-Filterung müssen die **inhaltsliefernden Endpoints** weiterhin über `check_feature_access` geschützt sein (kein Leck über direkte API-Aufrufe).
**Detail-Doku (Checklisten, Dateipfade):** `.claude/docs/technical/DASHBOARD_WIDGETS_AGENT_GUIDE.md` § 0.

View File

@ -0,0 +1,339 @@
# Architektur-Regeln Mitai Jinkendo
> **PFLICHTLEKTÜRE für Claude Code vor jeder Implementierung.**
> Diese Regeln sind verbindlich und dürfen nicht ohne explizite
> Genehmigung des Nutzers abgeändert werden.
---
## 1. Router-Architektur
### 1.1 Ein Modul = Ein Router
Jedes fachliche Modul hat genau eine Router-Datei in `backend/routers/`.
```
backend/routers/
├── auth.py # Authentifizierung
├── profiles.py # Nutzerprofile
├── weight.py # Gewichts-Tracking
├── sleep.py # Schlaf-Modul
├── training_types.py # Trainingstypen + HF
└── ... # je neues Modul = neue Datei
```
**Regeln:**
- Kein Endpoint darf außerhalb seines thematischen Routers definiert werden
- Neue Module immer als neue Router-Datei anlegen, nie in bestehende einfügen
- Router in `main.py` registrieren: `app.include_router(modul.router, prefix="/api")`
- Router-Datei-Name = Modul-Name in `version.py` MODULE_VERSIONS
### 1.2 API-First Prinzip
Jede Funktion ist zuerst als API-Endpoint implementiert die UI nutzt ausschließlich
diese Endpoints über `api.js`. Keine Business-Logik im Frontend.
```python
# ✅ Richtig: Logik im Backend-Endpoint
@router.get("/sleep/stats")
def get_sleep_stats(session=Depends(require_auth)):
# Berechnung hier
return {"avg_duration": ..., "sleep_debt": ...}
# ❌ Falsch: Berechnung im Frontend
const sleepDebt = entries.reduce((sum, e) => sum + (goal - e.duration), 0)
```
### 1.3 Einheitliche Fehlerbehandlung
```python
# ✅ Immer dieses Format:
raise HTTPException(status_code=404, detail="Eintrag nicht gefunden")
# Response: {"detail": "Eintrag nicht gefunden"}
# ❌ Nie eigene Formate:
return {"error": "not found"}
return {"message": "Fehler", "success": False}
```
---
## 2. Versionskontrollsystem
### 2.1 Versionierungsschema
**Semantic Versioning: `MAJOR.MINOR.PATCH`**
| Typ | Wann | Beispiel |
|-----|------|---------|
| MAJOR | Breaking Change, DB-Migration inkompatibel | 9.0.0 → 10.0.0 |
| MINOR | Neues Feature, neues Modul | 9.2.0 → 9.3.0 |
| PATCH | Bugfix, kleine Änderung, Refactor | 9.3.0 → 9.3.1 |
### 2.2 Versions-Dateien
**Backend: `backend/version.py`**
```python
APP_VERSION = "9.3.0"
BUILD_DATE = "2026-03-22"
MODULE_VERSIONS = {
"auth": "1.2.0",
"profiles": "1.1.0",
"weight": "1.0.3",
"circumference": "1.0.1",
"caliper": "1.0.1",
"activity": "1.1.0",
"nutrition": "1.0.2",
"photos": "1.0.0",
"insights": "1.3.0",
"prompts": "1.1.0",
"admin": "1.2.0",
"stats": "1.0.1",
"exportdata": "1.1.0",
"importdata": "1.0.0",
"membership": "2.1.0",
}
CHANGELOG = [
{
"version": "9.3.0",
"date": "2026-03-22",
"changes": [
"Feature: Sleep Module (sleep_log, JSONB-Segmente)",
"Feature: Vitalwerte-Seite in Navigation",
"Feature: Trainingstypen-Kategorisierung",
]
},
{
"version": "9.2.1",
"date": "2026-03-20",
"changes": [
"Fix: Feature-Enforcement Rollback",
"Fix: Erholungsstatus-Gewichtung korrigiert",
]
},
]
```
**Frontend: `frontend/src/version.js`**
```javascript
export const APP_VERSION = "9.3.0"
export const BUILD_DATE = "2026-03-22"
export const PAGE_VERSIONS = {
Dashboard: "1.3.0",
LoginScreen: "1.1.0",
WeightPage: "1.0.3",
ActivityPage: "1.2.0",
NutritionPage: "1.1.0",
AnalysisPage: "1.3.0",
SettingsPage: "1.4.0",
AdminPanel: "1.2.0",
SubscriptionPage: "1.0.0",
// Neue Seiten hier eintragen
}
```
### 2.3 Versions-Endpoint
**`GET /api/version`** öffentlich (kein Auth erforderlich)
```json
{
"app_version": "9.3.0",
"build_date": "2026-03-22",
"backend_version": "9.3.0",
"modules": {
"auth": "1.2.0",
"sleep": "1.0.0"
},
"db_schema_version": "20260322",
"environment": "production"
}
```
Dieser Endpoint wird in `backend/routers/version.py` implementiert und liest
direkt aus `version.py`.
### 2.4 Versions-Anzeige in der App
**Settings-Seite Versions-Panel:**
```
System-Versionen
─────────────────────────────────────
App (gesamt) 9.3.0
Backend 9.3.0 ✓ erreichbar
Frontend 9.3.0 ✓ geladen
DB-Schema 20260322
Umgebung production
─────────────────────────────────────
Module
auth 1.2.0
sleep 1.0.0
membership 2.1.0
[alle Module...]
─────────────────────────────────────
[Changelog] [Cache leeren]
```
Frontend ruft beim Laden der Settings-Seite `/api/version` ab und vergleicht
mit der eigenen `APP_VERSION` aus `version.js`. Bei Abweichung: Warnung anzeigen.
### 2.5 Pflicht-Regel: Versions-Bump bei jedem Commit
**Jede Code-Änderung erfordert:**
1. Versions-Bump in `backend/version.py` (APP_VERSION + betroffenes MODULE_VERSION)
2. Versions-Bump in `frontend/src/version.js` (APP_VERSION + betroffene PAGE_VERSION)
3. Changelog-Eintrag in `backend/version.py` CHANGELOG
**Claude Code prüft das im `/deploy` Command automatisch.**
Kein Commit ohne Versions-Bump keine Ausnahme.
### 2.6 DB-Schema-Version
Format: `YYYYMMDD` (Datum der letzten Migration)
Gespeichert in `backend/version.py`:
```python
DB_SCHEMA_VERSION = "20260322"
```
Bei jeder Schema-Änderung (ALTER TABLE, neue Tabelle) → DB_SCHEMA_VERSION aktualisieren.
---
## 3. Datenbankregeln
### 3.1 Pflichtfelder für neue Tabellen
```sql
-- Jede neue Tabelle braucht:
id SERIAL PRIMARY KEY,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
```
### 3.2 Source-Tracking bei Import-Daten
Tabellen die Daten aus externen Quellen empfangen brauchen:
```sql
source VARCHAR(50) DEFAULT 'manual'
-- Werte: 'manual' | 'apple_health' | 'garmin' | 'withings'
```
Manuelle Einträge (`source = 'manual'`) haben IMMER Vorrang bei Reimport:
```sql
-- Reimport überschreibt nur nicht-manuelle Einträge:
INSERT INTO sleep_log (...) ON CONFLICT (profile_id, date)
DO UPDATE SET ... WHERE sleep_log.source != 'manual'
```
### 3.3 Profile-ID Isolation
Jede Tabelle mit Nutzerdaten hat `profile_id` als Foreign Key.
Kein Endpoint gibt Daten eines anderen Profils zurück.
Profile-ID kommt IMMER aus der Session, nie aus Request-Parametern.
### 3.4 Boolean-Werte
```sql
-- PostgreSQL Boolean (nicht SQLite 0/1):
WHERE active = true ✓
WHERE active = 1 ✗
```
---
## 4. Frontend-Regeln
### 4.1 Alle API-Calls über api.js
```javascript
// ✅ Richtig:
import { api } from '../utils/api'
const data = await api.listSleep()
// ❌ Falsch:
const r = await fetch('/api/sleep')
```
### 4.2 Neue Seite = Eintrag in PAGE_VERSIONS
Jede neue Seite in `frontend/src/version.js` registrieren.
### 4.3 CSS-Variablen statt Hardcoded-Farben
```javascript
// ✅ Richtig:
style={{color: 'var(--accent)'}}
// ❌ Falsch:
style={{color: '#1D9E75'}}
```
### 4.4 Fehlerbehandlung in allen async Funktionen
```javascript
try {
const data = await api.meinEndpoint()
setData(data)
} catch(e) {
setError(e.message)
} finally {
setLoading(false)
}
```
---
## 5. Git & Deployment-Regeln
### 5.1 Nie direkt auf main pushen
Immer über Pull Request in Gitea: develop → main.
develop Branch niemals löschen.
### 5.2 Commit-Message Format
```
feat: neues Feature oder Modul
fix: Bugfix
refactor: Umbau ohne Funktionsänderung
docs: Dokumentation
version: Versions-Bump
ci: CI/CD Änderungen
chore: Maintenance
```
### 5.3 Versions-Bump im Commit
```
feat: Sleep Module v1.0.0
- sleep_log Tabelle mit JSONB-Segmenten
- Import aus Apple Health CSV
- Korrelationen Schlaf <-> Ruhepuls
version: 9.3.0 (backend + frontend)
module: sleep 1.0.0
```
---
## 6. Dokumentations-Regeln
### 6.1 Neue Module dokumentieren
Bei jedem neuen Modul:
1. Fachliche Spec: `.claude/docs/functional/MODUL_NAME.md`
2. Technische Spec: `.claude/docs/technical/MODUL_NAME.md`
3. Nach Fertigstellung: `.claude/library/` aktualisieren
### 6.2 CLAUDE.md aktuell halten
Nach größeren Änderungen CLAUDE.md Versions-Tabelle aktualisieren.
### 6.3 Lessons Learned dokumentieren
Jeder Rollback oder schwerer Bug → Eintrag in `.claude/rules/LESSONS_LEARNED.md`
---
## Zusammenfassung: Checkliste vor jedem Commit
```
[ ] Versions-Bump in backend/version.py (APP_VERSION + MODULE)
[ ] Versions-Bump in frontend/src/version.js (APP_VERSION + PAGE)
[ ] Changelog-Eintrag in backend/version.py
[ ] DB_SCHEMA_VERSION aktualisiert (wenn Schema geändert)
[ ] Neues Modul in PAGE_VERSIONS / MODULE_VERSIONS eingetragen
[ ] Auth auf alle neuen Endpoints (require_auth)
[ ] Fehlerformat einheitlich (HTTPException mit detail)
[ ] Neue Tabellen haben created_at + updated_at
[ ] Import-Tabellen haben source-Feld
[ ] api.js für alle Frontend API-Calls
```

View File

@ -0,0 +1,100 @@
# Coding Rules Mitai Jinkendo
Diese Regeln IMMER befolgen. Sie basieren auf Erfahrungen aus der Entwicklung.
## Backend
### 1. Auth auf jeden Endpoint
```python
# Jeder neue Endpoint braucht Auth:
@router.get("/neuer-endpoint")
def neuer_endpoint(session: dict = Depends(require_auth)):
pid = session['profile_id']
```
### 2. Profile-ID aus Session nie aus Header
```python
pid = session['profile_id'] # ✅
# Nicht: request.headers.get('X-Profile-Id') ❌
```
### 3. bcrypt für Passwörter
```python
from auth import hash_pin, verify_pin
hashed = hash_pin(plain_password) # ✅
# Nicht: hashlib.sha256(...) ❌
```
### 4. PostgreSQL-Syntax
```python
cur.execute("SELECT * FROM t WHERE id = %s AND active = true", (id,))
# Nicht: ? und active = 1 (SQLite-Syntax)
```
### 5. Rate Limiting für sensitive Endpoints
```python
from slowapi import Limiter
@router.post("/sensitive")
@limiter.limit("5/minute")
def sensitive(request: Request, ...):
```
### 6. Universal CSV Import / Admin-Vorlagen
Neues **Import-Zielmodul**, Änderungen an **`csv_parser`**, Executor, DB-`source`/`CHECK`, oder System-CSV-Vorlagen:
- Pflichtlektüre und Checkliste: **`.claude/docs/technical/UNIVERSAL_CSV_IMPORT_AGENT_GUIDE.md`**
- Keine zweite DB-Connection im Importpfad; Zeilenfehler ohne „aborted transaction“ (SAVEPOINT-Muster wo nötig)
- Admin Create/Update von Systemvorlagen: Validierung über `validate_csv_template` nicht umgehen
## Frontend
### 1. api.js für alle API-Calls
```javascript
await api.listWeight() // ✅
await fetch('/api/weight') // ❌ kein Token
```
### 2. Fehlerbehandlung in async Funktionen
```javascript
try {
const data = await api.meinEndpoint()
} catch(e) {
setError(e.message) // api.js wirft bereits Error mit detail-Text
}
```
### 3. Kein TypeScript
Das Projekt nutzt bewusst kein TypeScript keine .ts/.tsx Dateien erstellen.
### 4. Keine neuen npm-Pakete ohne Absprache
Erst fragen, dann installieren.
### 5. CSS-Variablen statt Hardcoded-Farben
```javascript
// ✅ Richtig:
style={{color: 'var(--accent)'}}
// ❌ Falsch:
style={{color: '#1D9E75'}}
```
## Git & Deployment
### 1. Nie direkt auf main pushen
Immer über Pull Request in Gitea: develop → main
### 2. develop Branch nie löschen
Er ist permanent nicht nach Merge löschen.
### 3. .env nie committen
Steht in .gitignore nie entfernen.
### 4. Commit-Message Format
```
feat: neues Feature
fix: Bugfix
refactor: Umbau ohne Funktionsänderung
docs: Dokumentation
ci: CI/CD Änderungen
chore: Maintenance
```

View File

@ -0,0 +1,77 @@
# Dokumentation verbindliche Regeln
> **PFLICHTLEKTÜRE** für alle Agenten vor größeren Änderungen (neben `ARCHITECTURE.md`, `CODING_RULES.md`, `LESSONS_LEARNED.md`).
> Ziel: **ein** nachvollziehbarer Einstieg unter `.claude/`, klare Trennung **fachlich / technisch / Arbeitspapier / Issue**.
---
## 1. Einstiegspunkte (Reihenfolge)
1. Repo-Root: `CLAUDE.md` (Kontext, Links, Pflicht-Dokus)
2. Agent-Übersicht: **`.claude/README.md`** (Baum, wo was liegt)
3. Spez-Index: **`.claude/docs/README.md`**
4. Aufgaben-Tracking: **Gitea** Übersicht lokal: **`.claude/docs/GITEA_ISSUES_INDEX.md`** (regelmäßig refreshen nach Bedarf)
---
## 2. Ablagepflicht nach Dokumententyp
| Typ | Pfad | Inhalt / Regel |
|-----|------|----------------|
| **Fachliche Spec (WAS)** | `.claude/docs/functional/` | Domäne, Use Cases, UX-Ziele, fachliche Datenarchitektur. **Keine** reine API-Parameterliste (→ technical). |
| **Technische Spec (WIE)** | `.claude/docs/technical/` | API-, DB-, Implementierungsmuster, Agent-Guides, Migrationen. |
| **Architektur-Querschnitt** | `.claude/docs/architecture/` | Kurze Überblicke (z.B. Frontend-Baum), ergänzend zu technical. |
| **Arbeitspapier / Zwischenstand** | `.claude/docs/working/` | Analysen, Sessions, Migration-Notizen, **keine** langfristige Norm. Kann veraltet sein → Datum im Dokument. **Nicht** als alleinige „Wahrheit“ für Produkt zitieren. |
| **Audits & Matrizen** | `.claude/docs/audit/` | Zeitlich begrenzte Reviews, Reconciliation, Gitea-Vorlagen. |
| **Issue-Begleitung (lang, versioniert im Repo)** | `docs/issues/` | Epics, detaillierte Issue-Ausarbeitungen, Abnahme-Dokus, die mit Gitea-Nummern korrespondieren. **Dateiname:** sinnvoller Slug, z.B. `issue-50-phase-0a-goal-system.md`. |
| **Governance in `docs/` (aktueller Ausnahmebereich)** | `docs/PLACEHOLDER_*.md` | Platzhalter-Governance & Deployment-Hinweise, solange Pfade in Skripten/Docker auf `docs/` zeigen. Bei Umzug nach `technical/` **alle** Referenzen und Deploy-Pfade anpassen. |
---
## 3. Verboten / vermeiden
- **Keine** vollständige Duplikation derselben Spec an zwei Pflegen (außer kurzer Stub mit Verweis auf Kanon).
- **Keine** normativen Regeln nur in Chat; Regeln hier oder in `ARCHITECTURE.md` / `CODING_RULES.md` festhalten.
- **Keine** Mischung „Issue-Checkliste + langfristige Spec“ in einer Datei ohne Überschrift lieber Spec in `functional|technical`, Checkliste in Gitea oder `docs/issues/`.
---
## 4. Pflege nach Änderungen
| Änderung | Pflege |
|----------|--------|
| Neues Feature (> 12 Tage) | Spec in `functional/` und ggf. `technical/`; Issue in Gitea; bei Bedarf `docs/issues/issue-NN-….md`. |
| Neue Endpoints / Tabellen | `technical/API_REFERENCE.md`, `technical/DATABASE.md` bzw. `.claude/library/*` nach Prozess. |
| Platzhalter (Registry-Pflicht) | `.claude/docs/technical/PLACEHOLDER_REGISTRY_FRAMEWORK.md`, Registrierung unter `backend/placeholder_registrations/`. |
| Mehrere offene Gitea-Themen | `GITEA_ISSUES_INDEX.md` aktualisieren (Kategorien, Dubletten-Hinweis). |
---
## 5. Lokale Agent-Artefakte (nicht zwingend im Git)
Bleiben unter `.claude/` für Kontinuität der Agenten, werden **standardmäßig nicht** versioniert:
- `.claude/task/` Arbeitspakete pro Thema
- `.claude/handover/` Session-Dateien (optional: nur `NEXT_SESSION_PROMPT.md` nach Bedarf versionieren)
Diese Ordner sind **kein** Ersatz für `working/` oder `docs/issues/`, wenn das Ergebnis für das Team festgehalten werden soll.
---
## 6. Kurzreferenz Pfade
```
.claude/README.md ← Einstieg Agent/Human
.claude/docs/README.md ← Spec-Katalog
.claude/docs/functional/ ← WAS
.claude/docs/technical/ ← WIE
.claude/docs/working/ ← Arbeitspapiere / Analysen
.claude/docs/audit/ ← Audits
.claude/docs/GITEA_ISSUES_INDEX.md ← Issue-Landkarte (lokal gepflegt)
docs/issues/ ← Issue-Epics (Repo)
docs/PLACEHOLDER_*.md ← Platzhalter (bis Migration der Pfade)
```
---
**Version:** 1.0 · **Stand:** 2026-04-08

View File

@ -0,0 +1,249 @@
# Implementation Rules Mitai Jinkendo
> **PFLICHTLEKTÜRE für Claude Code vor jeder Feature-Implementierung.**
> Diese Regeln sind verbindlich und dürfen nicht ohne explizite
> Genehmigung des Nutzers übersprungen werden.
---
## 1. Konzept-basierte Implementierung (MANDATORY)
### 1.1 Wann gilt dieser Prozess?
**PFLICHT bei:**
- Feature-Requests mit verlinktem Konzept/Spec-Dokument
- Gitea Issues mit "Konzept" Label
- User sagt "laut Konzept", "wie im Konzept beschrieben"
- Komplexe Features mit >3 Datenquellen oder >5 Funktionen
- Neue Chart-Endpoints mit spezifischen Anforderungen
**OPTIONAL bei:**
- Einfache Bugfixes (1-2 Zeilen)
- Triviale UI-Änderungen (Text, Farbe, Spacing)
- Code-Cleanup ohne Funktionsänderung
**Bei Unsicherheit:** Prozess anwenden. Lieber einmal zu viel als einmal zu wenig.
---
## 2. Der 5-Stufen-Prozess
### Stufe 1: Anforderungsanalyse (BEFORE ANY CODE)
```
□ Konzept/Spec-Dokument VOLLSTÄNDIG lesen
□ Pro Feature: Checkliste mit ALLEN geforderten Elementen erstellen
□ Datenquellen identifizieren (welche Tabellen, data_layer Funktionen)
□ Fehlende Funktionen dokumentieren (was muss neu gebaut werden)
□ Unklarheiten als Fragen dokumentieren
□ Gap-Analyse: Was existiert bereits? Was fehlt komplett?
```
**Output:** Anforderungs-Matrix (Tabelle oder Liste)
**Beispiel für E1 (Energiebilanz Chart):**
```
Gefordert im Konzept:
✓ Kalorienaufnahme täglich (nutrition_log.kcal)
✓ 7d Durchschnitt Aufnahme (berechnen)
✗ Trainingskalorien (activity_log.kcal) → FEHLT
✗ Gewichtstrend 7d geglättet (weight_log) → FEHLT
✗ Lagged comparison 3d/7d/14d → FEHLT
✓ TDEE geschätzt (Profile-basiert oder Formel)
✗ Energiebilanz als Balken → Chart-Typ ändern
Offene Fragen:
- Trainingskalorien: activity_log.kcal oder estimated_kcal?
- TDEE: Aus Profil oder Harris-Benedict berechnen?
- Lag-Korrelation: Pearson oder andere Methode?
```
### Stufe 2: Umsetzungskonzept erstellen
```
□ Backend: Welche Endpoints? Welche Parameter?
□ Backend: Welche data_layer Funktionen? (existierend + neu)
□ Backend: Welche Berechnungen? (Formeln dokumentieren)
□ Frontend: Welche Komponenten? Welche Chart-Typen?
□ Frontend: Welche API-Calls?
□ Dependencies: Müssen andere Module angepasst werden?
```
**Output:** Strukturiertes Umsetzungskonzept als Markdown
**Beispiel:**
```markdown
## E1: Energiebilanz Chart - Umsetzungskonzept
### Backend
**Endpoint:** `GET /api/charts/energy-balance?days=28`
**Datenquellen:**
- `nutrition_log` (kcal, date)
- `activity_log` (kcal, date) → aggregiert nach Tag
- `weight_log` (weight, date) → 7d gleitend
**Neue data_layer Funktionen:**
- `get_daily_training_calories(profile_id, days)` → Summe kcal pro Tag
- `get_weight_trend_7d(profile_id, days)` → 7d gleitender Durchschnitt
- `calculate_lag_correlation(energy_balance, weight_change, lag_days)` → Pearson
**Berechnungen:**
1. Energie-Bilanz = kcal_intake - (TDEE + training_kcal)
2. 7d avg intake = rolling_mean(kcal_intake, 7)
3. 7d avg bilanz = rolling_mean(bilanz, 7)
4. Lag-Korrelation: bilanz[t] vs. weight_change[t+3/7/14]
**Response Format:**
```json
{
"chart_type": "mixed", // Line + Bar kombiniert
"data": {
"labels": ["2026-01-01", ...],
"datasets": [
{"type": "line", "label": "Kalorien", "data": [...]},
{"type": "line", "label": "7d Ø Kalorien", "data": [...]},
{"type": "line", "label": "Training kcal", "data": [...]},
{"type": "line", "label": "TDEE", "data": [...], "borderDash": [5,5]},
{"type": "bar", "label": "Bilanz", "data": [...], "yAxisID": "y1"},
{"type": "line", "label": "Gewicht (7d)", "data": [...], "yAxisID": "y2"}
]
},
"metadata": {
"avg_intake": 2100,
"avg_balance": -300,
"weight_change_7d": -0.5,
"lag_correlation": {
"3d": 0.42,
"7d": 0.68,
"14d": 0.75
}
}
}
```
### Frontend
**Component:** `NutritionCharts.jsx``renderEnergyBalance()`
**Chart-Typ:** Recharts `ComposedChart` (Line + Bar kombiniert)
**API-Call:** `api.getEnergyBalanceChart(days)`
**Features:**
- Dual Y-Axis (links: kcal, rechts: kg)
- Legende mit Hover-Details
- Tooltips zeigen alle Werte
- Lag-Korrelation in Metadata-Box unter Chart
```
### Stufe 3: User-Approval einholen (MANDATORY)
```
□ Umsetzungskonzept dem User zeigen
□ Offene Fragen klären
□ Auf explizites OK warten
□ Bei Änderungswünschen: Konzept anpassen, erneut zeigen
```
**Niemals** mit der Implementierung beginnen ohne User-Approval!
### Stufe 4: Implementierung
```
□ Exakt nach genehmigtem Konzept implementieren
□ Niemals "ähnliches" bauen oder Abkürzungen nehmen
□ Bei unvorhergesehenen Problemen: User informieren, Konzept anpassen
□ Commits: Referenz zum Konzept im Commit-Message
```
**Commit-Message Format:**
```
feat: E1 Energiebilanz Chart (konzept-konform)
Backend:
- Neue data_layer Funktionen: get_daily_training_calories, get_weight_trend_7d
- Endpoint: /api/charts/energy-balance mit Lag-Korrelation
- Chart-Type: mixed (Line + Bar kombiniert)
Frontend:
- ComposedChart mit Dual Y-Axis (kcal + kg)
- Lag-Korrelation Metadata-Display
Konzept: .claude/docs/functional/mitai_jinkendo_konzept_diagramme_auswertungen_v2.md (E1)
```
### Stufe 5: Compliance-Check (BEFORE COMMIT)
```
□ Jedes Feature gegen Konzept prüfen (Checkliste abhaken)
□ Alle geforderten Elemente vorhanden?
□ Berechnungen korrekt nach Konzept?
□ Chart-Typ und Darstellung wie gefordert?
□ Metadata vollständig?
```
**Erst nach 100% Compliance: Commit + Push**
---
## 3. Warnsignale für Fehlverhalten
**Wenn der User sagt:**
- "Das steht aber im Konzept"
- "Es fehlt X" (nach Deploy)
- "Überprüfe das Konzept"
- "Das ist nicht wie gefordert"
**→ SOFORT STOPPEN**
- Konzept nochmal vollständig lesen
- Gap-Analyse machen: Was fehlt?
- Nachfragen, nicht raten
- Konzept-konform überarbeiten
---
## 4. Skill-Integration
Für komplexe Features (>10 Funktionen, >3 Module):
```bash
/implement-feature <feature-name> <konzept-datei>
```
Der Skill führt automatisch Stufe 1-3 aus und wartet auf Approval.
---
## 5. Ausnahmen
**Einzige erlaubte Ausnahme:**
User sagt explizit: "Ignoriere das Konzept" oder "Mach es anders als im Konzept"
**Alle anderen Fälle:** Prozess anwenden.
---
## 6. Eskalation bei Unklarheiten
**Bei Unklarheiten im Konzept:**
1. **Niemals raten oder "ähnliches" bauen**
2. **Immer nachfragen:**
- "Im Konzept steht X, aber unklar ist Y. Wie soll ich vorgehen?"
- "Option A oder Option B?"
3. **Warten auf Antwort**
4. **Konzept mit Antwort aktualisieren**
---
## Zusammenfassung: 5-Stufen-Checkliste
```
□ 1. Anforderungsanalyse (Konzept vollständig lesen, Checkliste erstellen)
□ 2. Umsetzungskonzept (Backend + Frontend + Datenquellen dokumentieren)
□ 3. User-Approval (Konzept zeigen, auf OK warten)
□ 4. Implementierung (Exakt nach Konzept, keine Abkürzungen)
□ 5. Compliance-Check (100% Checkliste abhaken vor Commit)
```
**Kein Schritt darf übersprungen werden.**

View File

@ -0,0 +1,186 @@
# Lessons Learned
Fehler die gemacht wurden damit sie nicht wiederholt werden.
## 1. Feature-Enforcement Rollback (20.03.2026)
**Was:** Membership-System Feature-Enforcement implementiert
**Problem:** Brach Analyse-Verlauf, Export-Sichtbarkeit und Zähler
**Rollback:** Commit 4fcde4a
**Lösung:** Einfaches ai_enabled + ai_limit_day System aktiv
**Regel:** Feature-Enforcement nie ohne vollständige Test-Suite aktivieren.
Zuerst Shadow-Mode (loggen aber nicht blockieren), dann schrittweise aktivieren.
## 2. session=Depends(require_auth) innerhalb Header()
**Was:** Automatisches Einfügen von Auth in bestehende Endpoints
**Problem:** `session` wurde innerhalb `Header(default=None, session=...)` eingebettet
**Folge:** FastAPI ignorierte Auth stillschweigend Endpoint ungeschützt
**Lösung:** session immer als separater Parameter
```python
# ❌ Was passiert ist:
def endpoint(x: str = Header(default=None, session=Depends(require_auth))):
# ✅ Korrekt:
def endpoint(x: str = Header(default=None), session: dict = Depends(require_auth)):
```
## 3. PostgreSQL Migration apt-get Probleme
**Was:** Docker Build mit apt-get postgresql-client
**Problem:** Build hing 30+ Minuten
**Lösung:** Reine Python-Lösung mit psycopg2-binary, kein apt-get
## 4. SQLite → PostgreSQL Datenmigration
**Probleme:**
- Leere date-Strings (`''`) → PostgreSQL wirft Fehler → zu NULL konvertieren
- Boolean: SQLite `0/1` → PostgreSQL `true/false`
- `?` Platzhalter → `%s`
## 5. dayjs.week() Plugin fehlt
**Was:** dayjs().week() ohne isoWeek-Plugin aufgerufen
**Problem:** Weißer Screen auf Verlauf/Ernährung
**Lösung:** Native ISO-Wochenberechnung (siehe FRONTEND.md)
## 6. Bun Crash bei langen Claude Code Sessions
**Was:** Claude Code CLI lief >30 Minuten
**Problem:** Bun (JS Runtime) crashed mit "Illegal instruction"
**Lösung:** Bei komplexen Tasks früher committen und neue Session starten
## 7. Docker Cache nach Dateiänderung
**Was:** main.py auf Pi kopiert, Container neu gestartet
**Problem:** Docker nutzte gecachten Layer alte Datei im Container
**Lösung:** Immer `--no-cache` bei Änderungen am Code:
```bash
docker compose build --no-cache backend
```
## 8. Placeholder Registry Framework Kritische Learnings (02.04.2026)
**Was:** Implementation von 14 Nutrition Placeholders mit neuem Registry Framework
### 8.1 OutputType.TEXT existiert nicht
**Problem:** `OutputType.TEXT` verursachte AttributeError
**Richtig:** `OutputType.STRING` für Text-Outputs
**Verfügbar:** NUMERIC, STRING, BOOLEAN, JSON, LIST, TEXT_SUMMARY
```python
# ❌ Falsch:
output_type=OutputType.TEXT
# ✅ Richtig:
output_type=OutputType.STRING
```
### 8.2 Time Window "mixed" ist problematisch
**Problem:** `time_window="mixed"` unklar für Export und Consumers
**Lösung:** Dominante Zeitkomponente wählen, Rest als Kommentar
```python
# ❌ Unklar:
time_window="mixed"
# ✅ Klar:
time_window="7d" # protein 7d avg; weight ist snapshot (secondary)
```
### 8.3 Units müssen präzise sein
**Problem:** Unpräzise Units führen zu Interpretationsproblemen
```python
# ❌ Unpräzise:
unit="score"
unit="kcal"
# ✅ Präzise:
unit="score (0-100)"
unit="kcal/day"
```
### 8.4 Date Aggregation bei CSV-Imports
**Problem:** Mehrere Einträge pro Tag → falsche "Tage"-Zählung
**Lösung:** Immer `GROUP BY date, SUM()` für Tages-Aggregation
```python
# ❌ Falsch (zählt Einträge):
SELECT protein_g FROM nutrition_log WHERE date >= ...
# ✅ Richtig (zählt Tage):
SELECT date, SUM(protein_g) as daily_protein
FROM nutrition_log
WHERE date >= ...
GROUP BY date
```
**Betroffen:** Alle Funktionen die "Tage" zählen oder daily averages berechnen
### 8.5 Evidence-Based = Nie Raten
**Problem:** CODE_DERIVED falsch gesetzt ohne Code-Inspektion
**Lösung:** Bei Unsicherheit UNRESOLVED oder TO_VERIFY nutzen
```python
# Wenn unklar:
metadata.set_evidence("field", EvidenceType.UNRESOLVED) # ehrlich
# Nicht:
metadata.set_evidence("field", EvidenceType.CODE_DERIVED) # halluziniert
```
### 8.6 Known Limitations = Dokumentations-Gold
**Problem:** Inkonsistenzen/Bugs verschwiegen
**Erfolg:** Transparent dokumentiert in `known_limitations`
**Beispiel:**
```python
known_limitations=(
"KRITISCHE INKONSISTENZ: Protein ist geglättet (7d average), "
"Gewicht ist single-point (latest). Anfällig für Gewichts-Outlier."
)
```
**Regel:** Probleme dokumentieren statt verstecken. User entscheidet über Fixes.
### 8.7 Formeln explizit dokumentieren
**Problem:** "Berechnet Score" zu vage, nicht reproduzierbar
**Erfolg:** Alle Formeln, Thresholds, TDEE-Modelle explizit dokumentiert
**Beispiel:**
```python
known_limitations=(
"TDEE-MODELL: weight_kg × 32.5 (vereinfacht). "
"NICHT berücksichtigt: Aktivitätslevel, Alter, Geschlecht."
)
```
### 8.8 No-Change Requirement ist absolut
**Problem:** Versuchung, offensichtliche Bugs zu fixen
**Regel:** NUR dokumentieren, User entscheidet
**Beispiel:** `protein_g_per_kg` hat Zeitfenster-Inkonsistenz (7d protein / latest weight)
→ Dokumentiert in known_limitations, NICHT gefixed
### 8.9 Testing nach jedem Deploy
**Problem:** Fehler erst nach komplettem Cluster entdeckt
**Erfolg:** Testing nach jedem Part (A, B, C) → frühe Fehlerkennung
**Workflow:**
1. Deploy Part A
2. Test Export
3. Werte verifizieren
4. Erst dann Part B
### 8.10 .claude in .gitignore
**Problem:** Versuch, `.claude/task/` Files zu committen
**Lösung:** Nur `backend/` Code committen, `.claude/` ist local docs
---
**Zusammenfassung Nutrition Cluster:**
- 14 Placeholders erfolgreich implementiert
- 2 Bugs gefunden und behoben (OutputType, Date Aggregation)
- 2 Metadaten-Inkonsistenzen korrigiert (time_window, unit)
- Alle kritischen Formeln dokumentiert
- Framework bewährt und skalierbar

View File

@ -0,0 +1,32 @@
name: Deploy Development
on:
push:
branches: [develop]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Deploy to Dev Server
uses: appleboy/ssh-action@master
with:
host: 192.168.2.49
username: lars
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/lars/docker/shinkan-dev
# Pull latest code
git pull origin develop || (git clone http://192.168.2.144:3000/Lars/shinkan-jinkendo.git . && git checkout develop)
# Build and restart containers
docker compose -f docker-compose.dev-env.yml down
docker compose -f docker-compose.dev-env.yml build --no-cache
docker compose -f docker-compose.dev-env.yml up -d
# Show status
docker compose -f docker-compose.dev-env.yml ps

View File

@ -0,0 +1,32 @@
name: Deploy Production
on:
push:
branches: [main]
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v3
- name: Deploy to Production Server
uses: appleboy/ssh-action@master
with:
host: 192.168.2.49
username: lars
key: ${{ secrets.SSH_PRIVATE_KEY }}
script: |
cd /home/lars/docker/shinkan
# Pull latest code
git pull origin main || (git clone http://192.168.2.144:3000/Lars/shinkan-jinkendo.git . && git checkout main)
# Build and restart containers
docker compose down
docker compose build --no-cache
docker compose up -d
# Show status
docker compose ps

24
backend/Dockerfile Normal file
View File

@ -0,0 +1,24 @@
FROM python:3.12-slim
WORKDIR /app
# Install system dependencies
RUN apt-get update && apt-get install -y \
postgresql-client \
&& rm -rf /var/lib/apt/lists/*
# Copy requirements and install dependencies
COPY requirements.txt .
RUN pip install --no-cache-dir -r requirements.txt
# Copy application code
COPY . .
# Create media directory
RUN mkdir -p /app/media
# Expose port
EXPOSE 8000
# Run application
CMD ["uvicorn", "main:app", "--host", "0.0.0.0", "--port", "8000"]

View File

@ -0,0 +1,114 @@
-- Migration 001: Auth & Membership (von Mitai übernommen, angepasst)
-- Erstellt: 2026-04-21
-- Beschreibung: Basis-Auth-System und Membership-Infrastruktur
-- Profiles (Nutzer)
CREATE TABLE profiles (
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
pin_hash VARCHAR(255) NOT NULL,
name VARCHAR(200),
role VARCHAR(50) DEFAULT 'user',
tier VARCHAR(50) DEFAULT 'free',
email_verified BOOLEAN DEFAULT false,
verification_token VARCHAR(255),
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_profiles_email ON profiles(email);
CREATE INDEX idx_profiles_role ON profiles(role);
-- Sessions (Auth-Tokens)
CREATE TABLE sessions (
id SERIAL PRIMARY KEY,
profile_id INT REFERENCES profiles(id) ON DELETE CASCADE,
token VARCHAR(255) UNIQUE NOT NULL,
created_at TIMESTAMP DEFAULT NOW(),
expires_at TIMESTAMP NOT NULL
);
CREATE INDEX idx_sessions_token ON sessions(token);
CREATE INDEX idx_sessions_profile ON sessions(profile_id);
-- Features (Feature-Definitionen)
CREATE TABLE features (
id SERIAL PRIMARY KEY,
name VARCHAR(100) UNIQUE NOT NULL,
display_name VARCHAR(200),
description TEXT,
created_at TIMESTAMP DEFAULT NOW()
);
-- Tier Limits (Limits pro Tier)
CREATE TABLE tier_limits (
id SERIAL PRIMARY KEY,
tier VARCHAR(50) NOT NULL,
feature_id INT REFERENCES features(id),
limit_value INT, -- -1 = unlimited
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(tier, feature_id)
);
-- Subscriptions (Nutzer-Subscriptions)
CREATE TABLE subscriptions (
id SERIAL PRIMARY KEY,
profile_id INT REFERENCES profiles(id) ON DELETE CASCADE,
tier VARCHAR(50) NOT NULL,
status VARCHAR(50) DEFAULT 'trial',
start_date DATE NOT NULL,
end_date DATE,
created_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_subscriptions_profile ON subscriptions(profile_id);
-- User Feature Usage (Tracking)
CREATE TABLE user_feature_usage (
id SERIAL PRIMARY KEY,
profile_id INT REFERENCES profiles(id) ON DELETE CASCADE,
feature_id INT REFERENCES features(id),
usage_count INT DEFAULT 0,
last_used TIMESTAMP,
last_reset TIMESTAMP DEFAULT NOW(),
created_at TIMESTAMP DEFAULT NOW(),
UNIQUE(profile_id, feature_id)
);
CREATE INDEX idx_usage_profile_feature ON user_feature_usage(profile_id, feature_id);
-- Insert Default Features (Shinkan-spezifisch)
INSERT INTO features (name, display_name, description) VALUES
('exercises', 'Übungen', 'Anzahl Übungen pro Verein'),
('training_units', 'Trainingseinheiten', 'Anzahl Trainingseinheiten pro Monat'),
('training_programs', 'Trainingsprogramme', 'Anzahl aktive Trainingsprogramme'),
('exercise_media', 'Medien-Uploads', 'Anzahl Medien-Uploads pro Monat'),
('wiki_import', 'MediaWiki-Import', 'Zugriff auf MediaWiki-Import'),
('ai_analysis', 'KI-Analysen', 'Anzahl KI-Analysen pro Monat (zukünftig)');
-- Insert Default Tier Limits
-- Free Tier
INSERT INTO tier_limits (tier, feature_id, limit_value)
SELECT 'free', id,
CASE
WHEN name = 'exercises' THEN 50
WHEN name = 'training_units' THEN 20
WHEN name = 'training_programs' THEN 2
WHEN name = 'exercise_media' THEN 10
WHEN name = 'wiki_import' THEN 0
WHEN name = 'ai_analysis' THEN 0
END
FROM features;
-- Premium Tier
INSERT INTO tier_limits (tier, feature_id, limit_value)
SELECT 'premium', id,
CASE
WHEN name = 'exercises' THEN -1 -- unlimited
WHEN name = 'training_units' THEN -1
WHEN name = 'training_programs' THEN -1
WHEN name = 'exercise_media' THEN 100
WHEN name = 'wiki_import' THEN 1
WHEN name = 'ai_analysis' THEN 50
END
FROM features;

View File

@ -0,0 +1,52 @@
-- Migration 002: Organization (Clubs, Divisions, Training Groups)
-- Erstellt: 2026-04-21
-- Beschreibung: Organisationsstruktur für Vereine und Trainingsgruppen
-- Clubs (Vereine)
CREATE TABLE clubs (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
abbreviation VARCHAR(50),
description TEXT,
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_clubs_status ON clubs(status);
-- Divisions (Sparten) - optional
CREATE TABLE divisions (
id SERIAL PRIMARY KEY,
club_id INT REFERENCES clubs(id) ON DELETE CASCADE,
name VARCHAR(200) NOT NULL,
focus_area VARCHAR(100), -- karate, selbstverteidigung, gewaltschutz
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_divisions_club ON divisions(club_id);
-- Training Groups (Trainingsgruppen)
CREATE TABLE training_groups (
id SERIAL PRIMARY KEY,
club_id INT REFERENCES clubs(id) ON DELETE CASCADE,
division_id INT REFERENCES divisions(id),
name VARCHAR(200) NOT NULL,
focus VARCHAR(100),
level VARCHAR(50),
age_group VARCHAR(50),
weekday VARCHAR(20),
time_start TIME,
time_end TIME,
location VARCHAR(200),
trainer_id INT REFERENCES profiles(id),
co_trainer_ids JSONB, -- [1, 2, 3]
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_groups_club ON training_groups(club_id);
CREATE INDEX idx_groups_trainer ON training_groups(trainer_id);
CREATE INDEX idx_groups_status ON training_groups(status);

View File

@ -0,0 +1,64 @@
-- Migration 003: Catalogs (Skills & Training Methods)
-- Erstellt: 2026-04-21
-- Beschreibung: Fähigkeiten- und Methodenkataloge
-- Skills (Fähigkeiten) - global
CREATE TABLE skills (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
category VARCHAR(100), -- kihon, kumite, kata, selbstverteidigung, fitness
description TEXT,
importance INT CHECK (importance BETWEEN 1 AND 5),
keywords JSONB,
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_skills_category ON skills(category);
CREATE INDEX idx_skills_status ON skills(status);
-- Training Methods (Trainingsmethoden) - global
CREATE TABLE training_methods (
id SERIAL PRIMARY KEY,
name VARCHAR(200) NOT NULL,
abbreviation VARCHAR(20),
category VARCHAR(100), -- intervall, rollenspiel, zirkel, koordination, etc.
description TEXT,
typical_duration INT, -- in Minuten
typical_group_size VARCHAR(50),
related_skills JSONB, -- [skill_id, skill_id, ...]
keywords JSONB,
status VARCHAR(50) DEFAULT 'active',
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_methods_category ON training_methods(category);
CREATE INDEX idx_methods_status ON training_methods(status);
-- Insert Basis-Skills (Beispiele)
INSERT INTO skills (name, category, description, importance) VALUES
('Dachi Waza', 'kihon', 'Standtechniken und Körperhaltung', 5),
('Tsuki Waza', 'kihon', 'Fausttechniken', 5),
('Keri Waza', 'kihon', 'Fußtechniken', 5),
('Uke Waza', 'kihon', 'Abwehrtechniken', 5),
('Distanzkontrolle', 'kumite', 'Kontrolle der Kampfdistanz', 4),
('Beinarbeit', 'kumite', 'Fußarbeit und Bewegung', 4),
('Reaktionsfähigkeit', 'kumite', 'Schnelle Reaktion auf Angriffe', 4),
('Aufmerksamkeit', 'selbstverteidigung', 'Gefahrenbewusstsein und Aufmerksamkeit', 5),
('Selbstbehauptung', 'selbstverteidigung', 'Selbstsicheres Auftreten', 5),
('Ausdauer', 'fitness', 'Kardiovaskuläre Ausdauer', 3),
('Kraft', 'fitness', 'Muskelkraft', 3),
('Flexibilität', 'fitness', 'Beweglichkeit', 3);
-- Insert Basis-Methods (Beispiele)
INSERT INTO training_methods (name, abbreviation, category, description, typical_duration) VALUES
('Intervalltraining', 'INT', 'kondition', 'Wechsel zwischen Belastung und Erholung', 20),
('Zirkeltraining', 'ZIR', 'kondition', 'Mehrere Stationen mit verschiedenen Übungen', 30),
('Rollenspiel', 'ROL', 'didaktik', 'Szenario-basiertes Training', 15),
('Strukturierte Übung', 'STR', 'didaktik', 'Schrittweise Anleitung einer Technik', 10),
('Partnerübung', 'PAR', 'didaktik', 'Training zu zweit', 15),
('Koordinationstraining', 'KOO', 'koordination', 'Schulung von Koordination und Balance', 15),
('Dauermethode', 'DAU', 'kondition', 'Kontinuierliche Belastung ohne Pausen', 20),
('Plyometrisches Training', 'PLY', 'kraft', 'Explosivkraft durch Sprungübungen', 15);

View File

@ -0,0 +1,69 @@
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: dev-shinkan-postgres
environment:
POSTGRES_DB: shinkan_dev
POSTGRES_USER: shinkan_dev
POSTGRES_PASSWORD: dev_password
volumes:
- dev-shinkan-db-data:/var/lib/postgresql/data
ports:
- "5435:5432"
restart: unless-stopped
networks:
- dev-shinkan-network
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: dev-shinkan-api
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: shinkan_dev
DB_USER: shinkan_dev
DB_PASSWORD: dev_password
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
OPENROUTER_MODEL: ${OPENROUTER_MODEL}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_USER: ${SMTP_USER}
SMTP_PASS: ${SMTP_PASS}
SMTP_FROM: ${SMTP_FROM}
APP_URL: https://dev.shinkan.jinkendo.de
ALLOWED_ORIGINS: https://dev.shinkan.jinkendo.de
ENVIRONMENT: development
volumes:
- dev-shinkan-media:/app/media
ports:
- "8098:8000"
depends_on:
- postgres
restart: unless-stopped
networks:
- dev-shinkan-network
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
VITE_API_URL: https://dev.shinkan.jinkendo.de
container_name: dev-shinkan-ui
ports:
- "3098:80"
restart: unless-stopped
networks:
- dev-shinkan-network
volumes:
dev-shinkan-db-data:
dev-shinkan-media:
networks:
dev-shinkan-network:
driver: bridge

69
docker-compose.yml Normal file
View File

@ -0,0 +1,69 @@
version: '3.8'
services:
postgres:
image: postgres:16-alpine
container_name: shinkan-db-prod
environment:
POSTGRES_DB: shinkan
POSTGRES_USER: shinkan_user
POSTGRES_PASSWORD: ${DB_PASSWORD}
volumes:
- shinkan-db-data:/var/lib/postgresql/data
ports:
- "5434:5432"
restart: unless-stopped
networks:
- shinkan-network
backend:
build:
context: ./backend
dockerfile: Dockerfile
container_name: shinkan-api
environment:
DB_HOST: postgres
DB_PORT: 5432
DB_NAME: shinkan
DB_USER: shinkan_user
DB_PASSWORD: ${DB_PASSWORD}
OPENROUTER_API_KEY: ${OPENROUTER_API_KEY}
OPENROUTER_MODEL: ${OPENROUTER_MODEL}
SMTP_HOST: ${SMTP_HOST}
SMTP_PORT: ${SMTP_PORT}
SMTP_USER: ${SMTP_USER}
SMTP_PASS: ${SMTP_PASS}
SMTP_FROM: ${SMTP_FROM}
APP_URL: https://shinkan.jinkendo.de
ALLOWED_ORIGINS: https://shinkan.jinkendo.de
ENVIRONMENT: production
volumes:
- shinkan-media:/app/media
ports:
- "8003:8000"
depends_on:
- postgres
restart: unless-stopped
networks:
- shinkan-network
frontend:
build:
context: ./frontend
dockerfile: Dockerfile
args:
VITE_API_URL: https://shinkan.jinkendo.de
container_name: shinkan-ui
ports:
- "3003:80"
restart: unless-stopped
networks:
- shinkan-network
volumes:
shinkan-db-data:
shinkan-media:
networks:
shinkan-network:
driver: bridge

34
frontend/Dockerfile Normal file
View File

@ -0,0 +1,34 @@
# Build stage
FROM node:20-alpine AS build
WORKDIR /app
# Copy package files
COPY package*.json ./
# Install dependencies
RUN npm install
# Copy source code
COPY . .
# Build argument for API URL
ARG VITE_API_URL
ENV VITE_API_URL=$VITE_API_URL
# Build app
RUN npm run build
# Production stage
FROM nginx:alpine
# Copy built app
COPY --from=build /app/dist /usr/share/nginx/html
# Copy nginx config (if exists)
# COPY nginx.conf /etc/nginx/nginx.conf
# Expose port
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]

14
frontend/index.html Normal file
View File

@ -0,0 +1,14 @@
<!doctype html>
<html lang="de">
<head>
<meta charset="UTF-8" />
<link rel="icon" type="image/svg+xml" href="/vite.svg" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content="Shinkan Jinkendo - Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung" />
<title>Shinkan Jinkendo</title>
</head>
<body>
<div id="root"></div>
<script type="module" src="/src/main.jsx"></script>
</body>
</html>

19
frontend/package.json Normal file
View File

@ -0,0 +1,19 @@
{
"name": "shinkan-jinkendo-frontend",
"version": "0.1.0",
"type": "module",
"scripts": {
"dev": "vite --port 3098",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-router-dom": "^6.22.0"
},
"devDependencies": {
"@vitejs/plugin-react": "^4.2.1",
"vite": "^5.1.4"
}
}

53
frontend/src/App.jsx Normal file
View File

@ -0,0 +1,53 @@
import React, { useState, useEffect } from 'react'
import { BrowserRouter as Router, Routes, Route, Navigate } from 'react-router-dom'
import { AuthProvider } from './context/AuthContext'
function App() {
const [version, setVersion] = useState(null)
useEffect(() => {
// Load version from API
fetch('/api/version')
.then(res => res.json())
.then(data => setVersion(data))
.catch(err => console.error('Failed to load version:', err))
}, [])
return (
<AuthProvider>
<Router>
<div className="app">
<div className="container">
<h1>🥋 Shinkan Jinkendo</h1>
<p>Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung</p>
{version && (
<div className="card" style={{ marginTop: '2rem' }}>
<h3>System Status</h3>
<p><strong>Version:</strong> {version.app_version}</p>
<p><strong>Build:</strong> {version.build_date}</p>
<p><strong>Environment:</strong> {version.environment}</p>
<p><strong>DB Schema:</strong> {version.db_schema_version}</p>
</div>
)}
<div className="card" style={{ marginTop: '2rem' }}>
<h3>🚧 In Entwicklung</h3>
<p>Die App wird gerade aufgebaut.</p>
<ul style={{ textAlign: 'left', marginTop: '1rem' }}>
<li> Backend-Basis</li>
<li> Docker-Setup</li>
<li> Datenbank-Schema</li>
<li>🔲 Auth-System</li>
<li>🔲 Übungsverwaltung</li>
<li>🔲 Trainingsplanung</li>
</ul>
</div>
</div>
</div>
</Router>
</AuthProvider>
)
}
export default App

1330
frontend/src/app.css Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,133 @@
import { createContext, useContext, useState, useEffect } from 'react'
const AuthContext = createContext(null)
const TOKEN_KEY = 'bodytrack_token'
const PROFILE_KEY = 'bodytrack_active_profile'
export function AuthProvider({ children }) {
const [session, setSession] = useState(null) // {token, profile_id, role}
const [loading, setLoading] = useState(true)
const [needsSetup, setNeedsSetup] = useState(false)
useEffect(() => {
checkStatus()
}, [])
const checkStatus = async () => {
try {
const r = await fetch('/api/auth/status')
const status = await r.json()
if (status.needs_setup) {
setNeedsSetup(true)
setLoading(false)
return
}
// Try existing token
const token = localStorage.getItem(TOKEN_KEY)
if (token) {
const me = await fetch('/api/auth/me', {
headers: { 'X-Auth-Token': token }
})
if (me.ok) {
const profile = await me.json()
setSession({ token, profile_id: profile.id, role: profile.role, profile })
setLoading(false)
return
}
// Token expired
localStorage.removeItem(TOKEN_KEY)
}
} catch(e) {
console.error('Auth check failed', e)
}
setLoading(false)
}
const login = async (credentials) => {
// Support both new {email, pin} and legacy {profile_id, pin}
const body = typeof credentials === 'object' ? credentials : { profile_id: credentials }
const r = await fetch('/api/auth/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(body)
})
if (!r.ok) {
const err = await r.json()
throw new Error(err.detail || 'Login fehlgeschlagen')
}
const data = await r.json()
localStorage.setItem(TOKEN_KEY, data.token)
localStorage.setItem(PROFILE_KEY, data.profile_id)
// Fetch full profile
const me = await fetch('/api/auth/me', { headers: { 'X-Auth-Token': data.token } })
const profile = await me.json()
setSession({ token: data.token, profile_id: data.profile_id, role: data.role, profile })
return data
}
const setup = async (formData) => {
const r = await fetch('/api/auth/setup', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(formData)
})
if (!r.ok) {
const err = await r.json()
throw new Error(err.detail || 'Setup fehlgeschlagen')
}
const data = await r.json()
localStorage.setItem(TOKEN_KEY, data.token)
localStorage.setItem(PROFILE_KEY, data.profile_id)
setNeedsSetup(false)
await checkStatus()
return data
}
const setAuthFromToken = (token, profile) => {
// Direct token/profile set (for email verification auto-login)
localStorage.setItem(TOKEN_KEY, token)
localStorage.setItem(PROFILE_KEY, profile.id)
setSession({
token,
profile_id: profile.id,
role: profile.role || 'user',
profile
})
}
const logout = async () => {
const token = localStorage.getItem(TOKEN_KEY)
if (token) {
await fetch('/api/auth/logout', { method: 'POST', headers: { 'X-Auth-Token': token } })
}
localStorage.removeItem(TOKEN_KEY)
setSession(null)
}
const isAdmin = session?.role === 'admin'
const canUseAI = session?.profile?.ai_enabled !== 0
const canExport = session?.profile?.export_enabled !== 0
return (
<AuthContext.Provider value={{
session, loading, needsSetup,
login, setup, logout, setAuthFromToken,
isAdmin, canUseAI, canExport,
token: session?.token,
profileId: session?.profile_id,
}}>
{children}
</AuthContext.Provider>
)
}
export function useAuth() {
return useContext(AuthContext)
}
export function getToken() {
return localStorage.getItem(TOKEN_KEY)
}

View File

@ -0,0 +1,63 @@
import { createContext, useContext, useState, useEffect } from 'react'
import { useAuth } from './AuthContext'
const ProfileContext = createContext(null)
export function ProfileProvider({ children }) {
const { session } = useAuth()
const [profiles, setProfiles] = useState([])
const [activeProfile, setActiveProfileState] = useState(null)
const [loading, setLoading] = useState(true)
const loadProfiles = async () => {
try {
const token = localStorage.getItem('bodytrack_token') || ''
const res = await fetch('/api/profiles', {
headers: { 'X-Auth-Token': token }
})
if (!res.ok) return []
return await res.json()
} catch(e) { return [] }
}
// Re-load whenever session changes (login/logout/switch)
useEffect(() => {
if (!session) {
setActiveProfileState(null)
setProfiles([])
setLoading(false)
return
}
setLoading(true)
loadProfiles().then(data => {
setProfiles(data)
// Always use the profile_id from the session token not localStorage
const match = data.find(p => p.id === session.profile_id)
setActiveProfileState(match || data[0] || null)
setLoading(false)
})
}, [session?.profile_id]) // re-runs when profile changes
const setActiveProfile = (profile) => {
setActiveProfileState(profile)
localStorage.setItem('bodytrack_active_profile', profile.id)
}
const refreshProfiles = () => loadProfiles().then(data => {
setProfiles(data)
if (activeProfile) {
const updated = data.find(p => p.id === activeProfile.id)
if (updated) setActiveProfileState(updated)
}
})
return (
<ProfileContext.Provider value={{ profiles, activeProfile, setActiveProfile, refreshProfiles, loading }}>
{children}
</ProfileContext.Provider>
)
}
export function useProfile() {
return useContext(ProfileContext)
}

10
frontend/src/main.jsx Normal file
View File

@ -0,0 +1,10 @@
import React from 'react'
import ReactDOM from 'react-dom/client'
import App from './App.jsx'
import './app.css'
ReactDOM.createRoot(document.getElementById('root')).render(
<React.StrictMode>
<App />
</React.StrictMode>,
)

223
frontend/src/utils/api.js Normal file
View File

@ -0,0 +1,223 @@
/**
* Shinkan Jinkendo API Client
*
* Zentrale API-Kommunikation mit automatischer Token-Injektion
*/
const API_URL = import.meta.env.VITE_API_URL || ''
/**
* Generic API request with automatic token injection
*/
async function request(endpoint, options = {}) {
const token = localStorage.getItem('authToken')
const headers = {
'Content-Type': 'application/json',
...options.headers
}
if (token) {
headers['X-Auth-Token'] = token
}
const response = await fetch(`${API_URL}${endpoint}`, {
...options,
headers
})
if (!response.ok) {
const error = await response.json().catch(() => ({ detail: 'Unknown error' }))
throw new Error(error.detail || `HTTP ${response.status}`)
}
return response.json()
}
// ============================================================================
// Auth
// ============================================================================
export async function login(email, password) {
return request('/api/auth/login', {
method: 'POST',
body: JSON.stringify({ email, password })
})
}
export async function register(email, password, name) {
return request('/api/auth/register', {
method: 'POST',
body: JSON.stringify({ email, password, name })
})
}
export async function logout() {
return request('/api/auth/logout', { method: 'POST' })
}
export async function getCurrentProfile() {
return request('/api/profiles/me')
}
// ============================================================================
// Clubs & Groups
// ============================================================================
export async function listClubs() {
return request('/api/clubs')
}
export async function createClub(data) {
return request('/api/clubs', {
method: 'POST',
body: JSON.stringify(data)
})
}
export async function listTrainingGroups(clubId) {
const query = clubId ? `?club_id=${clubId}` : ''
return request(`/api/groups${query}`)
}
export async function createTrainingGroup(data) {
return request('/api/groups', {
method: 'POST',
body: JSON.stringify(data)
})
}
// ============================================================================
// Skills & Methods
// ============================================================================
export async function listSkills() {
return request('/api/skills')
}
export async function createSkill(data) {
return request('/api/skills', {
method: 'POST',
body: JSON.stringify(data)
})
}
export async function listMethods() {
return request('/api/methods')
}
export async function createMethod(data) {
return request('/api/methods', {
method: 'POST',
body: JSON.stringify(data)
})
}
// ============================================================================
// Exercises
// ============================================================================
export async function listExercises(filters = {}) {
const query = new URLSearchParams(filters).toString()
return request(`/api/exercises${query ? '?' + query : ''}`)
}
export async function getExercise(id) {
return request(`/api/exercises/${id}`)
}
export async function createExercise(data) {
return request('/api/exercises', {
method: 'POST',
body: JSON.stringify(data)
})
}
export async function updateExercise(id, data) {
return request(`/api/exercises/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
})
}
export async function deleteExercise(id) {
return request(`/api/exercises/${id}`, { method: 'DELETE' })
}
// ============================================================================
// Training Planning
// ============================================================================
export async function listTrainingUnits(groupId, startDate, endDate) {
const query = new URLSearchParams({ group_id: groupId, start_date: startDate, end_date: endDate }).toString()
return request(`/api/training-units?${query}`)
}
export async function getTrainingUnit(id) {
return request(`/api/training-units/${id}`)
}
export async function createTrainingUnit(data) {
return request('/api/training-units', {
method: 'POST',
body: JSON.stringify(data)
})
}
export async function updateTrainingUnit(id, data) {
return request(`/api/training-units/${id}`, {
method: 'PUT',
body: JSON.stringify(data)
})
}
// ============================================================================
// Version & Health
// ============================================================================
export async function getVersion() {
return request('/api/version')
}
export async function healthCheck() {
return request('/health')
}
export const api = {
// Auth
login,
register,
logout,
getCurrentProfile,
// Clubs & Groups
listClubs,
createClub,
listTrainingGroups,
createTrainingGroup,
// Skills & Methods
listSkills,
createSkill,
listMethods,
createMethod,
// Exercises
listExercises,
getExercise,
createExercise,
updateExercise,
deleteExercise,
// Training Planning
listTrainingUnits,
getTrainingUnit,
createTrainingUnit,
updateTrainingUnit,
// System
getVersion,
healthCheck
}
export default api

14
frontend/vite.config.js Normal file
View File

@ -0,0 +1,14 @@
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react'
export default defineConfig({
plugins: [react()],
server: {
port: 3098,
host: true
},
build: {
outDir: 'dist',
sourcemap: false
}
})