diff --git a/.claude/docs/working/SHINKAN_PROJECT_SETUP.md b/.claude/docs/working/SHINKAN_PROJECT_SETUP.md new file mode 100644 index 0000000..0af3a3e --- /dev/null +++ b/.claude/docs/working/SHINKAN_PROJECT_SETUP.md @@ -0,0 +1,1202 @@ +# Shinkan Jinkendo – Technisches Setup + +> **Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung** +> Basis: Mitai Jinkendo Ökosystem +> Domain: shinkan.jinkendo.de (真観 - "Wahre Beobachtung") + +--- + +## 1. Fachliche Einordnung + +**Shinkan ist KEINE persönliche Tracking-App.** + +Shinkan ist eine **trainer- und vereinszentrierte Planungs- und Inhaltsplattform** mit Fokus auf: +- Übungsverwaltung und -suche +- Trainingsplanung für Gruppen +- Standardisierung und Wiederverwendung +- Freigabe und Governance von Inhalten +- Import aus bestehendem MediaWiki + +**Primäre Nutzer in MVP:** Trainer, Vereinsadmins, Redakteure +**Nicht in MVP:** Individuelle Sportler-Apps, persönliches Tracking, Gürtel-Tracking + +**Abgrenzung zu Mitai:** +- Mitai = persönliches Körper- und Trainings-Tracking +- Shinkan = Trainer-Arbeit, Übungen, Trainingsplanung, Vereinsstandards + +--- + +## 2. Übernommene Infrastruktur von Mitai + +| Komponente | Status | Verwendung | +|-----------|---------|------------| +| Auth-System | ✅ 1:1 übernehmen | Token + bcrypt | +| Membership-Basis | ✅ Übernehmen | Tiers, Features (vereinfacht) | +| User-Profiles | ✅ Übernehmen | Multi-Profile pro Account | +| Tech-Stack | ✅ 1:1 | React 18 + FastAPI + PostgreSQL 16 | +| Docker/Compose | ✅ 1:1 | Dev + Prod Umgebungen | +| PWA | ✅ 1:1 | Mobile + Desktop | +| Design-System | ✅ Basis | CSS-Variablen, mobile-first | +| Versioning | ✅ 1:1 | version.py + Migrations | +| Gitea CI/CD | ✅ 1:1 | Auto-Deploy dev/prod | + +**Ports:** +- Prod: 3003/8003 (Frontend/Backend) +- Dev: 3098/8098 (Frontend/Backend) + +--- + +## 3. Technische Basis (identisch zu Mitai) + +``` +Frontend: React 18 + Vite + PWA (Node 20) +Backend: FastAPI Python 3.12 +Datenbank: PostgreSQL 16 Alpine +Container: Docker + Docker Compose +Auth: Token-basiert + bcrypt +KI: OpenRouter API (optional, nicht MVP-kritisch) +``` + +--- + +## 4. Verzeichnisstruktur (angepasst) + +``` +shinkan-jinkendo/ +├── backend/ +│ ├── main.py # App-Setup + Router-Registration +│ ├── db.py # PostgreSQL Connection Pool +│ ├── db_init.py # DB-Init + Migrations +│ ├── auth.py # Hash, Verify, Sessions, Feature-Access +│ ├── models.py # Pydantic Models +│ ├── version.py # Versionskontrolle +│ ├── migrations/ # SQL-Migrationen (001_*.sql) +│ └── routers/ +│ ├── auth.py # Login, Register, Sessions +│ ├── profiles.py # User-Profiles +│ ├── clubs.py # Vereine / Sparten +│ ├── groups.py # Trainingsgruppen +│ ├── skills.py # Fähigkeiten (Admin + CRUD) +│ ├── methods.py # Trainingsmethoden (Admin + CRUD) +│ ├── exercises.py # Übungen (Kern-Modul) +│ ├── training_units.py # Trainingseinheiten +│ ├── training_programs.py # Trainingsprogramme +│ ├── planning.py # Planungsansichten +│ ├── import_wiki.py # MediaWiki-Import +│ ├── admin.py # Admin-Panel +│ └── membership.py # Subscription (vereinfacht) +│ +├── frontend/ +│ ├── src/ +│ │ ├── App.jsx # Root, Auth-Gates, Navigation +│ │ ├── app.css # CSS-Variablen + Styles +│ │ ├── version.js # Frontend-Versionierung +│ │ ├── config/ +│ │ │ ├── appNav.js # Hauptnavigation +│ │ │ └── adminNav.js # Admin-Navigation +│ │ ├── context/ +│ │ │ ├── AuthContext.jsx +│ │ │ └── ProfileContext.jsx +│ │ ├── pages/ +│ │ │ ├── Dashboard.jsx +│ │ │ ├── ExercisesPage.jsx # Übungssuche + CRUD +│ │ │ ├── PlanningPage.jsx # Trainingsplanung +│ │ │ ├── GroupsPage.jsx # Gruppenverwaltung +│ │ │ ├── SkillsPage.jsx # Fähigkeiten (Admin) +│ │ │ ├── MethodsPage.jsx # Methoden (Admin) +│ │ │ ├── ImportPage.jsx # MediaWiki-Import +│ │ │ └── SettingsPage.jsx +│ │ └── utils/ +│ │ ├── api.js # ALLE API-Calls +│ │ └── planning.js # Planungshelfer +│ └── public/ +│ ├── manifest.json # PWA-Manifest +│ └── icons/ +│ +├── .claude/ +│ ├── commands/ # Slash-Commands +│ ├── docs/ +│ │ ├── functional/ +│ │ │ └── SHINKAN_REQUIREMENTS.md # Anforderungsdokument +│ │ ├── technical/ +│ │ │ └── SHINKAN_DOMAIN_MODEL.md # Domänenmodell +│ │ └── rules/ # Architektur-Regeln (wie Mitai) +│ └── library/ +│ +├── docker-compose.yml # Production +├── docker-compose.dev-env.yml # Development +├── .env +├── .gitignore +└── CLAUDE.md # Agent-Einstieg +``` + +--- + +## 5. Domänenmodell (MVP Core) + +### 5.1 Organisationsstruktur + +```sql +-- Vereine +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() +) + +-- Sparten (optional, kann auch später) +divisions ( + id SERIAL PRIMARY KEY, + club_id INT REFERENCES clubs(id), + name VARCHAR(200) NOT NULL, + focus_area VARCHAR(100), -- karate, selbstverteidigung, gewaltschutz + created_at TIMESTAMP DEFAULT NOW() +) + +-- Trainingsgruppen +training_groups ( + id SERIAL PRIMARY KEY, + club_id INT REFERENCES clubs(id), + 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() +) +``` + +### 5.2 Fähigkeiten und Methoden (Kataloge) + +```sql +-- Fähigkeiten (global) +skills ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + category VARCHAR(100), -- kihon, kumite, kata, selbstverteidigung, fitness + description TEXT, + importance INT, -- 1-5 + keywords JSONB, + status VARCHAR(50) DEFAULT 'active', + created_at TIMESTAMP DEFAULT NOW() +) + +-- Trainingsmethoden (global) +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() +) +``` + +### 5.3 Übungen (Kernobjekt) + +```sql +-- Übungen +exercises ( + id SERIAL PRIMARY KEY, + title VARCHAR(300) NOT NULL, + summary TEXT, -- Kurzbeschreibung + goal TEXT NOT NULL, -- Ziel der Übung + execution TEXT NOT NULL, -- Durchführung + preparation TEXT, -- Vorbereitung / Aufbau + trainer_notes TEXT, -- Hinweise für Trainer + equipment JSONB, -- ["Matten", "Pratzen", "Bälle"] + + -- Metadaten + duration_min INT, + duration_max INT, + group_size_min INT, + group_size_max INT, + age_groups JSONB, -- ["minis", "kinder", "erwachsene"] + + -- Fachliche Zuordnung + focus_area VARCHAR(100), -- karate, selbstverteidigung, gewaltschutz + secondary_areas JSONB, + training_character VARCHAR(50), -- grundlage, aufbau, vertiefung, festigung, diagnose + + -- Methode + primary_method_id INT REFERENCES training_methods(id), + secondary_method_ids JSONB, + + -- Freigabe + visibility VARCHAR(50) DEFAULT 'private', -- private, club, official + status VARCHAR(50) DEFAULT 'draft', -- draft, in_review, approved, archived + created_by INT REFERENCES profiles(id), + club_id INT REFERENCES clubs(id), + + -- Import + import_source VARCHAR(50), -- mediawiki, manual + import_id VARCHAR(200), + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +) + +-- Übungs-Fähigkeiten (M:N) +exercise_skills ( + id SERIAL PRIMARY KEY, + exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, + skill_id INT REFERENCES skills(id), + is_primary BOOLEAN DEFAULT false, + intensity INT, -- 1-5 + development_contribution VARCHAR(50), -- low, medium, high + required_level INT, -- Eingangs-Niveau + target_level INT, -- Ziel-Niveau + created_at TIMESTAMP DEFAULT NOW() +) + +-- Übungsvarianten +exercise_variants ( + id SERIAL PRIMARY KEY, + exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, + variant_name VARCHAR(200) NOT NULL, + description TEXT, + execution_changes TEXT, + duration_min INT, + duration_max INT, + equipment_changes JSONB, + difficulty_adjustment VARCHAR(50), -- easier, harder, adapted + age_group_override JSONB, + skill_focus_override JSONB, + created_at TIMESTAMP DEFAULT NOW() +) + +-- Medien (1:N) +exercise_media ( + id SERIAL PRIMARY KEY, + exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, + media_type VARCHAR(50), -- image, video, document, sketch + file_path TEXT, + title VARCHAR(200), + description TEXT, + sort_order INT, + is_primary BOOLEAN DEFAULT false, + context VARCHAR(100), -- ablauf, detail, trainer_hint + created_at TIMESTAMP DEFAULT NOW() +) +``` + +### 5.4 Trainingsplanung + +```sql +-- Training Templates / Standards (vereinbar) +training_templates ( + id SERIAL PRIMARY KEY, + name VARCHAR(300) NOT NULL, + type VARCHAR(50), -- template, standard + club_id INT REFERENCES clubs(id), + division_id INT REFERENCES divisions(id), + + goal TEXT, + focus_areas JSONB, + duration_total INT, + + visibility VARCHAR(50) DEFAULT 'private', + status VARCHAR(50) DEFAULT 'active', + version INT DEFAULT 1, + created_by INT REFERENCES profiles(id), + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +) + +-- Trainingsabschnitte (für Templates + Units) +training_sections ( + id SERIAL PRIMARY KEY, + parent_type VARCHAR(50), -- template, unit + parent_id INT, + + title VARCHAR(200) NOT NULL, + section_type VARCHAR(100), -- aufwaermen, kihon, kumite, kata, abschluss, etc. + sort_order INT, + duration_planned INT, + goal TEXT, + notes TEXT, + + is_combination BOOLEAN DEFAULT false, -- Übungen als Komplex + combination_method_id INT REFERENCES training_methods(id), + + created_at TIMESTAMP DEFAULT NOW() +) + +-- Übungen in Abschnitten +section_exercises ( + id SERIAL PRIMARY KEY, + section_id INT REFERENCES training_sections(id) ON DELETE CASCADE, + exercise_id INT REFERENCES exercises(id), + variant_id INT REFERENCES exercise_variants(id), + sort_order INT, + duration_planned INT, + notes TEXT, + created_at TIMESTAMP DEFAULT NOW() +) + +-- Konkrete Trainingseinheiten +training_units ( + id SERIAL PRIMARY KEY, + group_id INT REFERENCES training_groups(id), + date DATE NOT NULL, + time_start TIME, + time_end TIME, + + derived_from_template_id INT REFERENCES training_templates(id), + derived_from_unit_id INT REFERENCES training_units(id), + + title VARCHAR(300), + goal TEXT, + focus_areas JSONB, + + -- Durchführung + actual_time_start TIME, + actual_time_end TIME, + completion_status VARCHAR(50), -- planned, in_progress, completed + + -- Reflexion + reflection_text TEXT, + what_worked_well TEXT, + what_to_improve TEXT, + + created_by INT REFERENCES profiles(id), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +) + +-- Trainingsprogramme +training_programs ( + id SERIAL PRIMARY KEY, + club_id INT REFERENCES clubs(id), + division_id INT REFERENCES divisions(id), + + name VARCHAR(300) NOT NULL, + description TEXT, + goal TEXT, + duration_weeks INT, + focus_areas JSONB, + + visibility VARCHAR(50) DEFAULT 'private', + status VARCHAR(50) DEFAULT 'active', + created_by INT REFERENCES profiles(id), + + created_at TIMESTAMP DEFAULT NOW() +) + +-- Programm-Einheiten (Reihenfolge) +program_units ( + id SERIAL PRIMARY KEY, + program_id INT REFERENCES training_programs(id) ON DELETE CASCADE, + unit_order INT, + template_id INT REFERENCES training_templates(id), + week_number INT, + notes TEXT, + created_at TIMESTAMP DEFAULT NOW() +) +``` + +### 5.5 Governance + +```sql +-- Änderungsanfragen (für geschützte Inhalte) +content_change_requests ( + id SERIAL PRIMARY KEY, + content_type VARCHAR(50), -- exercise, template, skill, method + content_id INT, + requested_by INT REFERENCES profiles(id), + change_description TEXT, + change_details JSONB, + status VARCHAR(50) DEFAULT 'pending', -- pending, approved, rejected + reviewed_by INT REFERENCES profiles(id), + review_notes TEXT, + created_at TIMESTAMP DEFAULT NOW(), + reviewed_at TIMESTAMP +) +``` + +### 5.6 MediaWiki-Import + +```sql +-- Import-Tracking +wiki_import_log ( + id SERIAL PRIMARY KEY, + import_type VARCHAR(50), -- skill, method, exercise + import_status VARCHAR(50), -- success, partial, failed + items_total INT, + items_imported INT, + items_failed INT, + error_log JSONB, + imported_by INT REFERENCES profiles(id), + created_at TIMESTAMP DEFAULT NOW() +) + +-- Import-Referenzen (für Re-Import-Schutz) +wiki_import_references ( + id SERIAL PRIMARY KEY, + wiki_page_title VARCHAR(300), + wiki_page_id VARCHAR(100), + content_type VARCHAR(50), -- skill, method, exercise + local_id INT, + last_imported TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(wiki_page_title, content_type) +) +``` + +--- + +## 6. MVP Core-Features (Release 1) + +### 6.1 Pflicht in MVP + +**Organisationsstruktur:** +- ✅ Verein anlegen/verwalten +- ✅ Trainingsgruppen anlegen/verwalten +- ✅ Trainer zuordnen + +**Kataloge:** +- ✅ Fähigkeiten-Katalog (Admin CRUD) +- ✅ Methoden-Katalog (Admin CRUD) + +**Übungen:** +- ✅ Übung anlegen (minimal: Titel, Ziel, Durchführung) +- ✅ Übung vollständig beschreiben (alle Felder) +- ✅ Übungsvarianten anlegen +- ✅ Fähigkeiten zuordnen (Haupt/Neben, Intensität) +- ✅ Methoden zuordnen (Haupt/Neben) +- ✅ Medien hochladen (Bilder, Videos) +- ✅ Übung suchen und filtern + - Schnellsuche (Titel, Schlagworte) + - Filter (Bereich, Zielgruppe, Fähigkeit, Dauer, Gruppengröße) +- ✅ Übung drucken +- ✅ Freigabelogik (privat / Verein / offiziell) + +**Trainingsplanung:** +- ✅ Trainingseinheit für Gruppe + Termin planen +- ✅ Training aus Vorlage ableiten +- ✅ Training aus altem Training kopieren +- ✅ Trainingsabschnitte verwalten +- ✅ Übungen zu Abschnitten zuordnen +- ✅ Kombinations-Flag für Abschnitte +- ✅ Kalenderansicht (Gruppe, Datum) +- ✅ Druckansicht +- ✅ Mobile Durchführungssicht +- ✅ Notizen zu Training + Übungen +- ✅ Reflexion nach Training + +**Import:** +- ✅ Einseitiger MediaWiki-Import + - Fähigkeiten + - Methoden + - Übungen +- ✅ Import-Tracking (Erfolg/Fehler) +- ✅ Duplikat-Erkennung (Wiki-Referenz) + +### 6.2 Bewusst NICHT in MVP + +**Kern-Features, die warten:** +- ❌ KI-Trainingsplanung +- ❌ KI-Suche mit externen Calls +- ❌ Sportler-Tracking (individuelle Entwicklung) +- ❌ Gürtel/Graduierungs-System +- ❌ Technik-Mastery-Tracking +- ❌ Reifegradmodell (vollständig) +- ❌ Turnier-/Wettkampfbegleitung +- ❌ Community/Marktplatz +- ❌ Komplexe Membership-Matrix +- ❌ Vollhistorisierung jeder Änderung +- ❌ Bidirektionale Wiki-Sync +- ❌ Offline-Sync (vollständig) + +**Rollen/Governance (vereinfacht):** +- ❌ Feingranulare Rechte-Matrix +- ❌ Workflow-basierte Freigabe +- ❌ Multi-Tenant-Administration + +--- + +## 7. Initiale Migrationen (MVP) + +### 7.1 001_auth_membership.sql + +```sql +-- Von Mitai übernehmen: +-- profiles, sessions, features, tier_limits, subscriptions +-- (siehe Mitai 001_*.sql + 002_*.sql) +``` + +### 7.2 002_organization.sql + +```sql +-- 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() +); + +-- Sparten (optional, kann später) +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), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- 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, + 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); +``` + +### 7.3 003_catalogs.sql + +```sql +-- Fähigkeiten +CREATE TABLE skills ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + category VARCHAR(100), + 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() +); + +-- Trainingsmethoden +CREATE TABLE training_methods ( + id SERIAL PRIMARY KEY, + name VARCHAR(200) NOT NULL, + abbreviation VARCHAR(20), + category VARCHAR(100), + description TEXT, + typical_duration INT, + typical_group_size VARCHAR(50), + related_skills JSONB, + 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_methods_category ON training_methods(category); +``` + +### 7.4 004_exercises.sql + +```sql +-- Übungen +CREATE TABLE exercises ( + id SERIAL PRIMARY KEY, + title VARCHAR(300) NOT NULL, + summary TEXT, + goal TEXT NOT NULL, + execution TEXT NOT NULL, + preparation TEXT, + trainer_notes TEXT, + equipment JSONB, + + duration_min INT, + duration_max INT, + group_size_min INT, + group_size_max INT, + age_groups JSONB, + + focus_area VARCHAR(100), + secondary_areas JSONB, + training_character VARCHAR(50), + + primary_method_id INT REFERENCES training_methods(id), + secondary_method_ids JSONB, + + visibility VARCHAR(50) DEFAULT 'private', + status VARCHAR(50) DEFAULT 'draft', + created_by INT REFERENCES profiles(id), + club_id INT REFERENCES clubs(id), + + import_source VARCHAR(50), + import_id VARCHAR(200), + + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Übungs-Fähigkeiten +CREATE TABLE exercise_skills ( + id SERIAL PRIMARY KEY, + exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, + skill_id INT REFERENCES skills(id), + is_primary BOOLEAN DEFAULT false, + intensity INT CHECK (intensity BETWEEN 1 AND 5), + development_contribution VARCHAR(50), + required_level INT, + target_level INT, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Varianten +CREATE TABLE exercise_variants ( + id SERIAL PRIMARY KEY, + exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, + variant_name VARCHAR(200) NOT NULL, + description TEXT, + execution_changes TEXT, + duration_min INT, + duration_max INT, + equipment_changes JSONB, + difficulty_adjustment VARCHAR(50), + age_group_override JSONB, + skill_focus_override JSONB, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Medien +CREATE TABLE exercise_media ( + id SERIAL PRIMARY KEY, + exercise_id INT REFERENCES exercises(id) ON DELETE CASCADE, + media_type VARCHAR(50), + file_path TEXT, + title VARCHAR(200), + description TEXT, + sort_order INT, + is_primary BOOLEAN DEFAULT false, + context VARCHAR(100), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_exercises_created_by ON exercises(created_by); +CREATE INDEX idx_exercises_club ON exercises(club_id); +CREATE INDEX idx_exercises_visibility ON exercises(visibility); +CREATE INDEX idx_exercise_skills_skill ON exercise_skills(skill_id); +``` + +### 7.5 005_training_planning.sql + +```sql +-- Templates / Standards +CREATE TABLE training_templates ( + id SERIAL PRIMARY KEY, + name VARCHAR(300) NOT NULL, + type VARCHAR(50), + club_id INT REFERENCES clubs(id), + division_id INT REFERENCES divisions(id), + goal TEXT, + focus_areas JSONB, + duration_total INT, + visibility VARCHAR(50) DEFAULT 'private', + status VARCHAR(50) DEFAULT 'active', + version INT DEFAULT 1, + created_by INT REFERENCES profiles(id), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Abschnitte +CREATE TABLE training_sections ( + id SERIAL PRIMARY KEY, + parent_type VARCHAR(50), + parent_id INT, + title VARCHAR(200) NOT NULL, + section_type VARCHAR(100), + sort_order INT, + duration_planned INT, + goal TEXT, + notes TEXT, + is_combination BOOLEAN DEFAULT false, + combination_method_id INT REFERENCES training_methods(id), + created_at TIMESTAMP DEFAULT NOW() +); + +-- Übungen in Abschnitten +CREATE TABLE section_exercises ( + id SERIAL PRIMARY KEY, + section_id INT REFERENCES training_sections(id) ON DELETE CASCADE, + exercise_id INT REFERENCES exercises(id), + variant_id INT REFERENCES exercise_variants(id), + sort_order INT, + duration_planned INT, + notes TEXT, + created_at TIMESTAMP DEFAULT NOW() +); + +-- Trainingseinheiten +CREATE TABLE training_units ( + id SERIAL PRIMARY KEY, + group_id INT REFERENCES training_groups(id) ON DELETE CASCADE, + date DATE NOT NULL, + time_start TIME, + time_end TIME, + derived_from_template_id INT REFERENCES training_templates(id), + derived_from_unit_id INT REFERENCES training_units(id), + title VARCHAR(300), + goal TEXT, + focus_areas JSONB, + actual_time_start TIME, + actual_time_end TIME, + completion_status VARCHAR(50), + reflection_text TEXT, + what_worked_well TEXT, + what_to_improve TEXT, + created_by INT REFERENCES profiles(id), + created_at TIMESTAMP DEFAULT NOW(), + updated_at TIMESTAMP DEFAULT NOW() +); + +-- Programme +CREATE TABLE training_programs ( + id SERIAL PRIMARY KEY, + club_id INT REFERENCES clubs(id), + division_id INT REFERENCES divisions(id), + name VARCHAR(300) NOT NULL, + description TEXT, + goal TEXT, + duration_weeks INT, + focus_areas JSONB, + visibility VARCHAR(50) DEFAULT 'private', + status VARCHAR(50) DEFAULT 'active', + created_by INT REFERENCES profiles(id), + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE TABLE program_units ( + id SERIAL PRIMARY KEY, + program_id INT REFERENCES training_programs(id) ON DELETE CASCADE, + unit_order INT, + template_id INT REFERENCES training_templates(id), + week_number INT, + notes TEXT, + created_at TIMESTAMP DEFAULT NOW() +); + +CREATE INDEX idx_units_group_date ON training_units(group_id, date); +CREATE INDEX idx_sections_parent ON training_sections(parent_type, parent_id); +``` + +### 7.6 006_governance.sql + +```sql +-- Änderungsanfragen +CREATE TABLE content_change_requests ( + id SERIAL PRIMARY KEY, + content_type VARCHAR(50), + content_id INT, + requested_by INT REFERENCES profiles(id), + change_description TEXT, + change_details JSONB, + status VARCHAR(50) DEFAULT 'pending', + reviewed_by INT REFERENCES profiles(id), + review_notes TEXT, + created_at TIMESTAMP DEFAULT NOW(), + reviewed_at TIMESTAMP +); + +CREATE INDEX idx_change_requests_status ON content_change_requests(status); +``` + +### 7.7 007_wiki_import.sql + +```sql +-- Import-Log +CREATE TABLE wiki_import_log ( + id SERIAL PRIMARY KEY, + import_type VARCHAR(50), + import_status VARCHAR(50), + items_total INT, + items_imported INT, + items_failed INT, + error_log JSONB, + imported_by INT REFERENCES profiles(id), + created_at TIMESTAMP DEFAULT NOW() +); + +-- Import-Referenzen +CREATE TABLE wiki_import_references ( + id SERIAL PRIMARY KEY, + wiki_page_title VARCHAR(300), + wiki_page_id VARCHAR(100), + content_type VARCHAR(50), + local_id INT, + last_imported TIMESTAMP, + created_at TIMESTAMP DEFAULT NOW(), + UNIQUE(wiki_page_title, content_type) +); + +CREATE INDEX idx_wiki_refs_page ON wiki_import_references(wiki_page_title); +``` + +--- + +## 8. MediaWiki-Import-Strategie + +### 8.1 Grundprinzip + +**Einseitig, einmalig, dann Shinkan-nativ weiter** + +- ✅ Import aus MediaWiki nach Shinkan +- ❌ KEINE bidirektionale Synchronisation +- ❌ KEINE Live-Kopplung +- ❌ KEINE kontinuierliche Aktualisierung + +### 8.2 Import-Reihenfolge + +1. **Fähigkeiten** (skills) - Basis-Katalog +2. **Methoden** (training_methods) - Methodenkatalog +3. **Übungen** (exercises) - Hauptinhalt + +**Programme/Trainings:** Nur wenn Datenqualität ausreicht + +### 8.3 Import-Logik + +```python +# Pseudo-Code Import-Workflow +for wiki_page in wiki_pages: + # Duplikat-Check + existing = check_wiki_reference(wiki_page.title, content_type) + if existing: + log_skip(wiki_page) + continue + + # Parse + Transform + data = parse_wiki_page(wiki_page) + + # Validate + if not validate_minimal_fields(data): + log_error(wiki_page, "missing required fields") + continue + + # Import + try: + local_id = create_content(data) + create_wiki_reference(wiki_page, local_id) + log_success(wiki_page, local_id) + except Exception as e: + log_error(wiki_page, str(e)) +``` + +### 8.4 Import-Metadaten + +Jeder importierte Inhalt erhält: +- `import_source = 'mediawiki'` +- `import_id = wiki_page_id` +- Referenz in `wiki_import_references` +- Status = `draft` (muss nachbearbeitet werden) + +### 8.5 Nach-Import-Workflow + +Nach Import müssen Trainer/Admins: +- Importierte Inhalte prüfen +- Unvollständige Felder ergänzen +- Fähigkeiten/Methoden zuordnen +- Status auf `approved` setzen + +--- + +## 9. Docker-Setup (wie Mitai) + +### 9.1 docker-compose.yml (Production) + +```yaml +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 + + 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} + APP_URL: https://shinkan.jinkendo.de + ALLOWED_ORIGINS: https://shinkan.jinkendo.de + volumes: + - shinkan-media:/app/media + ports: + - "8003:8000" + depends_on: + - postgres + restart: unless-stopped + + frontend: + build: + context: ./frontend + dockerfile: Dockerfile + args: + VITE_API_URL: https://shinkan.jinkendo.de + container_name: shinkan-ui + ports: + - "3003:80" + restart: unless-stopped + +volumes: + shinkan-db-data: + shinkan-media: +``` + +### 9.2 docker-compose.dev-env.yml (Development) + +```yaml +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 + + 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 + APP_URL: https://dev.shinkan.jinkendo.de + ALLOWED_ORIGINS: https://dev.shinkan.jinkendo.de + volumes: + - dev-shinkan-media:/app/media + ports: + - "8098:8000" + depends_on: + - postgres + restart: unless-stopped + + 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 + +volumes: + dev-shinkan-db-data: + dev-shinkan-media: +``` + +--- + +## 10. Initiale Version + +```python +# backend/version.py + +APP_VERSION = "0.1.0" +BUILD_DATE = "2026-04-21" +DB_SCHEMA_VERSION = "20260421" + +MODULE_VERSIONS = { + "auth": "1.0.0", + "profiles": "1.0.0", + "clubs": "0.1.0", + "groups": "0.1.0", + "skills": "0.1.0", + "methods": "0.1.0", + "exercises": "0.1.0", + "training_units": "0.1.0", + "training_programs": "0.1.0", + "planning": "0.1.0", + "import_wiki": "0.1.0", + "admin": "1.0.0", + "membership": "1.0.0", +} + +CHANGELOG = [ + { + "version": "0.1.0", + "date": "2026-04-21", + "changes": [ + "Initial MVP Setup", + "Feature: Übungsverwaltung (Kern-Modul)", + "Feature: Fähigkeiten- und Methodenkataloge", + "Feature: Trainingsplanung für Gruppen", + "Feature: Trainingsabschnitte mit Kombinations-Flag", + "Feature: MediaWiki-Import (einseitig)", + "Feature: Freigabelogik (privat/Verein/offiziell)", + ] + } +] +``` + +--- + +## 11. Umsetzungs-Reihenfolge (Empfehlung) + +### Phase 1: Basis (2-3 Tage) +- Repository + Docker-Setup +- Auth + Membership von Mitai kopieren +- DB-Migrationen 001-003 +- Version.py anpassen + +### Phase 2: Kataloge (2 Tage) +- Skills CRUD (Admin) +- Methods CRUD (Admin) +- Frontend Admin-Pages + +### Phase 3: Übungen (5-7 Tage) +- Exercises CRUD +- Exercise-Skills M:N +- Exercise-Variants +- Exercise-Media +- Suche + Filter +- Druckansicht + +### Phase 4: Trainingsplanung (5-7 Tage) +- Training-Groups CRUD +- Training-Templates/Standards +- Training-Sections +- Training-Units +- Kalenderansicht +- Mobile Durchführungssicht + +### Phase 5: Import (3-4 Tage) +- MediaWiki-Parser +- Import-Workflow (Skills, Methods, Exercises) +- Import-Log + Duplikat-Erkennung + +### Phase 6: Governance (2-3 Tage) +- Freigabelogik +- Change-Requests (Basis) +- Eigene vs. Vereins-Inhalte + +**Geschätzte Gesamtdauer MVP:** 4-5 Wochen + +--- + +## 12. Bewusst nur vorbereitet, nicht umgesetzt (in MVP) + +Diese Elemente sollen durch das Datenmodell **nicht verbaut** werden, gehören aber **nicht in MVP**: + +**Reifegradmodelle:** +- Skill-Levels pro Modell +- Maturity-Stages +- Target-Descriptions pro Level + +**Individuelle Sportler-Entwicklung:** +- Athlete-Profiles +- Skill-Progress-Tracking +- Assessment-History + +**Erweiterte Governance:** +- Workflow-basierte Freigabe +- Review-Zyklen +- Versionierung mit Branches + +**Community:** +- Shared Content-Pool +- Bewertungen +- Kommentare + +**KI:** +- Trainingsplanung +- Übungssuche mit Semantik +- Automatische Programm-Generierung + +--- + +## 13. Offene Punkte / Entscheidungsbedarf + +``` +🤔 MediaWiki-Parser: Welches Format nutzt das aktuelle Wiki? (Semantic MediaWiki) +🤔 Medien-Upload: Lokal oder S3/Cloud? +🤔 Suche: PostgreSQL Full-Text oder ElasticSearch später? +🤔 Offline: Service Worker + IndexedDB oder nur gute Mobile-UX? +🤔 Druckformat: PDF-Export oder nur HTML-Print? +🤔 Sparten: Schon in MVP oder erst später? +``` + +--- + +## 14. Nächste konkrete Schritte + +1. **Repository erstellen** auf Gitea: `shinkan-jinkendo` +2. **Basis-Dateien kopieren** aus Mitai: + - `backend/auth.py`, `db.py`, `db_init.py` + - `frontend/src/context/`, `utils/api.js` +3. **Migrationen schreiben** (001-007) +4. **Version.py anpassen** +5. **CLAUDE.md erstellen** für Shinkan +6. **Erste Router implementieren:** + - `skills.py` + - `methods.py` + - `exercises.py` + +--- + +**Version:** 2.0 (komplett überarbeitet) +**Stand:** 21.04.2026 +**Autor:** Claude Code (basierend auf Anforderungsdokument) diff --git a/.claude/rules/ARCHITECTURE.md b/.claude/rules/ARCHITECTURE.md new file mode 100644 index 0000000..d5e4e3b --- /dev/null +++ b/.claude/rules/ARCHITECTURE.md @@ -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. diff --git a/.claude/rules/ARCHITECTURE_old.md b/.claude/rules/ARCHITECTURE_old.md new file mode 100644 index 0000000..13a5ea5 --- /dev/null +++ b/.claude/rules/ARCHITECTURE_old.md @@ -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 +``` diff --git a/.claude/rules/CODING_RULES.md b/.claude/rules/CODING_RULES.md new file mode 100644 index 0000000..542bc53 --- /dev/null +++ b/.claude/rules/CODING_RULES.md @@ -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 +``` diff --git a/.claude/rules/DOCUMENTATION.md b/.claude/rules/DOCUMENTATION.md new file mode 100644 index 0000000..f760324 --- /dev/null +++ b/.claude/rules/DOCUMENTATION.md @@ -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 diff --git a/.claude/rules/IMPLEMENTATION_RULES.md b/.claude/rules/IMPLEMENTATION_RULES.md new file mode 100644 index 0000000..364e0c9 --- /dev/null +++ b/.claude/rules/IMPLEMENTATION_RULES.md @@ -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 +``` + +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.** diff --git a/.claude/rules/LESSONS_LEARNED.md b/.claude/rules/LESSONS_LEARNED.md new file mode 100644 index 0000000..1b01cfd --- /dev/null +++ b/.claude/rules/LESSONS_LEARNED.md @@ -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 diff --git a/.gitea/workflows/deploy-dev.yml b/.gitea/workflows/deploy-dev.yml new file mode 100644 index 0000000..3484de5 --- /dev/null +++ b/.gitea/workflows/deploy-dev.yml @@ -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 diff --git a/.gitea/workflows/deploy-prod.yml b/.gitea/workflows/deploy-prod.yml new file mode 100644 index 0000000..d733c10 --- /dev/null +++ b/.gitea/workflows/deploy-prod.yml @@ -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 diff --git a/backend/Dockerfile b/backend/Dockerfile new file mode 100644 index 0000000..41de5da --- /dev/null +++ b/backend/Dockerfile @@ -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"] diff --git a/backend/migrations/001_auth_membership.sql b/backend/migrations/001_auth_membership.sql new file mode 100644 index 0000000..4f3142c --- /dev/null +++ b/backend/migrations/001_auth_membership.sql @@ -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; diff --git a/backend/migrations/002_organization.sql b/backend/migrations/002_organization.sql new file mode 100644 index 0000000..d06463a --- /dev/null +++ b/backend/migrations/002_organization.sql @@ -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); diff --git a/backend/migrations/003_catalogs.sql b/backend/migrations/003_catalogs.sql new file mode 100644 index 0000000..4a11075 --- /dev/null +++ b/backend/migrations/003_catalogs.sql @@ -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); diff --git a/docker-compose.dev-env.yml b/docker-compose.dev-env.yml new file mode 100644 index 0000000..13812c8 --- /dev/null +++ b/docker-compose.dev-env.yml @@ -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 diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a4045bc --- /dev/null +++ b/docker-compose.yml @@ -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 diff --git a/frontend/Dockerfile b/frontend/Dockerfile new file mode 100644 index 0000000..b822ada --- /dev/null +++ b/frontend/Dockerfile @@ -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;"] diff --git a/frontend/index.html b/frontend/index.html new file mode 100644 index 0000000..c818fb6 --- /dev/null +++ b/frontend/index.html @@ -0,0 +1,14 @@ + + + + + + + + Shinkan Jinkendo + + +
+ + + diff --git a/frontend/package.json b/frontend/package.json new file mode 100644 index 0000000..3ca0172 --- /dev/null +++ b/frontend/package.json @@ -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" + } +} diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx new file mode 100644 index 0000000..605e89a --- /dev/null +++ b/frontend/src/App.jsx @@ -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 ( + + +
+
+

🥋 Shinkan Jinkendo

+

Trainer- und Vereinsplattform für Kampfsport-Trainingsplanung

+ + {version && ( +
+

System Status

+

Version: {version.app_version}

+

Build: {version.build_date}

+

Environment: {version.environment}

+

DB Schema: {version.db_schema_version}

+
+ )} + +
+

🚧 In Entwicklung

+

Die App wird gerade aufgebaut.

+
    +
  • ✅ Backend-Basis
  • +
  • ✅ Docker-Setup
  • +
  • ✅ Datenbank-Schema
  • +
  • 🔲 Auth-System
  • +
  • 🔲 Übungsverwaltung
  • +
  • 🔲 Trainingsplanung
  • +
+
+
+
+
+
+ ) +} + +export default App diff --git a/frontend/src/app.css b/frontend/src/app.css new file mode 100644 index 0000000..f8092ee --- /dev/null +++ b/frontend/src/app.css @@ -0,0 +1,1330 @@ +:root { + --bg: #f4f3ef; + --surface: #ffffff; + --surface2: #f9f8f5; + --border: rgba(0,0,0,0.09); + --border2: rgba(0,0,0,0.16); + --text1: #1c1b18; + --text2: #5a5955; + --text3: #9a9892; + --accent: #1D9E75; + --accent-light: #E1F5EE; + --accent-dark: #0a5c43; + --danger: #D85A30; + --warn: #EF9F27; + /* Höhe der eigentlichen Tab-Zeile (ohne Abstand/Home-Indicator) */ + --nav-h: 56px; + --nav-pad-top: 8px; + --header-h: 52px; + --font: system-ui, -apple-system, 'Segoe UI', sans-serif; + --capture-content-max: 800px; + /* Admin: nutzt volle Hauptspalte bis zu dieser Obergrenze (siehe .app-main:has(.admin-shell)) */ + --admin-main-max: min(1560px, calc(100vw - 220px)); +} +@media (prefers-color-scheme: dark) { + :root { + --bg: #181816; --surface: #222220; --surface2: #1e1e1c; + --border: rgba(255,255,255,0.08); --border2: rgba(255,255,255,0.14); + --text1: #eeecea; --text2: #aaa9a4; --text3: #686762; + --accent-light: #04342C; --accent-dark: #5DCAA5; + } +} +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html, body, #root { height: 100%; } +body { font-family: var(--font); background: var(--bg); color: var(--text1); -webkit-text-size-adjust: 100%; } + +.app-shell { display: flex; flex-direction: column; height: 100%; max-width: 600px; margin: 0 auto; } +.app-header { + height: var(--header-h); display: flex; align-items: center; padding: 0 16px; + background: var(--surface); border-bottom: 1px solid var(--border); + position: sticky; top: 0; z-index: 10; +} +.app-logo { font-size: 18px; font-weight: 700; color: var(--accent); letter-spacing: -0.02em; } +/* unten: Tab-Leiste + Abstand nach oben zur Leiste + Home-Indicator (iPhone) */ +.app-main { + flex: 1; + overflow-y: auto; + padding: 16px 16px calc(var(--nav-h) + var(--nav-pad-top) + env(safe-area-inset-bottom, 0px) + 20px); +} + +.bottom-nav { + position: fixed; + bottom: 0; + left: 50%; + transform: translateX(-50%); + width: 100%; + max-width: 600px; + display: flex; + align-items: center; + background: var(--surface); + border-top: 1px solid var(--border); + z-index: 20; + overflow-x: auto; + overflow-y: visible; + -webkit-overflow-scrolling: touch; + scrollbar-width: none; + -ms-overflow-style: none; + justify-content: flex-start; + gap: 2px; + padding: var(--nav-pad-top) 6px env(safe-area-inset-bottom, 0px); + min-height: calc(var(--nav-h) + var(--nav-pad-top) + env(safe-area-inset-bottom, 0px)); + height: auto; + box-sizing: border-box; +} +.bottom-nav::-webkit-scrollbar { + display: none; +} +.nav-item { + flex: 0 0 auto; + min-width: 56px; + max-width: 96px; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + gap: 3px; + color: var(--text3); + text-decoration: none; + font-weight: 500; + transition: color 0.15s; + padding: 2px 4px 4px; + box-sizing: border-box; +} +.nav-item span { + font-size: 10px; + line-height: 1.15; + text-align: center; + max-width: 100%; +} +.nav-item.active { color: var(--accent); } +.nav-item svg { flex-shrink: 0; } + +/* Cards */ +.card { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 16px; } +.card + .card { margin-top: 12px; } +.card-title { font-size: 13px; font-weight: 600; color: var(--text3); text-transform: uppercase; letter-spacing: 0.05em; margin-bottom: 10px; } + +/* Stats grid */ +.stats-grid { display: grid; grid-template-columns: 1fr 1fr; gap: 10px; } +.stat-card { background: var(--surface); border: 1px solid var(--border); border-radius: 10px; padding: 14px; } +.stat-val { font-size: 26px; font-weight: 700; color: var(--text1); line-height: 1; } +.stat-label { font-size: 12px; color: var(--text3); margin-top: 3px; } +.stat-delta { font-size: 12px; font-weight: 600; margin-top: 4px; } +.delta-pos { color: var(--accent); } +.delta-neg { color: var(--danger); } + +/* Form */ +.form-section { margin-bottom: 20px; } +.form-section-title { + font-size: 13px; font-weight: 600; color: var(--text3); + text-transform: uppercase; letter-spacing: 0.05em; + margin-bottom: 10px; padding-bottom: 6px; border-bottom: 1px solid var(--border); +} +.form-row { display: flex; align-items: center; gap: 10px; padding: 9px 0; border-bottom: 1px solid var(--border); } +.form-row:last-child { border-bottom: none; } +.form-label { flex: 1; font-size: 14px; color: var(--text1); } +.form-sub { font-size: 11px; color: var(--text3); display: block; margin-top: 1px; } +.form-input { + width: 90px; padding: 7px 10px; text-align: right; + font-family: var(--font); font-size: 15px; font-weight: 500; color: var(--text1); + background: var(--surface2); border: 1.5px solid var(--border2); + border-radius: 8px; transition: border-color 0.15s; +} +.form-input:focus { outline: none; border-color: var(--accent); } +.form-unit { font-size: 12px; color: var(--text3); width: 24px; } + +/* Einstellungen Profil: Label als Überschrift oben, volle Breite, linksbündig */ +.settings-page__field { + display: flex; + flex-direction: column; + align-items: stretch; + gap: 8px; + padding: 12px 0; + border-bottom: 1px solid var(--border); + text-align: left; +} +.settings-page__field-label { + display: block; + font-size: 14px; + font-weight: 600; + color: var(--text1); + text-align: left; + line-height: 1.3; +} +.settings-page__field .form-input { + width: 100%; + max-width: 100%; + min-width: 0; + text-align: left; + box-sizing: border-box; +} +.form-select { + font-family: var(--font); font-size: 13px; color: var(--text1); + background: var(--surface2); border: 1.5px solid var(--border2); + border-radius: 8px; padding: 7px 10px; width: 100%; +} + +/* Buttons */ +.btn { + display: inline-flex; align-items: center; gap: 6px; + font-family: var(--font); font-size: 14px; font-weight: 600; + padding: 10px 18px; border-radius: 10px; border: none; cursor: pointer; + transition: opacity 0.15s, transform 0.1s; +} +.btn:active { transform: scale(0.97); } +.btn-primary { background: var(--accent); color: white; } +.btn-secondary { background: var(--surface2); border: 1px solid var(--border2); color: var(--text2); } +.btn-danger { background: #FCEBEB; color: var(--danger); } +.btn-full { width: 100%; justify-content: center; } +.btn:disabled { opacity: 0.5; pointer-events: none; } + +/* Badge */ +.badge { display: inline-block; font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 4px; } + +/* AI content */ +.ai-content { font-size: 14px; line-height: 1.7; color: var(--text2); white-space: pre-wrap; } +.ai-content strong { color: var(--text1); font-weight: 600; } + +/* Photo grid */ +.photo-grid { display: grid; grid-template-columns: repeat(3, 1fr); gap: 6px; } +.photo-thumb { aspect-ratio: 1; border-radius: 8px; object-fit: cover; width: 100%; cursor: pointer; } + +/* Tabs */ +.tabs { display: flex; gap: 4px; background: var(--surface2); border-radius: 10px; padding: 3px; margin-bottom: 16px; } +.tab { flex: 1; text-align: center; padding: 7px; border-radius: 8px; font-size: 13px; font-weight: 500; color: var(--text3); cursor: pointer; border: none; background: transparent; font-family: var(--font); } +.tab.active { background: var(--surface); color: var(--text1); box-shadow: 0 1px 3px rgba(0,0,0,0.1); } + +/* Section */ +.section-gap { margin-bottom: 16px; } +.page-title { font-size: 20px; font-weight: 700; margin-bottom: 16px; } + +/* Verlauf: Mobile Tabs horizontale Leiste, Desktop vertikal links (P4 / RESPONSIVE_UI §5.2) */ +/* KPI-Kachel-Raster: gemeinsam für Verlauf Körper, Dashboard KPI-Board, … + Desktop: title-Tooltip; Touch: ℹ → Bottom-Sheet (siehe KpiTilesOverview.jsx) */ +.kpi-tiles-grid, +.body-kpi-overview { + display: grid; + grid-template-columns: repeat(auto-fill, minmax(158px, 1fr)); + gap: 8px; + margin-bottom: 12px; +} +.kpi-tiles-card, +.body-kpi-card { + background: var(--surface2); + border-radius: 10px; + padding: 10px 10px 10px 12px; + border: 1px solid var(--border); + cursor: help; + text-align: left; + transition: border-color 0.15s ease, box-shadow 0.15s ease; +} +@media (hover: none) { + .kpi-tiles-card, + .body-kpi-card { + cursor: default; + } +} +.kpi-tiles-card:hover, +.body-kpi-card:hover { + border-color: var(--border2); + box-shadow: 0 1px 5px rgba(0, 0, 0, 0.07); +} + +.kpi-tiles-info-btn, +.body-kpi-info-btn { + position: absolute; + top: 6px; + right: 6px; + display: inline-flex; + align-items: center; + justify-content: center; + min-width: 36px; + min-height: 36px; + margin: 0; + padding: 0; + border: none; + border-radius: 8px; + background: transparent; + color: var(--text3); + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} +.kpi-tiles-info-btn:active, +.body-kpi-info-btn:active { + background: var(--surface); + color: var(--accent); +} + +.kpi-tiles-touch-backdrop, +.body-kpi-touch-backdrop { + position: fixed; + inset: 0; + z-index: 10050; + display: flex; + align-items: flex-end; + justify-content: center; + padding: 0 12px; + padding-bottom: max(12px, env(safe-area-inset-bottom)); + background: rgba(0, 0, 0, 0.45); + animation: kpi-tiles-fade-in 0.15s ease; +} + +@keyframes kpi-tiles-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} +@keyframes body-kpi-fade-in { + from { opacity: 0; } + to { opacity: 1; } +} + +.kpi-tiles-touch-sheet, +.body-kpi-touch-sheet { + width: 100%; + max-width: 520px; + max-height: min(72vh, 560px); + overflow: auto; + margin: 0 auto; + padding: 14px 16px 18px; + border-radius: 16px 16px 0 0; + background: var(--surface); + border: 1px solid var(--border); + border-bottom: none; + box-shadow: 0 -8px 32px rgba(0, 0, 0, 0.18); +} + +.kpi-tiles-touch-sheet__head, +.body-kpi-touch-sheet__head { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 12px; + margin-bottom: 10px; +} + +.kpi-tiles-touch-sheet__title, +.body-kpi-touch-sheet__title { + margin: 0; + font-size: 16px; + font-weight: 700; + color: var(--text1); + line-height: 1.3; + flex: 1; + min-width: 0; +} + +.kpi-tiles-touch-sheet__close, +.body-kpi-touch-sheet__close { + flex-shrink: 0; + width: 40px; + height: 40px; + margin: -6px -8px 0 0; + padding: 0; + border: none; + border-radius: 10px; + background: transparent; + color: var(--text2); + font-size: 26px; + line-height: 1; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.kpi-tiles-touch-sheet__close:active, +.body-kpi-touch-sheet__close:active { + background: var(--surface2); +} + +.kpi-tiles-touch-sheet__body, +.body-kpi-touch-sheet__body { + font-size: 13px; + line-height: 1.5; + color: var(--text2); + white-space: pre-wrap; + word-break: break-word; +} + +.kpi-tiles-touch-sheet__body--muted, +.body-kpi-touch-sheet__body--muted { + color: var(--text3); + font-style: italic; +} + +/* KPI: Kurz-Hinweis max. 2 Zeilen — Details weiter per ℹ */ +.kpi-tiles-card__hint { + max-height: 2.8em; +} + +/* Verlauf Ernährung: Donut (Ø-Quote) + wöchentliche Makro-Verteilung (E3) */ +.nutrition-macro-pair { + display: grid; + gap: 12px; + margin-bottom: 12px; + align-items: stretch; +} + +@media (min-width: 780px) { + .nutrition-macro-pair { + grid-template-columns: minmax(280px, 1fr) minmax(320px, 1.25fr); + } +} + +.nutrition-macro-pair__weekly { + min-width: 0; +} + +/* Einheitliche Chart-Höhe (Donut-Bereich ≈ E3-Balken) */ +.nutrition-macro-pair__chart-wrap { + width: 100%; + min-height: 260px; +} + +.nutrition-macro-pair__donut-inner { + display: flex; + flex-direction: column; + gap: 12px; + width: 100%; +} + +.nutrition-macro-pair__donut-chart { + width: 100%; + min-height: 260px; +} + +.nutrition-macro-pair__legend { + width: 100%; + padding-top: 2px; +} + +.nutrition-macro-pair .card.nutrition-macro-pair__donut, +.nutrition-macro-pair .card.nutrition-macro-pair__weekly { + display: flex; + flex-direction: column; +} + +.history-page__title { + margin-bottom: 12px; +} + +.history-page__layout { + display: flex; + flex-direction: column; + gap: 16px; +} + +.history-tabs { + margin-bottom: 0; +} + +.history-tabs__scroller { + display: flex; + flex-direction: row; + gap: 6px; + overflow-x: auto; + padding-bottom: 6px; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.history-tabs__scroller::-webkit-scrollbar { + display: none; +} + +.history-tab-btn { + white-space: nowrap; + flex-shrink: 0; + padding: 7px 14px; + border-radius: 20px; + border: 1.5px solid var(--border2); + background: var(--surface); + color: var(--text2); + font-family: var(--font); + font-size: 13px; + font-weight: 500; + cursor: pointer; +} + +.history-tab-btn:hover { + border-color: var(--accent); + color: var(--text1); +} + +.history-tab-btn.history-tab-btn--active { + border-color: var(--accent); + background: var(--accent); + color: white; +} + +.history-tab-btn.history-tab-btn--active:hover { + color: white; +} + +@media (min-width: 1024px) { + .history-page__layout { + flex-direction: row; + align-items: flex-start; + gap: 24px; + } + + .history-tabs { + flex: 0 0 260px; + max-width: 280px; + position: sticky; + top: 16px; + align-self: flex-start; + } + + .history-tabs__scroller { + flex-direction: column; + overflow-x: visible; + overflow-y: auto; + max-height: calc(100vh - 120px); + padding-bottom: 0; + gap: 8px; + } + + .history-tab-btn { + display: flex; + align-items: center; + width: 100%; + text-align: left; + border-radius: 10px; + white-space: normal; + flex-shrink: 0; + padding: 10px 14px; + } + + .history-content { + flex: 1; + min-width: 0; + } +} + +/* KI-Analyse (P5): Mobile Prompt-Leiste oben / horizontal, Desktop links ~300px (RESPONSIVE_UI §5.3) */ +.analysis-page__header { + display: flex; + justify-content: space-between; + align-items: flex-start; + margin-bottom: 16px; + gap: 12px; + flex-wrap: wrap; +} +.analysis-page__header > div:first-child { + flex: 1; + min-width: 0; +} + +.analysis-split { + display: flex; + flex-direction: column; + gap: 16px; +} + +.analysis-split__nav { + display: flex; + flex-direction: row; + gap: 6px; + overflow-x: auto; + padding-bottom: 6px; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.analysis-split__nav::-webkit-scrollbar { + display: none; +} + +.analysis-split__nav-item { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 8px 14px; + border-radius: 20px; + border: 1.5px solid var(--border2); + background: var(--surface); + color: var(--text2); + font-family: var(--font); + font-size: 13px; + font-weight: 500; + cursor: pointer; + white-space: nowrap; +} + +.analysis-split__nav-item:hover { + border-color: var(--accent); + color: var(--text1); +} + +.analysis-split__nav-item--active { + border-color: var(--accent); + background: var(--accent); + color: white; +} + +.analysis-split__nav-item--active:hover { + color: white; +} + +.analysis-split__nav-item--active .muted { + color: rgba(255, 255, 255, 0.88) !important; +} + +.analysis-split__nav-cat-count { + margin-left: 6px; + font-size: 11px; + font-weight: 500; + opacity: 0.92; +} + +.analysis-split__nav-item--active .analysis-split__nav-cat-count { + color: rgba(255, 255, 255, 0.95); + opacity: 1; +} + +a.analysis-split__nav-item { + text-decoration: none; + box-sizing: border-box; +} + +.analysis-split__main { + min-width: 0; +} + +@media (min-width: 1024px) { + .analysis-split { + flex-direction: row; + align-items: flex-start; + gap: 24px; + } + + .analysis-split__nav-wrap { + flex: 0 0 300px; + max-width: 320px; + position: sticky; + top: 16px; + align-self: flex-start; + } + + .analysis-split__nav { + flex-direction: column; + overflow-x: visible; + overflow-y: auto; + max-height: calc(100vh - 140px); + padding-bottom: 0; + gap: 8px; + } + + .analysis-split__nav-item { + width: 100%; + justify-content: flex-start; + text-align: left; + border-radius: 10px; + white-space: normal; + } + + .analysis-split__main { + flex: 1; + } +} + +/* Erfassung: eine einheitliche Inhaltsbreite (Desktop), zentriert; mobil volle Breite */ +.capture-page { + width: 100%; + max-width: 100%; + box-sizing: border-box; +} + +@media (min-width: 1024px) { + .capture-page { + max-width: var(--capture-content-max); + margin-left: auto; + margin-right: auto; + } +} + +/* Admin: Session-Metriken / Attributprofile — volle Breite, linksbündig (nicht globale 90px-Zahlfelder) */ +.activity-attribute-profiles .aaf-stack { + max-width: 42rem; +} +.activity-attribute-profiles .aaf-field { + margin-bottom: 1rem; +} +.activity-attribute-profiles .aaf-label { + display: block; + font-size: 14px; + font-weight: 600; + color: var(--text1); + text-align: left; + margin-bottom: 6px; + line-height: 1.35; +} +.activity-attribute-profiles .aaf-sublabel { + display: block; + font-size: 12px; + font-weight: 600; + color: var(--text2); + text-align: left; + margin-bottom: 4px; +} +.activity-attribute-profiles .aaf-hint { + font-size: 12px; + color: var(--text3); + text-align: left; + margin: 6px 0 0; + line-height: 1.45; +} +.activity-attribute-profiles .aaf-input, +.activity-attribute-profiles textarea.aaf-input { + display: block; + width: 100%; + box-sizing: border-box; + padding: 10px 12px; + text-align: left; + font-family: var(--font); + font-size: 15px; + font-weight: 500; + color: var(--text1); + background: var(--surface2); + border: 1.5px solid var(--border2); + border-radius: 8px; + transition: border-color 0.15s; +} +.activity-attribute-profiles textarea.aaf-input { + resize: vertical; + min-height: 4.5rem; + font-weight: 400; +} +.activity-attribute-profiles .aaf-input:focus { + outline: none; + border-color: var(--accent); +} +.activity-attribute-profiles .aaf-split { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 12px; +} +@media (max-width: 560px) { + .activity-attribute-profiles .aaf-split { + grid-template-columns: 1fr; + } +} +.activity-attribute-profiles .aaf-field-select { + padding: 12px 0; + border-bottom: 1px solid var(--border); +} +.activity-attribute-profiles .aaf-field-select:last-child { + border-bottom: none; +} +.activity-attribute-profiles .aaf-field-select .form-label { + display: block; + text-align: left; + margin-bottom: 6px; + font-weight: 600; + flex: unset; +} +.activity-attribute-profiles .aaf-field-select .form-input, +.activity-attribute-profiles .aaf-field-select select.form-input { + width: 100%; + max-width: none; + min-width: 0; + text-align: left; + box-sizing: border-box; + padding: 10px 12px; +} +.activity-attribute-profiles .aaf-toolbar { + display: flex; + flex-wrap: wrap; + align-items: flex-end; + gap: 12px; + padding: 12px 0; + border-bottom: 1px solid var(--border); +} +.activity-attribute-profiles .aaf-toolbar .form-label { + display: block; + text-align: left; + margin-bottom: 6px; + font-weight: 600; + flex: unset; +} +.activity-attribute-profiles .aaf-toolbar__grow { + flex: 1 1 240px; + min-width: 0; +} +.activity-attribute-profiles .aaf-toolbar .form-input, +.activity-attribute-profiles .aaf-toolbar select.form-input { + width: 100%; + min-width: 140px; + max-width: none; + text-align: left; + box-sizing: border-box; + padding: 10px 12px; +} +.activity-attribute-profiles .aaf-toolbar__compact { + flex: 0 0 auto; +} +.activity-attribute-profiles .aaf-toolbar__compact .form-input, +.activity-attribute-profiles .aaf-toolbar__compact select.form-input { + width: 100%; + min-width: 5rem; +} +.activity-attribute-profiles .aaf-inline-edit .form-input, +.activity-attribute-profiles .aaf-inline-edit select.form-input { + text-align: left; + min-width: 4.5rem; + width: auto; + max-width: none; + box-sizing: border-box; + padding: 8px 10px; +} + +/* Erfassung: Sub-Navigation (Mobil = Chips, Desktop = linke Spalte) */ +.capture-shell { + width: 100%; +} + +.capture-shell__layout { + display: flex; + flex-direction: column; + gap: 16px; +} + +.capture-shell__nav { + display: flex; + flex-direction: row; + gap: 6px; + overflow-x: auto; + padding-bottom: 6px; + -ms-overflow-style: none; + scrollbar-width: none; +} + +.capture-shell__nav::-webkit-scrollbar { + display: none; +} + +.capture-shell__nav-item { + flex-shrink: 0; + display: inline-flex; + align-items: center; + gap: 6px; + padding: 7px 12px; + border-radius: 20px; + border: 1.5px solid var(--border2); + background: var(--surface); + color: var(--text2); + font-family: var(--font); + font-size: 13px; + font-weight: 500; + text-decoration: none; + white-space: nowrap; + cursor: pointer; + box-sizing: border-box; +} + +.capture-shell__nav-item:hover { + border-color: var(--accent); + color: var(--text1); +} + +.capture-shell__nav-item--active { + border-color: var(--accent); + background: var(--accent); + color: white; +} + +.capture-shell__nav-item--active:hover { + color: white; +} + +.capture-shell__nav-item--highlight:not(.capture-shell__nav-item--active) { + border-color: #7f77dd88; + background: #7f77dd14; +} + +.capture-shell__nav-icon { + font-size: 15px; + line-height: 1; +} + +.capture-shell__nav-label { + line-height: 1.2; +} + +.capture-shell__main { + min-width: 0; +} + +@media (min-width: 1024px) { + .capture-shell__layout { + flex-direction: row; + align-items: flex-start; + gap: 24px; + } + + .capture-shell__nav-wrap { + flex: 0 0 260px; + max-width: 280px; + position: sticky; + top: 16px; + align-self: flex-start; + } + + .capture-shell__nav { + flex-direction: column; + overflow-x: visible; + overflow-y: auto; + max-height: calc(100vh - 140px); + padding-bottom: 0; + gap: 8px; + } + + .capture-shell__nav-item { + width: 100%; + justify-content: flex-start; + border-radius: 10px; + white-space: normal; + padding: 9px 12px; + } + + .capture-shell__main { + flex: 1; + } +} + +/* Einstellungen: gleiche Split-Struktur wie Analyse/Admin */ +.settings-shell { + width: 100%; +} + +/* Referenzwerte: Übersichtskacheln (responsive, bis 4 Spalten Desktop) */ +.ref-value-tiles-grid { + display: grid; + gap: 12px; + grid-template-columns: 1fr; +} + +@media (min-width: 520px) { + .ref-value-tiles-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + +@media (min-width: 900px) { + .ref-value-tiles-grid { + grid-template-columns: repeat(3, minmax(0, 1fr)); + } +} + +@media (min-width: 1200px) { + .ref-value-tiles-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + } +} + +.ref-value-tile { + display: block; + width: 100%; + margin: 0; + padding: 14px 14px 12px; + text-align: left; + font-family: var(--font); + border-radius: 12px; + border: 1.5px solid var(--border2); + background: var(--surface); + color: var(--text1); + cursor: pointer; + box-sizing: border-box; + transition: border-color 0.15s, box-shadow 0.15s; +} + +.ref-value-tile:hover { + border-color: var(--accent); +} + +.ref-value-tile:focus-visible { + outline: 2px solid var(--accent); + outline-offset: 2px; +} + +.ref-value-tile--active { + border-color: var(--accent); + background: var(--surface2); +} + +/* Admin: Split-Layout wie .analysis-split (nur Gruppen in der Nav) */ +.admin-shell { + width: 100%; +} + +.admin-page { + width: 100%; + min-width: 0; +} + +/* Desktop: volle Breite der Admin-Spalte (nicht wie Erfassung 800px); Lesegröße leicht skaliert */ +@media (min-width: 1024px) { + .admin-page { + max-width: 100%; + margin-left: 0; + margin-right: 0; + font-size: clamp(15px, 0.88rem + 0.25vw, 18px); + } +} + +.muted { color: var(--text3); font-size: 13px; } +.empty-state { text-align: center; padding: 48px 16px; color: var(--text3); } +.empty-state h3 { font-size: 16px; color: var(--text2); margin-bottom: 6px; } +.spinner { width: 20px; height: 20px; border: 2px solid var(--border); border-top-color: var(--accent); border-radius: 50%; animation: spin 0.7s linear infinite; display: inline-block; } +@keyframes spin { to { transform: rotate(360deg); } } +@keyframes slideDown { + from { transform: translate(-50%, -20px); opacity: 0; } + to { transform: translate(-50%, 0); opacity: 1; } +} + +/* Additional vars */ +:root { + --warn-bg: #FAEEDA; + --warn-text: #7a4b08; +} +@media (prefers-color-scheme: dark) { + :root { + --warn-bg: #3a2103; + --warn-text: #FAC775; + } +} + +/* Header with profile avatar */ +.app-header { display:flex; align-items:center; justify-content:space-between; } +.app-header a { display:flex; } + +/* ── Responsive shell: Desktop sidebar (≥1024px) — spec RESPONSIVE_UI.md ───── */ +.app-shell__column { + display: flex; + flex-direction: column; + flex: 1; + min-width: 0; + min-height: 0; +} + +.desktop-sidebar { + display: none; + flex-direction: column; + width: 220px; + height: 100vh; + position: fixed; + left: 0; + top: 0; + z-index: 30; + background: var(--surface); + border-right: 1px solid var(--border); + padding: 16px 0 16px; +} + +.desktop-sidebar__brand { + display: flex; + align-items: center; + gap: 10px; + padding: 0 16px 20px; + border-bottom: 1px solid var(--border); + margin-bottom: 12px; +} + +.desktop-sidebar__logo { + width: 40px; + height: 40px; + border-radius: 50%; + border: 3px solid var(--accent); + flex-shrink: 0; + opacity: 0.85; +} + +.desktop-sidebar__title { + font-size: 15px; + font-weight: 700; + color: var(--accent); + letter-spacing: -0.02em; + line-height: 1.2; +} + +.desktop-sidebar__nav { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1; + overflow-y: auto; + padding: 0 0 12px; +} + +.desktop-sidebar__link { + display: flex; + align-items: center; + gap: 10px; + padding: 10px 16px 10px 13px; + text-decoration: none; + color: var(--text2); + font-size: 14px; + font-weight: 500; + border-left: 3px solid transparent; + border-radius: 0 8px 8px 0; + transition: background 0.15s, color 0.15s; +} + +.desktop-sidebar__link:hover { + background: var(--surface2); + color: var(--text1); +} + +.desktop-sidebar__link.desktop-sidebar__link--active { + background: var(--accent-light); + color: var(--accent); + border-left-color: var(--accent); +} + +.desktop-sidebar__footer { + border-top: 1px solid var(--border); + padding: 16px 12px 0; + display: flex; + align-items: center; + gap: 8px; +} + +.desktop-sidebar__user { + display: flex; + align-items: center; + gap: 10px; + flex: 1; + min-width: 0; + text-decoration: none; + color: inherit; +} + +.desktop-sidebar__user-text { + display: flex; + flex-direction: column; + min-width: 0; +} + +.desktop-sidebar__user-name { + font-size: 13px; + font-weight: 600; + color: var(--text1); + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; +} + +.desktop-sidebar__user-tier { + font-size: 11px; + color: var(--text3); + text-transform: lowercase; +} + +.desktop-sidebar__logout { + flex-shrink: 0; + background: none; + border: none; + padding: 8px; + cursor: pointer; + color: var(--text3); + border-radius: 8px; + display: flex; + align-items: center; + justify-content: center; +} + +.desktop-sidebar__logout:hover { + color: var(--danger); + background: rgba(216, 90, 48, 0.08); +} + +@media (max-width: 1023px) { + .app-shell { + display: flex; + flex-direction: column; + height: 100%; + max-width: 600px; + margin: 0 auto; + } +} + +@media (min-width: 1024px) { + .app-shell { + display: block; + max-width: none; + margin: 0; + width: 100%; + min-height: 100%; + } + + .desktop-sidebar { + display: flex; + } + + .app-shell__column { + margin-left: 220px; + min-height: 100vh; + } + + .app-header--mobile { + display: none !important; + } + + .bottom-nav { + display: none !important; + } + + .app-main { + padding: 24px 32px 32px; + padding-bottom: max(32px, env(safe-area-inset-bottom, 0px)); + max-width: 1200px; + margin-left: auto; + margin-right: auto; + width: 100%; + box-sizing: border-box; + } + + /* Admin: mehr horizontaler Raum für Tabellen auf großen Screens (:has ~2022+, sonst bleibt 1200px) */ + .app-main:has(.admin-shell) { + max-width: var(--admin-main-max); + } + + /* Dashboard (P3): Begrüßung + Kennzahlen-Zeile */ + .dashboard-greeting { + display: flex; + align-items: baseline; + justify-content: space-between; + gap: 16px; + flex-wrap: wrap; + margin-bottom: 16px; + } + + .dashboard-greeting__meta { + margin-top: 0 !important; + text-align: right; + } +} + +/* ── Dashboard layout (Mobile baseline + Desktop im Block oben teilweise) ─ */ + +.dashboard-page { + width: 100%; +} + +.dashboard-greeting { + margin-bottom: 16px; +} + +/* + * Dashboard-Raster (KPI, Nebeneinander-Kacheln): 2 / 4 Spalten. + * StatCard, DashboardTile: span via --tile-sm / --tile-lg (JS clamp). + */ +.dashboard-stat-grid, +.dashboard-tile-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 8px; +} + +.dashboard-stat-grid--mobile-4col, +.dashboard-tile-grid--mobile-4col { + grid-template-columns: repeat(4, minmax(0, 1fr)); +} + +.dashboard-stat-card { + background: var(--surface); + border-radius: 12px; + padding: 12px 10px; + border: 1px solid var(--border); + transition: border-color 0.15s; +} + +.dashboard-stat-card, +.dashboard-tile { + min-width: 0; + box-sizing: border-box; + grid-column: span var(--tile-sm, 1); +} + +@media (min-width: 1024px) { + .dashboard-stat-card, + .dashboard-tile { + grid-column: span var(--tile-lg, 1); + } +} + +/* ── Dashboard-Abschnitte (Überschrift + Trennlinie) ─ */ +.dashboard-section { + margin-bottom: 22px; +} + +.dashboard-section:last-child { + margin-bottom: 0; +} + +.dashboard-section__header { + display: flex; + align-items: flex-end; + justify-content: space-between; + gap: 12px; + flex-wrap: wrap; + padding-bottom: 10px; + margin-bottom: 12px; + border-bottom: 1px solid var(--border); +} + +.dashboard-section__headline { + flex: 1; + min-width: 0; +} + +.dashboard-section__title { + font-size: 13px; + font-weight: 700; + color: var(--text3); + text-transform: uppercase; + letter-spacing: 0.05em; + margin: 0; +} + +.dashboard-section__description { + font-size: 12px; + color: var(--text3); + margin: 4px 0 0 0; + line-height: 1.35; + max-width: 640px; +} + +.dashboard-section__body { + display: flex; + flex-direction: column; + gap: 12px; +} + +.dashboard-section__actions { + flex-shrink: 0; +} + +.dashboard-pill-row { + display: flex; + gap: 6px; + flex-wrap: wrap; +} + +/* Ernährung/Aktivität: Raster wie KPI; Kacheln per DashboardTile steuerbar */ +.dashboard-summary-row.dashboard-tile-grid { + margin-bottom: 0; +} + +.dashboard-erholung-grid .dashboard-tile > .card, +.dashboard-summary-row .dashboard-tile > .card { + height: 100%; +} + +@media (min-width: 1024px) { + .dashboard-stat-grid, + .dashboard-tile-grid { + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + } +} diff --git a/frontend/src/context/AuthContext.jsx b/frontend/src/context/AuthContext.jsx new file mode 100644 index 0000000..1bcd76a --- /dev/null +++ b/frontend/src/context/AuthContext.jsx @@ -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 ( + + {children} + + ) +} + +export function useAuth() { + return useContext(AuthContext) +} + +export function getToken() { + return localStorage.getItem(TOKEN_KEY) +} diff --git a/frontend/src/context/ProfileContext.jsx b/frontend/src/context/ProfileContext.jsx new file mode 100644 index 0000000..6a34811 --- /dev/null +++ b/frontend/src/context/ProfileContext.jsx @@ -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 ( + + {children} + + ) +} + +export function useProfile() { + return useContext(ProfileContext) +} diff --git a/frontend/src/main.jsx b/frontend/src/main.jsx new file mode 100644 index 0000000..8d63283 --- /dev/null +++ b/frontend/src/main.jsx @@ -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( + + + , +) diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js new file mode 100644 index 0000000..8c8b894 --- /dev/null +++ b/frontend/src/utils/api.js @@ -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 diff --git a/frontend/vite.config.js b/frontend/vite.config.js new file mode 100644 index 0000000..4b68d47 --- /dev/null +++ b/frontend/vite.config.js @@ -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 + } +})