feat: Complete MVP setup - Docker, Frontend, Migrations, CI/CD
Some checks failed
Deploy Development / deploy (push) Failing after 4s
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:
parent
a426c03598
commit
b2bc8590c4
1202
.claude/docs/working/SHINKAN_PROJECT_SETUP.md
Normal file
1202
.claude/docs/working/SHINKAN_PROJECT_SETUP.md
Normal file
File diff suppressed because it is too large
Load Diff
470
.claude/rules/ARCHITECTURE.md
Normal file
470
.claude/rules/ARCHITECTURE.md
Normal 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.
|
||||
339
.claude/rules/ARCHITECTURE_old.md
Normal file
339
.claude/rules/ARCHITECTURE_old.md
Normal 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
|
||||
```
|
||||
100
.claude/rules/CODING_RULES.md
Normal file
100
.claude/rules/CODING_RULES.md
Normal 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
|
||||
```
|
||||
77
.claude/rules/DOCUMENTATION.md
Normal file
77
.claude/rules/DOCUMENTATION.md
Normal 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 (> 1–2 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
|
||||
249
.claude/rules/IMPLEMENTATION_RULES.md
Normal file
249
.claude/rules/IMPLEMENTATION_RULES.md
Normal 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.**
|
||||
186
.claude/rules/LESSONS_LEARNED.md
Normal file
186
.claude/rules/LESSONS_LEARNED.md
Normal 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
|
||||
32
.gitea/workflows/deploy-dev.yml
Normal file
32
.gitea/workflows/deploy-dev.yml
Normal 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
|
||||
32
.gitea/workflows/deploy-prod.yml
Normal file
32
.gitea/workflows/deploy-prod.yml
Normal 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
24
backend/Dockerfile
Normal 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"]
|
||||
114
backend/migrations/001_auth_membership.sql
Normal file
114
backend/migrations/001_auth_membership.sql
Normal 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;
|
||||
52
backend/migrations/002_organization.sql
Normal file
52
backend/migrations/002_organization.sql
Normal 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);
|
||||
64
backend/migrations/003_catalogs.sql
Normal file
64
backend/migrations/003_catalogs.sql
Normal 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);
|
||||
69
docker-compose.dev-env.yml
Normal file
69
docker-compose.dev-env.yml
Normal 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
69
docker-compose.yml
Normal 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
34
frontend/Dockerfile
Normal 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
14
frontend/index.html
Normal 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
19
frontend/package.json
Normal 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
53
frontend/src/App.jsx
Normal 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
1330
frontend/src/app.css
Normal file
File diff suppressed because it is too large
Load Diff
133
frontend/src/context/AuthContext.jsx
Normal file
133
frontend/src/context/AuthContext.jsx
Normal 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)
|
||||
}
|
||||
63
frontend/src/context/ProfileContext.jsx
Normal file
63
frontend/src/context/ProfileContext.jsx
Normal 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
10
frontend/src/main.jsx
Normal 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
223
frontend/src/utils/api.js
Normal 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
14
frontend/vite.config.js
Normal 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
|
||||
}
|
||||
})
|
||||
Loading…
Reference in New Issue
Block a user