From 73bea5ee861da6cacc35bf2c4573252e742d4f77 Mon Sep 17 00:00:00 2001 From: Lars Date: Fri, 20 Mar 2026 18:57:39 +0100 Subject: [PATCH] feat: v9c Phase 1 - Feature consolidation & cleanup migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit PHASE 1: Cleanup & Analyse - Feature-Konsolidierung: export_csv/json/zip → data_export (1 Feature) - Umbenennung: csv_import → data_import - Auto-Migration bei Container-Start (apply_v9c_migration.py) - Diagnose-Script (check_features.sql) Lessons Learned angewendet: - Ein Feature für Export, nicht drei - Migration ist idempotent (kann mehrfach laufen) - Zeigt BEFORE/AFTER State im Log Finaler Feature-Katalog (10 statt 13): - Data: weight, circumference, caliper, nutrition, activity, photos - AI: ai_calls, ai_pipeline - Export/Import: data_export, data_import Tier Limits: - FREE: 30 data entries, 0 AI/export/import - BASIC: unlimited data, 3 AI/month, 5 export/month, 3 import/month - PREMIUM/SELFHOSTED: unlimited Migration läuft automatisch auf dev UND prod beim Container-Start. Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 1415 +++---------------- backend/apply_v9c_migration.py | 116 ++ backend/migrations/check_features.sql | 50 + backend/migrations/v9c_cleanup_features.sql | 141 ++ 4 files changed, 477 insertions(+), 1245 deletions(-) create mode 100644 backend/migrations/check_features.sql create mode 100644 backend/migrations/v9c_cleanup_features.sql diff --git a/CLAUDE.md b/CLAUDE.md index fcb7d18..2750d2a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -1,1295 +1,220 @@ # Mitai Jinkendo – Entwickler-Kontext für Claude Code ## Projekt-Übersicht -**Mitai Jinkendo** (身体 Jinkendo) ist eine selbst-gehostete PWA für Körper-Tracking (Gewicht, Körperfett, Umfänge, Ernährung, Aktivität) mit KI-Auswertung. Teil der **Jinkendo**-App-Familie (人拳道 – Der menschliche Weg der Kampfkunst). - -**Produktfamilie:** mitai · miken · ikigai · shinkan · kenkou (alle unter jinkendo.de) +**Mitai Jinkendo** (身体 Jinkendo) – selbst-gehostete PWA für Körper-Tracking mit KI-Auswertung. +Teil der **Jinkendo**-App-Familie (人拳道). Domains: jinkendo.de / .com / .life ## Tech-Stack -| Komponente | Technologie | Version | -|-----------|-------------|---------| -| Frontend | React 18 + Vite + PWA | Node 20 | -| Backend | FastAPI (Python) | Python 3.12 | -| Datenbank | PostgreSQL 16 (Alpine) | v9b | -| Container | Docker + Docker Compose | - | -| Webserver | nginx (Reverse Proxy) | Alpine | -| Auth | Token-basiert + bcrypt | - | -| KI | OpenRouter API (claude-sonnet-4) | - | +| Komponente | Technologie | +|-----------|-------------| +| 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 (claude-sonnet-4) | -## Ports -| Service | Prod | Dev | -|---------|------|-----| -| Frontend | 3002 | 3099 | -| Backend | 8002 | 8099 | +**Ports:** Prod 3002/8002 · Dev 3099/8099 – nie ändern! ## Verzeichnisstruktur ``` -mitai-jinkendo/ -├── backend/ -│ ├── main.py # FastAPI App Setup + Router Registration (~75 Zeilen) -│ ├── db.py # PostgreSQL Connection Pool + Helpers -│ ├── auth.py # Auth Functions (hash, verify, sessions) -│ ├── models.py # Pydantic Models (11 Models) -│ ├── routers/ # Modular Endpoint Structure (14 Router) -│ │ ├── auth.py # Login, Logout, Password Reset (7 Endpoints) -│ │ ├── profiles.py # Profile CRUD + Current User (7 Endpoints) -│ │ ├── weight.py # Weight Tracking (5 Endpoints) -│ │ ├── circumference.py # Body Measurements (4 Endpoints) -│ │ ├── caliper.py # Skinfold Tracking (4 Endpoints) -│ │ ├── activity.py # Workout Logging + CSV Import (6 Endpoints) -│ │ ├── nutrition.py # Nutrition + FDDB Import (4 Endpoints) -│ │ ├── photos.py # Progress Photos (3 Endpoints) -│ │ ├── insights.py # AI Analysis + Pipeline (8 Endpoints) -│ │ ├── prompts.py # AI Prompt Management (2 Endpoints) -│ │ ├── admin.py # User Management (7 Endpoints) -│ │ ├── stats.py # Dashboard Stats (1 Endpoint) -│ │ ├── exportdata.py # CSV/JSON/ZIP Export (3 Endpoints) -│ │ └── importdata.py # ZIP Import (1 Endpoint) -│ ├── requirements.txt -│ └── Dockerfile -├── frontend/ -│ ├── src/ -│ │ ├── App.jsx # Root, Auth-Gates, Navigation -│ │ ├── app.css # Globale Styles, CSS-Variablen -│ │ ├── context/ -│ │ │ ├── AuthContext.jsx # Session, Login, Logout -│ │ │ └── ProfileContext.jsx # Aktives Profil -│ │ ├── pages/ # Alle Screens -│ │ └── utils/ -│ │ ├── api.js # Alle API-Calls (injiziert Token automatisch) -│ │ ├── calc.js # Körperfett-Formeln -│ │ ├── interpret.js # Regelbasierte Auswertung -│ │ ├── Markdown.jsx # Eigener MD-Renderer -│ │ └── guideData.js # Messanleitungen -│ └── public/ # Icons (Jinkendo Ensō-Logo) -├── .gitea/workflows/ -│ ├── deploy-prod.yml # Auto-Deploy bei Push auf main -│ ├── deploy-dev.yml # Auto-Deploy bei Push auf develop -│ └── test.yml # Build-Test bei jedem Push -├── docker-compose.yml # Produktion (Ports 3002/8002) -├── docker-compose.dev-env.yml # Development (Ports 3099/8099) -└── CLAUDE.md # Diese Datei +backend/ +├── main.py # App-Setup + Router-Registration (~75 Zeilen) +├── db.py # PostgreSQL Connection Pool +├── auth.py # Hash, Verify, Sessions +├── models.py # Pydantic Models +└── routers/ # 14 Router-Module + auth · profiles · weight · circumference · caliper + activity · nutrition · photos · insights · prompts + admin · stats · exportdata · importdata + +frontend/src/ +├── App.jsx # Root, Auth-Gates, Navigation +├── app.css # CSS-Variablen + globale Styles +├── context/ # AuthContext · ProfileContext +├── pages/ # Alle Screens +└── utils/ + api.js # ALLE API-Calls – Token automatisch injiziert + calc.js · interpret.js · Markdown.jsx · guideData.js + +.claude/ +├── settings.json +├── commands/ # /deploy /merge-to-prod /refactor /ui-responsive etc. +└── docs/ + ├── BACKLOG.md + ├── functional/ # Fachliche Specs (TRAINING_TYPES, AI_PROMPTS, RESPONSIVE_UI) + └── technical/ # MEMBERSHIP_SYSTEM.md ``` -## Aktuelle Version: v9c-dev (März 2026) - -### Was implementiert ist: -- ✅ Multi-User mit E-Mail + Passwort Login (bcrypt) -- ✅ Auth-Middleware auf ALLE Endpoints (60+ Endpoints geschützt) -- ✅ Rate Limiting (Login: 5/min, Reset: 3/min) -- ✅ CORS konfigurierbar via ALLOWED_ORIGINS in .env -- ✅ Admin/User Rollen, KI-Limits (simple daily limits), Export-Berechtigungen -- ✅ Gewicht, Umfänge, Caliper (4 Formeln), Ernährung, Aktivität -- ✅ FDDB CSV-Import (Ernährung), Apple Health CSV-Import (Aktivität) -- ✅ KI-Analyse: 6 Einzel-Prompts + 3-stufige Pipeline (parallel) -- ✅ **KI-Analyse Historisierung**: Alle Analysen werden gespeichert (nicht überschrieben) -- ✅ **Pipeline korrekt**: Speichert unter scope='pipeline', erscheint nur 1x in UI -- ✅ Konfigurierbare Prompts mit Template-Variablen (Admin kann bearbeiten) -- ✅ Verlauf mit 5 Tabs + Zeitraumfilter + KI pro Sektion -- ✅ Dashboard mit Kennzahlen, Zielfortschritt, Combo-Chart -- ✅ Assistent-Modus (Schritt-für-Schritt Messung) -- ✅ PWA (iPhone Home Screen), Jinkendo Ensō-Logo -- ✅ E-Mail (SMTP) für Password-Recovery -- ✅ Admin-Panel: User verwalten, KI-Limits, E-Mail-Test, PIN/Email setzen -- ✅ Multi-Environment: Prod (mitai.jinkendo.de) + Dev (dev.mitai.jinkendo.de) -- ✅ Gitea CI/CD mit Auto-Deploy auf Raspberry Pi 5 -- ✅ PostgreSQL 16 Migration (vollständig von SQLite migriert) -- ✅ Export: CSV, JSON, ZIP (mit Fotos) -- ✅ Automatische SQLite→PostgreSQL Migration bei Container-Start -- ✅ **Modulare Backend-Architektur**: 14 Router-Module, main.py von 1878→75 Zeilen (-96%) - -### Aktuelles KI-Limit-System (Simple): -```sql --- Tägliche Limits pro Profil -profiles.ai_enabled BOOLEAN -- KI an/aus -profiles.ai_limit_day INTEGER -- Tägliches Limit (NULL = unbegrenzt) -ai_usage (profile_id, date, count) -- Täglicher Counter - --- Funktionen in routers/insights.py: -check_ai_limit(pid) -- Prüft Limit vor KI-Call -inc_ai_usage(pid) -- Inkrementiert Counter nach Call -``` - -### Feature-Roadmap & Dokumentation - -> **Für Claude Code – Dokumentations-Struktur:** -> -> ``` -> .claude/docs/ -> ├── BACKLOG.md ← Vollständiges Feature-Backlog (Übersicht) -> ├── functional/ ← Fachliche Anforderungen (was soll es können?) -> │ ├── SLEEP_MODULE.md ← v9d Schlaf -> │ ├── TRAINING_TYPES.md ← v9d Trainingstypen + HF -> │ ├── GOALS_VITALS.md ← v9e Ziele + Vitalwerte -> │ ├── AI_PROMPTS.md ← v9f KI-Prompt Flexibilisierung -> │ └── MEDITATION.md ← v9g Meditation + Selbstwahrnehmung -> └── technical/ ← Technische Implementierung (wie wird es gebaut?) -> └── MEMBERSHIP_SYSTEM.md ← v9c kombiniert (fachlich + technisch, bereits vorhanden) -> ``` -> -> **Workflow:** -> 1. Fachliche Beschreibung (`functional/`) prüfen und freigeben -> 2. Technischen Plan aus Fachlichem ableiten (`technical/`) -> 3. Implementierung starten - -### Aktuelle Version: v9c – Membership & Subscription -📚 **Dokumentation:** `.claude/docs/technical/MEMBERSHIP_SYSTEM.md` -*(kombinierte fachliche + technische Beschreibung)* - -**Offen:** -- 🔲 Feature-Enforcement-System (Redesign nach Rollback 20.03.2026) -- 🔲 Selbst-Registrierung mit E-Mail-Verifizierung -- 🔲 Trial-System UI (Countdown-Banner) - -### Nächste Versionen -| Version | Feature | Fachlich | Technisch | -|---------|---------|----------|-----------| -| v9d | Schlaf-Modul + Trainingstypen + HF | `functional/SLEEP_MODULE.md` | wird erstellt | -| v9d | Trainingstypen + HF + Ruhepuls | `functional/TRAINING_TYPES.md` ✅ | wird erstellt | -| v9d | Ruhepuls + HF-Zonen + VO2Max | `functional/TRAINING_TYPES.md` | wird erstellt | -| v9e | Primärziele + Vitalwerte | `functional/GOALS_VITALS.md` | wird erstellt | -| v9f | KI-Prompt Flexibilisierung | `functional/AI_PROMPTS.md` ✅ | wird erstellt | -| v9g | Meditation + Selbstwahrnehmung | `functional/MEDITATION.md` | wird erstellt | -| v9h | Fitness-Connectoren + Stripe | wird erstellt | wird erstellt | - -### Responsive UI (parallel) -📚 **Command:** `/ui-responsive` | **Dok:** `.claude/docs/functional/RESPONSIVE_UI.md` - - -## Was in v9c kommt: Subscription & Coupon Management System -📚 **Detail-Dokumentation:** `.claude/docs/MEMBERSHIP_SYSTEM.md` – beim Implementieren zuerst lesen! -**Phase 1 (DB-Schema): ✅ DONE** -**Phase 2 (Backend API): ✅ DONE** -**Phase 3 (Admin Frontend): ✅ DONE** -**Phase 4 (Feature Enforcement): ⚠️ DEAKTIVIERT** (Rollback am 20.03.2026 - Bugs) - -**Core Features (Backend & Admin-UI komplett):** -- ✅ DB-Schema (11 neue Tabellen, Feature-Registry Pattern) -- ✅ Flexibles Tier-System (free/basic/premium/selfhosted) - Admin-editierbar via API -- ✅ **Coupon-System** (3 Typen: single_use, period, wellpass) -- ✅ Coupon-Stacking-Logik (Pause + Resume bei Wellpass-Override) -- ✅ Access-Grant-System (zeitlich begrenzte Zugriffe mit Quelle-Tracking) -- ✅ User-Activity-Log (JSONB details) -- ✅ User-Stats (Aggregierte Statistiken) -- ✅ Individuelle User-Restrictions (Admin kann Limits pro User setzen) -- ✅ 7 neue Router, 30+ neue Endpoints (subscription, coupons, features, tiers, tier-limits, user-restrictions, access-grants) - -**Admin-Frontend (vollständig implementiert):** -- ✅ **AdminFeaturesPage** - Feature-Konfiguration (sortierung, reset_period, limits, visibility) -- ✅ **AdminTiersPage** - Tier-Verwaltung (CRUD, pricing monthly/yearly) -- ✅ **AdminTierLimitsPage** - Matrix-Editor (Tier x Feature, responsive mobile/desktop views) -- ✅ **AdminCouponsPage** - Coupon-Manager (CRUD, 3 Typen, auto-generate codes, redemption history) -- ✅ **AdminUserRestrictionsPage** - User-Override-System (effektive Werte, auto-remove redundant overrides) -- ✅ **SubscriptionPage** - User Subscription-Info + Coupon-Einlösung (tier badge, limits, usage progress bars) -- ✅ Alle Routes in App.jsx registriert - -**⚠️ Feature Enforcement - ROLLBACK (20.03.2026):** -- Initial implementation broke core functionality (analysis history, export visibility, counters) -- Complete rollback to working state (commit 4fcde4a) -- Simple AI limit system (ai_enabled, ai_limit_day) now active -- v9c backend/admin UI remains functional but NOT enforcing limits -- Needs complete reimplementation with proper testing before re-enabling - -**Noch NICHT implementiert:** -- 🔲 Feature-Enforcement-System (needs redesign) -- 🔲 Selbst-Registrierung mit E-Mail-Verifizierung -- 🔲 Trial-System UI (Countdown-Banner) -- 🔲 App-Settings Admin-Panel (globale Konfiguration) - -**📚 Vollständige v9c Dokumentation:** -Siehe `/docs/MEMBERSHIP_SYSTEM.md` für: -- Vollständige Architektur-Dokumentation -- Datenbank-Schema Details -- API-Endpoints Übersicht -- Design-Entscheidungen und Rationale -- Lessons Learned vom Feature-Enforcement-Rollback - -**E-Mail Templates (v9c):** -- 🔲 Registrierung + E-Mail-Verifizierung -- 🔲 Einladungslink -- 🔲 Passwort-Reset (bereits vorhanden) - -**Spätere Features (v9d/v9e):** -- 🔲 Bonus-System (Login-Streaks → Punkte → Geschenk-Coupons) -- 🔲 Trial-Reminder-E-Mails (3 Tage vor Ablauf) -- 🔲 Monatliches Nutzungs-Summary per E-Mail -- 🔲 Self-Service Upgrade (Stripe-Integration) -- 🔲 Partner-Verwaltung (Wellpass, Hansefit, etc.) -- 🔲 Admin-Benachrichtigungen (neue Registrierungen, etc.) - -### Was in v9d kommt: Schlaf + Sport-Vertiefung -📚 **Detail-Dokumentation:** `.claude/docs/SLEEP_MODULE.md` – wird erstellt wenn v9d startet -- 🔲 **Schlaf-Modul** – Einschlafzeit, Aufwachzeit, Qualität (1-5), Schlafdauer-Trend -- 🔲 Schlaf-Import aus Apple Health CSV -- 🔲 Trainingstypen-Kategorisierung (Cardio, Kraft, Schnellkraft, Mobility, HIIT, Erholung) -- 🔲 Ruhetage als bewusste Einheit erfassen -- 🔲 Herzfrequenz-Ruhepuls erfassen + Trend -- 🔲 HF-Zonen definieren (5 Zonen) -- 🔲 VO2Max-Schätzung - -### Was in v9e kommt: Ziele & Vitalwerte -📚 **Detail-Dokumentation:** `.claude/docs/GOALS_VITALS.md` – wird erstellt wenn v9e startet -- 🔲 Primärziele (Gewichtsabnahme, Muskelaufbau, Kondition, Gesundheit, Wettkampf) -- 🔲 Ziel-spezifische Dashboard-Ansicht -- 🔲 Vitalwerte: Blutdruck, SpO2, HRV -- 🔲 Hydration-Tracking -- 🔲 Sportartprofile (Laufen, Radfahren, Kampfsport, Yoga, etc.) - -### Was in v9f kommt: KI-Prompt Flexibilisierung -📚 **Detail-Dokumentation:** `.claude/docs/AI_PROMPTS.md` – wird erstellt wenn v9f startet -- 🔲 Prompt-Bibliothek mit Kategorien (Körper, Ernährung, Training, Schlaf, Mental) -- 🔲 Platzhalter-Browser (kategorisiert, mit Beschreibung + Beispielwert) -- 🔲 Prompt-Vorschau mit echten Daten -- 🔲 Pipeline konfigurierbar (welche Module, Gewichtung, Zeitraum je Modul) -- 🔲 Mehrere Pipeline-Konfigurationen speichern - -### Was in v9g kommt: Meditation & Selbstwahrnehmung -📚 **Detail-Dokumentation:** `.claude/docs/MEDITATION.md` – wird erstellt wenn v9g startet -- 🔲 Täglicher Check-in (Energie, Stimmung, Stress 1-5) -- 🔲 Meditationssessions erfassen (Dauer, Art) -- 🔲 Streak-Tracking -- 🔲 Selbstwahrnehmungs-Journal (Freitext) -- 🔲 Korrelations-Analysen (Schlaf ↔ Ruhepuls ↔ Leistung ↔ Stimmung) -- 🔲 → Basis für `miken.jinkendo.de` (eigene App) - -### Was in v9h kommt: Fitness-Connectoren & Gamification -- 🔲 OAuth2-Grundgerüst -- 🔲 Strava Connector -- 🔲 Withings Connector (Waage) -- 🔲 Garmin Connector -- 🔲 Bonus-System (Streaks → Punkte → Coupons) -- 🔲 Stripe-Integration - -### Responsive UI (parallel zu Features) -- 🔲 Desktop: Sidebar Navigation + Content volle Breite -- 🔲 Tablet: 2-spaltige Cards -- 🔲 Mobile: wie jetzt (Bottom Navigation) -- 🔲 Siehe `/commands/ui-responsive.md` - -### Detaillierte Feature-Dokumentation -Liegt in `.claude/docs/` – nur laden wenn aktiv implementiert: -- `MEMBERSHIP_SYSTEM.md` – v9c Subscription-System (aktuell aktiv) -- `SLEEP_MODULE.md` – v9d Schlaf-Modul (wenn v9d startet) -- `AI_PROMPTS.md` – v9f KI-Prompt Flexibilisierung - ---- - -## v9c Architektur-Details: Subscription & Coupon System - -### Datenbank-Schema (Neue Tabellen) - -#### **app_settings** - Globale Konfiguration -```sql -key, value, value_type, description, updated_at, updated_by - -Beispiele: -- trial_days: 14 -- trial_behavior: 'downgrade' | 'lock' -- allow_registration: true/false -- default_tier_trial: 'premium' -- gift_coupons_per_month: 3 -``` - -#### **tiers** - Tier-Konfiguration (vereinfacht) -```sql -id, slug, name, description, sort_order, active - -Initial Tiers: -- free, basic, premium, selfhosted - -Limits sind jetzt in tier_limits Tabelle (siehe unten)! -``` - -#### **features** - Feature-Registry (alle limitierbaren Features) -```sql -id, slug, name, category, description, unit -default_limit (NULL = unbegrenzt) -reset_period ('monthly' | 'daily' | 'never') -visible_in_admin, sort_order, active - -Initial Features: -- weight_entries: Gewichtseinträge, default: 30, never -- circumference_entries: Umfangsmessungen, default: 30, never -- caliper_entries: Caliper-Messungen, default: 30, never -- nutrition_entries: Ernährungseinträge, default: 30, never -- activity_entries: Aktivitäten, default: 30, never -- photos: Progress-Fotos, default: 5, never -- ai_calls: KI-Analysen, default: 0, monthly -- ai_pipeline: KI-Pipeline, default: 0, monthly -- csv_import: CSV-Importe, default: 0, monthly -- data_export: Daten-Exporte, default: 0, monthly -- fitness_connectors: Fitness-Connectoren, default: 0, never - -Neue Features einfach per INSERT hinzufügen - kein Schema-Change! -``` - -#### **tier_limits** - Limits pro Tier + Feature -```sql -id, tier_slug, feature_slug, limit_value, enabled - -Beispiel Free Tier: -- ('free', 'weight_entries', 30, true) -- ('free', 'ai_calls', 0, false) -- KI deaktiviert -- ('free', 'data_export', 0, false) - -Beispiel Premium: -- ('premium', 'weight_entries', NULL, true) -- unbegrenzt -- ('premium', 'ai_calls', NULL, true) -- unbegrenzt - -Admin kann in UI Matrix bearbeiten: Tier x Feature -``` - -#### **user_feature_restrictions** - Individuelle User-Limits -```sql -id, profile_id, feature_slug, limit_value, enabled -reason, set_by (admin_id) - -Überschreibt Tier-Limits für spezifische User. -Admin kann jeden User individuell einschränken oder erweitern. -``` - -#### **user_feature_usage** - Nutzungs-Tracking -```sql -id, profile_id, feature_slug, period_start, usage_count, last_used - -Für Features mit reset_period (z.B. ai_calls monthly). -Wird automatisch zurückgesetzt am Monatsanfang. -``` - -#### **coupons** - Coupon-Verwaltung -```sql -code, type ('single_use' | 'multi_use_period' | 'gift') -valid_from, valid_until, grants_tier, duration_days -max_redemptions, current_redemptions -created_by, created_for, notes, active - -Beispiel Single-Use: - Code: FRIEND-GIFT-XYZ, 30 Tage Premium, max 1x - -Beispiel Multi-Use Period: - Code: WELLPASS-2026-03, gültig 01.03-31.03, unbegrenzte Einlösungen -``` - -#### **coupon_redemptions** - Einlösungs-Historie -```sql -coupon_id, profile_id, redeemed_at, access_grant_id -UNIQUE(coupon_id, profile_id) - User kann denselben Coupon nur 1x einlösen -``` - -#### **access_grants** - Zeitlich begrenzte Zugriffe -```sql -profile_id, granted_tier, valid_from, valid_until -source ('coupon' | 'admin_grant' | 'trial') -active (false wenn pausiert durch Wellpass-Override) -paused_at, paused_by (access_grant_id das pausiert hat) - -Stacking-Logik: -- Multi-Use Period Coupon (Wellpass): pausiert andere grants -- Single-Use Coupon: stackt zeitlich (Resume nach Ablauf) -``` - -#### **user_activity_log** - Aktivitäts-Tracking -```sql -profile_id, activity_type, details (JSONB), ip_address, user_agent, created - -Activity Types: -- login, password_change, email_change, coupon_redeemed -- tier_change, export, ai_analysis, registration -``` - -#### **user_stats** - Aggregierte Statistiken -```sql -profile_id, first_login, last_login, total_logins -current_streak_days, longest_streak_days, last_streak_date -total_weight_entries, total_ai_analyses, total_exports -bonus_points (später), gift_coupons_available (später) -``` - -#### **profiles** - Erweiterte Spalten -```sql -tier, tier_locked (Admin kann Tier festnageln) -trial_ends_at, email_verified, email_verify_token -invited_by, contract_type, contract_valid_until -stripe_customer_id (vorbereitet für v9d) -``` - ---- - -### Backend-Erweiterungen - -#### Neue Router (v9c): -``` -routers/tiers.py - Tier-Verwaltung (List, Edit, Create) -routers/features.py - Feature-Registry (List, Add, Edit, Delete) ⭐ NEU -routers/tier_limits.py - Tier-Limits-Matrix (Admin bearbeitet Tier x Feature) ⭐ NEU -routers/coupons.py - Coupon-System (Redeem, Admin CRUD) -routers/access_grants.py - Zugriffs-Verwaltung (Current, Grant, Revoke) -routers/user_admin.py - Erweiterte User-Verwaltung (Activity, Stats, Feature-Restrictions) -routers/settings.py - App-Einstellungen (Admin) -routers/registration.py - Registrierung + E-Mail-Verifizierung -``` - -#### Neue Middleware: -```python -check_feature_access(profile_id, feature_slug, action='use') - """ - Zentrale Feature-Access-Prüfung. - Hierarchie: - 1. User-Restriction (höchste Priorität) - 2. Tier-Limit - 3. Feature-Default - - Returns: {'allowed': bool, 'limit': int, 'used': int, 'remaining': int, 'reason': str} - """ - -increment_feature_usage(profile_id, feature_slug) - """ - Inkrementiert Nutzungszähler. - Berücksichtigt reset_period (monthly, daily, never). - """ - -log_activity(profile_id, activity_type, details=None) - """ - Loggt User-Aktivitäten in user_activity_log. - """ -``` - -#### Hintergrund-Tasks (Cron): -```python -check_expired_access() # Täglich 00:00 - Trial/Coupon-Ablauf prüfen -reset_monthly_limits() # 1. jeden Monats - AI-Calls zurücksetzen -update_user_streaks() # Täglich 23:59 - Login-Streaks aktualisieren -``` - ---- - -### Zugriffs-Hierarchie - -``` -Effektiver Tier wird ermittelt durch (Priorität absteigend): -1. Admin-Override (tier_locked=true) → nutzt profiles.tier -2. Aktiver access_grant (nicht pausiert, valid_until > now) -3. Trial (trial_ends_at > now) -4. Base tier (profiles.tier) - -Wellpass-Override-Logik: -- User hat Single-Use Coupon (20 Tage verbleibend) -- User löst Wellpass-Coupon ein (gültig bis 31.03) -- Single-Use access_grant wird pausiert (active=false, paused_by=wellpass_grant_id) -- Nach Wellpass-Ablauf: Single-Use wird reaktiviert (noch 20 Tage) -``` - ---- - -### Tier-Limits & Feature-Gates - -**Daten-Sichtbarkeit bei Downgrade:** -- Frontend: Buttons/Features ausblenden (Export, KI, Import) -- Backend: API limitiert Rückgabe (z.B. nur letzte 30 Gewichtseinträge bei free) -- Daten bleiben erhalten, werden nur versteckt -- Bei Upgrade wieder sichtbar - -**Feature-Checks:** -```python -# Beispiel: Gewicht-Eintrag erstellen -@check_feature_limit('weight', 'create') -def create_weight_entry(): - # Prüft: Hat User max_weight_entries erreicht? - # Falls ja: HTTPException 403 "Limit erreicht - Upgrade erforderlich" -``` - ---- - -### Frontend-Erweiterungen - -#### Neue Seiten: -``` -RegisterPage.jsx - Registrierung (Name, E-Mail, Passwort) -VerifyEmailPage.jsx - E-Mail-Verifizierung (Token aus URL) -RedeemCouponPage.jsx - Coupon-Eingabe (oder Modal) -AdminCouponsPage.jsx - Coupon-Verwaltung (Admin) -AdminTiersPage.jsx - Tier-Verwaltung (CRUD) (Admin) -AdminFeaturesPage.jsx - Feature-Registry (List, Add, Edit) ⭐ NEU -AdminTierLimitsPage.jsx - Tier x Feature Matrix (bearbeiten) ⭐ NEU -AdminUserRestrictionsPage.jsx - User-spezifische Limits (bearbeiten) ⭐ NEU -AdminSettingsPage.jsx - App-Einstellungen (Admin) -``` - -#### Neue Komponenten: -```jsx - // Tier-Anzeige mit Icon -... // Feature-basierte Sichtbarkeit ⭐ GEÄNDERT - // "Trial endet in 5 Tagen" Banner - // Coupon-Eingabefeld - // User-Activity-Log - // "5/10 verwendet" Anzeige ⭐ NEU - // Matrix-Editor ⭐ NEU - // Login-Streak (später) -``` - -#### Erweiterte Admin-Seiten: -``` -AdminUsersPage.jsx erweitert um: -- Activity-Log Button → zeigt user_activity_log -- Stats Button → zeigt user_stats -- Access-Grants Button → zeigt aktive/abgelaufene Zugriffe -- Feature-Restrictions Button → individuelle Feature-Limits setzen ⭐ GEÄNDERT -- Grant Access Button → manuell Tier-Zugriff gewähren -- Usage-Overview → zeigt user_feature_usage für alle Features ⭐ NEU -``` - -#### Admin-Interface-Details: - -**AdminFeaturesPage.jsx** - Feature-Registry verwalten -```jsx -// Alle Features auflisten + neue hinzufügen - - {features.map(f => ( - - {f.name} - {f.category} - {f.unit} - {f.reset_period} - {f.default_limit ?? '∞'} - - - - - - ))} - - -``` - -**AdminTierLimitsPage.jsx** - Matrix-Editor -```jsx -// Matrix-View: Tiers (Spalten) x Features (Zeilen) - - - - Feature - Free - Basic - Premium - Selfhosted - - - - - Gewichtseinträge - - - - - - - KI-Analysen/Monat - 0 - - ∞ - ∞ - - - -``` - -**AdminUserRestrictionsPage.jsx** - Individuelle User-Limits -```jsx - - - - - {features.map(f => ( - - {f.name} - {getTierLimit(user.tier, f.slug)} - {getUsage(user.id, f.slug)} - - - - - - ))} - -``` - ---- - -### E-Mail Templates (v9c) - -**1. Registrierung + E-Mail-Verifizierung:** -``` -Betreff: Willkommen bei Mitai Jinkendo - E-Mail bestätigen - -Hallo {name}, - -vielen Dank für deine Registrierung bei Mitai Jinkendo! - -Bitte bestätige deine E-Mail-Adresse: -{app_url}/verify-email?token={token} - -Nach der Bestätigung startet dein 14-Tage Premium Trial automatisch. - -Viel Erfolg bei deinem Training! -Dein Mitai Jinkendo Team -``` - -**2. Einladungslink (Admin):** -``` -Betreff: Du wurdest zu Mitai Jinkendo eingeladen - -Hallo, - -{admin_name} hat dich zu Mitai Jinkendo eingeladen! - -Registriere dich jetzt: -{app_url}/register?invite={token} - -Du erhältst {tier} Zugriff. - -Dein Mitai Jinkendo Team -``` - ---- - -### Migrations-Reihenfolge (v9c) - -``` -Phase 1 - DB Schema: -1. app_settings Tabelle + Initialdaten -2. tiers Tabelle + 4 Standard-Tiers -3. coupons Tabelle -4. coupon_redemptions Tabelle -5. access_grants Tabelle -6. user_activity_log Tabelle -7. user_stats Tabelle -8. user_restrictions Tabelle -9. profiles Spalten erweitern -10. Bestehende Profile migrieren (Lars → tier='selfhosted', email_verified=true) - -Phase 2 - Backend: -11. Tier-System Router + Middleware -12. Registrierungs-Flow -13. Coupon-System -14. Access-Grant-Logik -15. Activity-Logging -16. Erweiterte Admin-Endpoints - -Phase 3 - Frontend: -17. Registrierungs-Seiten -18. Tier-System UI-Komponenten -19. Coupon-Eingabe -20. Erweiterte Admin-Panels -21. Feature-Gates in bestehende Seiten einbauen - -Phase 4 - Cron-Jobs: -22. Expired-Access-Checker -23. Monthly-Reset -24. Streak-Updater - -Phase 5 - Testing & Deployment: -25. Dev-Testing -26. Prod-Deployment -``` - ---- +## Aktuelle Version: v9c-dev + +### Implementiert ✅ +- Login (E-Mail + bcrypt), Auth-Middleware alle Endpoints, Rate Limiting +- Gewicht · Umfänge · Caliper · Ernährung · Aktivität + CSV-Imports +- KI-Analyse: 6 Prompts + 3-stufige Pipeline +- Dashboard · Verlauf · Assistent-Modus · Fotos +- Admin-Panel · E-Mail (SMTP) · PWA +- PostgreSQL 16 · Modulare Router-Architektur +- Membership-System: Tiers · Coupons · Access-Grants · Admin-UI +- Export: CSV · JSON · ZIP + +### Offen v9c 🔲 +- Feature-Enforcement (Rollback 20.03.2026 – Redesign nötig) +- Selbst-Registrierung + E-Mail-Verifizierung +- Trial-System UI + +📚 Details: `.claude/docs/technical/MEMBERSHIP_SYSTEM.md` + +## Feature-Roadmap + +> Vollständiges Backlog: `.claude/docs/BACKLOG.md` +> Beim Implementieren: verlinkte Dok-Datei zuerst lesen! + +| Version | Feature | Dokumentation | +|---------|---------|---------------| +| v9c | Membership (aktiv) | `technical/MEMBERSHIP_SYSTEM.md` ✅ | +| v9d | Schlaf-Modul | `functional/SLEEP_MODULE.md` (ausstehend) | +| v9d | Trainingstypen + HF | `functional/TRAINING_TYPES.md` ✅ | +| v9e | Ziele + Vitalwerte | `functional/GOALS_VITALS.md` (ausstehend) | +| v9f | KI-Prompt Flexibilisierung | `functional/AI_PROMPTS.md` ✅ | +| v9g | Meditation + Selbstwahrnehmung | `functional/MEDITATION.md` (ausstehend) | +| v9h | Connectoren + Stripe | ausstehend | +| — | Responsive UI | `functional/RESPONSIVE_UI.md` ✅ | ## Deployment -### Infrastruktur ``` -Internet → privat.stommer.com (Fritz!Box DynDNS) - → Synology NAS (Reverse Proxy + Let's Encrypt) - → Raspberry Pi 5 (192.168.2.49, Docker) +Internet → Fritz!Box (privat.stommer.com) → Synology NAS → Raspberry Pi 5 (192.168.2.49) + +Git Workflow: + develop → Auto-Deploy → dev.mitai.jinkendo.de (bodytrack-dev/, Port 3099/8099) + main → Auto-Deploy → mitai.jinkendo.de (bodytrack/, Port 3002/8002) + +Gitea: http://192.168.2.144:3000/Lars/mitai-jinkendo +Runner: Raspberry Pi (/home/lars/gitea-runner/) + +Manuell: + cd /home/lars/docker/bodytrack[-dev] + docker compose -f docker-compose[.dev-env].yml build --no-cache && up -d ``` -### Git Workflow +## Datenbank-Schema (PostgreSQL 16) ``` -develop branch → Auto-Deploy → dev.mitai.jinkendo.de (Port 3099/8099) -main branch → Auto-Deploy → mitai.jinkendo.de (Port 3002/8002) +profiles – Nutzer (role, pin_hash/bcrypt, email, tier) +sessions – Auth-Tokens +weight_log – Gewicht (profile_id, date, weight) +circumference_log – 8 Umfangspunkte +caliper_log – Hautfalten, 4 Methoden +nutrition_log – Kalorien + Makros +activity_log – Training +photos – Progress-Fotos +ai_insights – KI-Auswertungen (scope = prompt-slug) +ai_prompts – Konfigurierbare Prompts (11 Standard) +ai_usage – KI-Calls pro Tag pro Profil + +v9c neu (Membership): +subscriptions · coupons · coupon_redemptions · features +tier_limits · user_restrictions · access_grants · user_activity_log + +Schema-Datei: backend/schema.sql ``` -### Deployment-Befehle (manuell falls nötig) -```bash -# Prod -cd /home/lars/docker/bodytrack -docker compose -f docker-compose.yml build --no-cache -docker compose -f docker-compose.yml up -d - -# Dev -cd /home/lars/docker/bodytrack-dev -docker compose -f docker-compose.dev-env.yml build --no-cache -docker compose -f docker-compose.dev-env.yml up -d +## API & Auth ``` +Alle Endpoints: /api/... +Auth-Header: X-Auth-Token: +Fehler: {"detail": "Fehlermeldung"} +Rate Limit: HTTP 429 -## Datenbank-Schema (PostgreSQL 16, v9b) -### Wichtige Tabellen: -- `profiles` – Nutzer (role, pin_hash/bcrypt, email, auth_type, ai_enabled, tier) -- `sessions` – Auth-Tokens mit Ablaufdatum -- `weight_log` – Gewichtseinträge (profile_id, date, weight) -- `circumference_log` – 8 Umfangspunkte -- `caliper_log` – Hautfaltenmessung, 4 Methoden -- `nutrition_log` – Kalorien + Makros (aus FDDB-CSV) -- `activity_log` – Training (aus Apple Health oder manuell) -- `photos` – Progress Photos -- `ai_insights` – KI-Auswertungen (scope = prompt-slug) -- `ai_prompts` – Konfigurierbare Prompts mit Templates (11 Prompts) -- `ai_usage` – KI-Calls pro Tag pro Profil - -**Schema-Datei:** `backend/schema.sql` (vollständiges PostgreSQL-Schema) -**Migration-Script:** `backend/migrate_to_postgres.py` (SQLite→PostgreSQL, automatisch) - -## Auth-Flow (v9b) +Auth-Flow: + Login → E-Mail + Passwort → Token in localStorage + Token → X-Auth-Token Header → require_auth() + Profile-Id → immer aus Session, nie aus Header! + SHA256 → automatisch zu bcrypt migriert beim Login ``` -Login-Screen → E-Mail + Passwort → Token im localStorage -Token → X-Auth-Token Header → Backend require_auth() -Profile-Id → aus Session (nicht aus Header!) -SHA256 Passwörter → automatisch zu bcrypt migriert beim Login -``` - -## API-Konventionen -- Alle Endpoints: `/api/...` -- Auth-Header: `X-Auth-Token: ` -- Responses: immer JSON -- Fehler: `{"detail": "Fehlermeldung"}` -- Rate Limit überschritten: HTTP 429 ## Umgebungsvariablen (.env) ``` -# Database (PostgreSQL) -DB_HOST=postgres -DB_PORT=5432 -DB_NAME=mitai_prod -DB_USER=mitai_prod -DB_PASSWORD= # REQUIRED - -# AI -OPENROUTER_API_KEY= # KI-Calls (optional, alternativ ANTHROPIC_API_KEY) +DB_HOST/PORT/NAME/USER/PASSWORD # PostgreSQL +OPENROUTER_API_KEY # KI OPENROUTER_MODEL=anthropic/claude-sonnet-4 -ANTHROPIC_API_KEY= # Direkte Anthropic API (optional) - -# Email -SMTP_HOST= # E-Mail (für Recovery) -SMTP_PORT=587 -SMTP_USER= -SMTP_PASS= -SMTP_FROM= - -# App +SMTP_HOST/PORT/USER/PASS/FROM # E-Mail APP_URL=https://mitai.jinkendo.de ALLOWED_ORIGINS=https://mitai.jinkendo.de -DATA_DIR=/app/data PHOTOS_DIR=/app/photos -ENVIRONMENT=production ``` -## Wichtige Hinweise für Claude Code -1. **Ports immer 3002/8002 (Prod) oder 3099/8099 (Dev)** – nie ändern -2. **npm install** (nicht npm ci) – kein package-lock.json vorhanden -3. **PostgreSQL-Migrations** – Schema-Änderungen in `backend/schema.sql`, dann Container neu bauen -4. **Pipeline-Prompts** haben slug-Prefix `pipeline_` – nie als Einzelanalyse zeigen -5. **dayjs.week()** braucht Plugin – stattdessen native JS ISO-Wochenberechnung -6. **useNavigate()** nur in React-Komponenten, nicht in Helper-Functions -7. **api.js nutzen** für alle API-Calls – injiziert Token automatisch -8. **bcrypt** für alle neuen Passwort-Operationen verwenden -9. **session=Depends(require_auth)** als separater Parameter – nie in Header() einbetten -10. **RealDictCursor verwenden** – `get_cursor(conn)` statt `conn.cursor()` für dict-like row access +## Kritische Regeln für Claude Code -## Code-Style -- React: Functional Components, Hooks -- CSS: Inline-Styles + globale CSS-Variablen (var(--accent), var(--text1), etc.) -- API-Calls: immer über `api.js` (injiziert Token automatisch) -- Kein TypeScript (bewusst, für Einfachheit) -- Python: keine Type-Hints Pflicht, aber bei neuen Funktionen erwünscht +### Must-Do: +1. `api.js` für ALLE API-Calls nutzen – nie direktes `fetch()` ohne Token +2. `session: dict = Depends(require_auth)` als **separater** Parameter – nie in `Header()` einbetten +3. `bcrypt` für alle Passwort-Operationen +4. Neue DB-Spalten nur via Schema-Migration, nicht direkt +5. `npm install` (nicht npm ci) – kein package-lock.json -## Design-System +### Bekannte Fallstricke: +```python +# ❌ FALSCH – führt zu ungeschütztem Endpoint: +def endpoint(x: str = Header(default=None, session=Depends(require_auth))): -### Farben (CSS-Variablen) -```css ---accent: #1D9E75 /* Jinkendo Grün – Buttons, Links, Akzente */ ---accent-dark: #085041 /* Dunkelgrün – Icon-Hintergrund, Header */ ---accent-light: #E1F5EE /* Hellgrün – Hintergründe, Badges */ ---bg: /* Seitenhintergrund (hell/dunkel auto) */ ---surface: /* Card-Hintergrund */ ---surface2: /* Sekundäre Fläche */ ---border: /* Rahmen */ ---text1: /* Primärer Text */ ---text2: /* Sekundärer Text */ ---text3: /* Muted Text, Labels */ ---danger: #D85A30 /* Fehler, Warnungen */ +# ✅ RICHTIG: +def endpoint(x: str = Header(default=None), session: dict = Depends(require_auth)): ``` -### CSS-Klassen -```css -.card /* Weißer Container, border-radius 12px, box-shadow */ -.btn /* Basis-Button */ -.btn-primary /* Grüner Button (#1D9E75) */ -.btn-secondary /* Grauer Button */ -.btn-full /* 100% Breite */ -.form-input /* Eingabefeld, volle Breite */ -.form-label /* Feldbezeichnung, klein, uppercase */ -.form-row /* Label + Input + Unit nebeneinander */ -.form-unit /* Einheit rechts (kg, cm, etc.) */ -.section-gap /* margin-bottom zwischen Sektionen */ -.spinner /* Ladekreis, animiert */ -``` - -### Abstände & Größen -``` -Seiten-Padding: 16px seitlich -Card-Padding: 16-20px -Border-Radius: 12px (Cards), 8px (Buttons/Inputs), 50% (Avatare) -Icon-Größe: 16-20px inline, 24px standalone -Font-Größe: 12px (Labels), 14px (Body), 16-18px (Subtitel), 20-24px (Titel) -Font-Weight: 400 (normal), 600 (semi-bold), 700 (bold) -Bottom-Padding: 80px (für Mobile-Navigation) -``` - -### Komponenten-Muster - -**Titelzeile einer Seite:** -```jsx -
-
- Seitentitel -
- -
-``` - -**Ladezustand:** -```jsx -if (loading) return ( -
-
-
-) -``` - -**Fehlerzustand:** -```jsx -if (error) return ( -
- {error} -
-) -``` - -**Leerer Zustand:** -```jsx -{items.length === 0 && ( -
-
📭
-
Noch keine Einträge
-
-)} -``` - -**Metric Card:** -```jsx -
-
LABEL
-
- {value} -
-
Einheit
-
-``` - -### Jinkendo Logo-System -``` -Grundelement: Ensō-Kreis (offen, Lücke 4-5 Uhr) -Farbe Ensō: #1D9E75 -Hintergrund: #085041 (dunkelgrün) -Kern-Symbol: #5DCAA5 (mintgrün) -Wortmarke: Jin(light) + ken(bold #1D9E75) + do(light) -``` - -### Verfügbare Custom Commands -``` -/deploy → Commit + Push vorbereiten -/merge-to-prod → develop → main mergen -/test → Manuelle Tests durchführen -/new-feature → Neues Feature-Template -/ui-component → Neue Komponente erstellen -/ui-page → Neue Seite erstellen -/fix-bug → Bug analysieren und beheben -/add-endpoint → Neuen API-Endpoint hinzufügen -/db-add-column → Neue DB-Spalte hinzufügen -``` - -## Jinkendo App-Familie & Markenarchitektur - -### Philosophie -**Jinkendo** (人拳道) = Jin (人 Mensch) + Ken (拳 Faust) + Do (道 Weg) -"Der menschliche Weg der Kampfkunst" – ruhig aber kraftvoll, Selbstwahrnehmung, Meditation, Zielorientiert - -### App-Familie (Subdomain-Architektur) -``` -mitai.jinkendo.de → Körper-Tracker (身体 = eigener Körper) ← DIESE APP -miken.jinkendo.de → Meditation (眉間 = drittes Auge) -ikigai.jinkendo.de → Lebenssinn/Ziele (生き甲斐) -shinkan.jinkendo.de → Kampfsport (真観 = wahre Wahrnehmung) -kenkou.jinkendo.de → Gesundheit allgemein (健康) – für später aufsparen -``` - -### Registrierte Domains -- jinkendo.de, jinkendo.com, jinkendo.life – alle registriert bei Strato - -## v9b Detailplan – Freemium Tier-System - -### Tier-Modell -``` -free → Selbst-Registrierung, 14-Tage Trial, eingeschränkt -basic → Kernfunktionen (Abo Stufe 1) -premium → Alles inkl. KI und Connectoren (Abo Stufe 2) -selfhosted → Lars' Heimversion, keine Einschränkungen -``` - -### Geplante DB-Erweiterungen (profiles Tabelle) -```sql -tier TEXT DEFAULT 'free' -trial_ends_at TEXT -- ISO datetime -sub_valid_until TEXT -- ISO datetime -email_verified INTEGER DEFAULT 0 -email_verify_token TEXT -invited_by TEXT -- profile_id FK -invitation_token TEXT -``` - -### Tier-Limits (geplant) -| Feature | free | basic | premium | selfhosted | -|---------|------|-------|---------|------------| -| Gewicht-Einträge | 30 | unbegrenzt | unbegrenzt | unbegrenzt | -| KI-Analysen/Monat | 0 | 3 | unbegrenzt | unbegrenzt | -| Ernährung Import | ❌ | ✅ | ✅ | ✅ | -| Export | ❌ | ✅ | ✅ | ✅ | -| Fitness-Connectoren | ❌ | ❌ | ✅ | ✅ | - -### Registrierungs-Flow (geplant) -``` -1. Selbst-Registrierung: Name + E-Mail + Passwort -2. Auto-Trial: tier='free', trial_ends_at=now+14d -3. E-Mail-Bestätigung → email_verified=1 -4. Trial läuft ab → Upgrade-Prompt -5. Einladungslinks: Admin generiert Token → direkt basic-Tier -6. Stripe Integration: später (v9b ohne Stripe, nur Tier-Logik) -``` - -## Infrastruktur Details - -### Heimnetzwerk -``` -Internet - → Fritz!Box 7530 AX (DynDNS: privat.stommer.com) - → Synology NAS (192.168.2.63, Reverse Proxy + Let's Encrypt) - → Raspberry Pi 5 (192.168.2.49, Docker) - → MiniPC (192.168.2.144, Gitea auf Port 3000) -``` - -### Synology Reverse Proxy Regeln -``` -mitai.jinkendo.de → HTTP 192.168.2.49:3002 (Prod Frontend) -dev.mitai.jinkendo.de → HTTP 192.168.2.49:3099 (Dev Frontend) -``` - -### AdGuard DNS Rewrites (für internes Routing) -``` -mitai.jinkendo.de → 192.168.2.63 -dev.mitai.jinkendo.de → 192.168.2.63 -``` - -### Fritz!Box DNS-Rebind Ausnahmen -``` -jinkendo.de -mitai.jinkendo.de -``` - -### Pi Verzeichnisstruktur -``` -/home/lars/docker/ -├── bodytrack/ → Prod (main branch, docker-compose.yml) -└── bodytrack-dev/ → Dev (develop branch, docker-compose.dev-env.yml) -``` - -### Gitea Runner -``` -Runner: raspberry-pi (auf Pi installiert) -Service: /etc/systemd/system/gitea-runner.service -Binary: /home/lars/gitea-runner/act_runner -``` - -### Container Namen -``` -Prod: mitai-api, mitai-ui -Dev: dev-mitai-api, dev-mitai-ui -``` - -## Bekannte Probleme & Lösungen - -### Admin User-Erstellung – Email fehlt (v9c TODO) -**Problem:** Bei Admin-CRUD `/api/profiles` (POST) wird keine Email-Adresse abgefragt. -**Impact:** Neue User können sich nicht einloggen (Login erfordert Email). -**Workaround:** Email manuell via `/api/admin/profiles/{pid}/email` setzen. -**Fix-TODO:** POST `/api/profiles` sollte Email als optionales Feld akzeptieren. - -### AdminUserRestrictionsPage – Effektive Werte anzeigen (v9c GELÖST) -**Problem:** Ursprüngliches Design zeigte leere Felder wenn kein Override existierte. -**Issues:** -- User konnte nicht sehen welcher Wert aktuell gilt (Override vs Tier-Standard) -- "unlimited" konnte nicht eingegeben/gespeichert werden (nur Platzhalter) -- Redundante Overrides (Wert = Tier-Standard) wurden nicht verhindert -- Tier-Limits verwendeten falschen Fallback (default_limit statt null) - -**Lösung (März 2026):** ```javascript -// getDisplayValue() zeigt effektiven Wert (Override ODER Tier-Limit) -const restriction = restrictions.find(r => r.feature_id === featureId) -if (restriction) return formatValue(restriction.limit_value) -return formatValue(tierLimits[featureId]) // Tier-Standard als Fallback +// ❌ FALSCH – dayjs.week() existiert nicht ohne Plugin: +dayjs(date).week() -// formatValue() konvertiert NULL zu "unlimited" (statt leer) -if (val === null) return 'unlimited' - -// handleChange() entfernt Override wenn Wert = Tier-Standard -if (parsedValue === tierLimit) { - newChanges[featureId] = { action: 'remove' } // Redundanter Override -} - -// Tier-Limits-Fallback wie TierLimitsPage -limits[feature.id] = limitsMatrix.limits[key] ?? null // nicht default_limit! +// ✅ RICHTIG – native ISO-Wochenberechnung: +const w = (d => Math.ceil(((new Date(d.setDate(d.getDate()+4-(d.getDay()||7)))- + new Date(d.getFullYear(),0,1))/86400000+1)/7))(new Date(date)) ``` -**Ergebnis:** -- ✅ User sieht immer den aktuellen effektiven Wert -- ✅ "unlimited" kann getippt und gespeichert werden (grün gefärbt) -- ✅ Redundante Overrides werden automatisch entfernt -- ✅ Selfhosted-Tier zeigt korrekt "unlimited" statt "0" - -### dayjs.week() – NIEMALS verwenden -```javascript -// ❌ Falsch: -const week = dayjs(date).week() - -// ✅ Richtig (ISO 8601): -const weekNum = (() => { - const dt = new Date(date) - dt.setHours(0,0,0,0) - dt.setDate(dt.getDate()+4-(dt.getDay()||7)) - const y = new Date(dt.getFullYear(),0,1) - return Math.ceil(((dt-y)/86400000+1)/7) -})() -``` - -### session=Depends(require_auth) – Korrekte Platzierung ```python -# ❌ Falsch (führt zu NameError oder ungeschütztem Endpoint): -def endpoint(x_profile_id: Optional[str] = Header(default=None, session=Depends(require_auth))): - -# ✅ Richtig (separater Parameter): -def endpoint(x_profile_id: Optional[str] = Header(default=None), - session: dict = Depends(require_auth)): +# PostgreSQL Boolean (nicht SQLite 0/1): +WHERE active = true # ✅ +WHERE active = 1 # ❌ ``` -### Recharts Bar fill=function – nicht unterstützt -```jsx -// ❌ Falsch: - entry.color}/> +## Design-System (Kurzreferenz) +```css +/* Farben */ +--accent: #1D9E75 --accent-dark: #085041 --danger: #D85A30 +--bg · --surface · --surface2 · --border · --text1 · --text2 · --text3 -// ✅ Richtig: - +/* Klassen */ +.card · .btn · .btn-primary · .btn-secondary · .btn-full +.form-input · .form-label · .form-row · .spinner + +/* Abstände */ +Seiten-Padding: 16px · Card-Padding: 16-20px · Border-Radius: 12px/8px +Bottom-Padding Mobile: 80px (Navigation) ``` -### PostgreSQL Boolean-Syntax -```python -# ❌ Falsch (SQLite-Syntax): -cur.execute("SELECT * FROM ai_prompts WHERE active=1") +> Vollständige CSS-Variablen und Komponenten-Muster: `frontend/src/app.css` +> Responsive Layout-Spec: `.claude/docs/functional/RESPONSIVE_UI.md` -# ✅ Richtig (PostgreSQL): -cur.execute("SELECT * FROM ai_prompts WHERE active=true") +## Dokumentations-Referenzen + +> **Für Claude Code:** Beim Arbeiten an einem Thema die entsprechende Datei lesen: + +| Thema | Datei | +|-------|-------| +| Backend-Architektur, Router, DB-Zugriff | `.claude/docs/architecture/BACKEND.md` | +| Frontend-Architektur, api.js, Komponenten | `.claude/docs/architecture/FRONTEND.md` | +| Coding Rules (Pflichtregeln) | `.claude/docs/rules/CODING_RULES.md` | +| Lessons Learned (Fehler vermeiden) | `.claude/docs/rules/LESSONS_LEARNED.md` | +| Feature Backlog (Übersicht) | `.claude/docs/BACKLOG.md` | +| Membership-System (v9c, technisch) | `.claude/docs/technical/MEMBERSHIP_SYSTEM.md` | +| Trainingstypen + HF (v9d, fachlich) | `.claude/docs/functional/TRAINING_TYPES.md` | +| KI-Prompt Flexibilisierung (v9f, fachlich) | `.claude/docs/functional/AI_PROMPTS.md` | +| Responsive UI (fachlich) | `.claude/docs/functional/RESPONSIVE_UI.md` | + +## Jinkendo App-Familie ``` - -### RealDictCursor für dict-like row access -```python -# ❌ Falsch: -cur = conn.cursor() -cur.execute("SELECT COUNT(*) FROM weight_log") -count = cur.fetchone()[0] # Tuple index - -# ✅ Richtig: -cur = get_cursor(conn) # Returns RealDictCursor -cur.execute("SELECT COUNT(*) as count FROM weight_log") -count = cur.fetchone()['count'] # Dict key -``` - -## v9b Migration – Lessons Learned - -### PostgreSQL Migration (SQLite → PostgreSQL) -**Problem:** Docker Build hing 30+ Minuten bei `apt-get install postgresql-client` -**Lösung:** Alle apt-get dependencies entfernt, reine Python-Lösung mit psycopg2-binary - -**Problem:** Leere date-Strings (`''`) führten zu PostgreSQL-Fehlern -**Lösung:** Migration-Script konvertiert leere Strings zu NULL für DATE-Spalten - -**Problem:** Boolean-Felder (SQLite INTEGER 0/1 vs PostgreSQL BOOLEAN) -**Lösung:** Migration konvertiert automatisch, Backend nutzt `active=true` statt `active=1` - -### API Endpoint Consistency (März 2026) -**Problem:** 11 kritische Endpoint-Mismatches zwischen Frontend und Backend gefunden -**Gelöst:** -- AI-Endpoints konsistent: `/api/insights/run/{slug}`, `/api/insights/pipeline` -- Password-Reset: `/api/auth/forgot-password`, `/api/auth/reset-password` -- Admin-Endpoints: `/permissions`, `/email`, `/pin` Sub-Routes -- Export: JSON + ZIP Endpoints hinzugefügt -- Prompt-Bearbeitung: PUT-Endpoint für Admins - -**Tool:** Vollständiger Audit via Explore-Agent empfohlen bei größeren Änderungen - -## Export/Import Spezifikation (v9c) - -### ZIP-Export Struktur -``` -mitai-export-{name}-{YYYY-MM-DD}.zip -├── README.txt ← Erklärung des Formats + Versionsnummer -├── profile.json ← Profildaten (ohne Passwort-Hash) -├── data/ -│ ├── weight.csv ← Gewichtsverlauf -│ ├── circumferences.csv ← Umfänge (8 Messpunkte) -│ ├── caliper.csv ← Caliper-Messungen -│ ├── nutrition.csv ← Ernährungsdaten -│ └── activity.csv ← Aktivitäten -├── insights/ -│ └── ai_insights.json ← KI-Auswertungen (alle gespeicherten) -└── photos/ - ├── {date}_{id}.jpg ← Progress-Fotos - └── ... -``` - -### CSV Format (alle Dateien) -``` -- Trennzeichen: Semikolon (;) – Excel/LibreOffice kompatibel -- Encoding: UTF-8 mit BOM (für Windows Excel) -- Datumsformat: YYYY-MM-DD -- Dezimaltrennzeichen: Punkt (.) -- Erste Zeile: Header -- Nullwerte: leer (nicht "null" oder "NULL") -``` - -### weight.csv Spalten -``` -id;date;weight;note;source;created -``` - -### circumferences.csv Spalten -``` -id;date;waist;hip;chest;neck;upper_arm;thigh;calf;forearm;note;created -``` - -### caliper.csv Spalten -``` -id;date;chest;abdomen;thigh;tricep;subscapular;suprailiac;midaxillary;method;bf_percent;note;created -``` - -### nutrition.csv Spalten -``` -id;date;meal_name;kcal;protein;fat;carbs;fiber;note;source;created -``` - -### activity.csv Spalten -``` -id;date;name;type;duration_min;kcal;heart_rate_avg;heart_rate_max;distance_km;note;source;created -``` - -### profile.json Struktur -```json -{ - "export_version": "2", - "export_date": "2026-03-18", - "app": "Mitai Jinkendo", - "profile": { - "name": "Lars", - "email": "lars@stommer.com", - "sex": "m", - "height": 178, - "birth_year": 1980, - "goal_weight": 82, - "goal_bf_pct": 14, - "avatar_color": "#1D9E75", - "auth_type": "password", - "session_days": 30, - "ai_enabled": true, - "tier": "selfhosted" - }, - "stats": { - "weight_entries": 150, - "nutrition_entries": 300, - "activity_entries": 45, - "photos": 12 - } -} -``` - -### ai_insights.json Struktur -```json -[ - { - "id": "uuid", - "scope": "gesamt", - "created": "2026-03-18T10:00:00", - "result": "KI-Analyse Text..." - } -] -``` - -### README.txt Inhalt -``` -Mitai Jinkendo – Datenexport -Version: 2 -Exportiert am: YYYY-MM-DD -Profil: {name} - -Inhalt: -- profile.json: Profildaten und Einstellungen -- data/*.csv: Messdaten (Semikolon-getrennt, UTF-8) -- insights/: KI-Auswertungen (JSON) -- photos/: Progress-Fotos (JPEG) - -Import: -Dieser Export kann in Mitai Jinkendo unter -Einstellungen → Import → "Mitai Backup importieren" -wieder eingespielt werden. - -Format-Version 2 (ab v9b): -Alle CSV-Dateien sind UTF-8 mit BOM kodiert. -Trennzeichen: Semikolon (;) -Datumsformat: YYYY-MM-DD -``` - -### Import-Funktion (zu implementieren) -**Endpoint:** `POST /api/import/zip` -**Verhalten:** -- Akzeptiert ZIP-Datei (multipart/form-data) -- Erkennt export_version aus profile.json -- Importiert nur fehlende Einträge (kein Duplikat) -- Fotos werden nicht überschrieben falls bereits vorhanden -- Gibt Zusammenfassung zurück: wie viele Einträge je Kategorie importiert -- Bei Fehler: vollständiger Rollback (alle oder nichts) - -**Duplikat-Erkennung:** -```python -# INSERT ... ON CONFLICT (profile_id, date) DO NOTHING -# weight: UNIQUE (profile_id, date) -# nutrition: UNIQUE (profile_id, date, meal_name) -# activity: UNIQUE (profile_id, date, name) -# caliper: UNIQUE (profile_id, date) -# circumferences: UNIQUE (profile_id, date) -``` - -**Frontend:** Neuer Button in SettingsPage: -``` -[ZIP exportieren] [JSON exportieren] [Backup importieren] +mitai.jinkendo.de → Körper-Tracker (diese App) +miken.jinkendo.de → Meditation (眉間) +ikigai.jinkendo.de → Lebenssinn (生き甲斐) +shinkan.jinkendo.de → Kampfsport (真観) ``` diff --git a/backend/apply_v9c_migration.py b/backend/apply_v9c_migration.py index a639b75..78a9fca 100644 --- a/backend/apply_v9c_migration.py +++ b/backend/apply_v9c_migration.py @@ -60,6 +60,9 @@ def apply_migration(): if not migration_needed(conn): print("[v9c Migration] Already applied, skipping.") conn.close() + + # Even if main migration is done, check cleanup + apply_cleanup_migration() return print("[v9c Migration] Applying subscription system migration...") @@ -128,10 +131,123 @@ def apply_migration(): cur.close() conn.close() + # After successful migration, apply cleanup + apply_cleanup_migration() + except Exception as e: print(f"[v9c Migration] ❌ Error: {e}") raise +def cleanup_features_needed(conn): + """Check if feature cleanup migration is needed.""" + cur = conn.cursor() + + # Check if old export features still exist + cur.execute(""" + SELECT COUNT(*) as count FROM features + WHERE id IN ('export_csv', 'export_json', 'export_zip') + """) + old_exports = cur.fetchone()['count'] + + # Check if csv_import needs to be renamed + cur.execute(""" + SELECT COUNT(*) as count FROM features + WHERE id = 'csv_import' + """) + old_import = cur.fetchone()['count'] + + cur.close() + + # Cleanup needed if old features exist + return old_exports > 0 or old_import > 0 + + +def apply_cleanup_migration(): + """Apply v9c feature cleanup migration.""" + print("[v9c Cleanup] Checking if cleanup migration is needed...") + + try: + conn = get_db_connection() + + if not cleanup_features_needed(conn): + print("[v9c Cleanup] Already applied, skipping.") + conn.close() + return + + print("[v9c Cleanup] Applying feature consolidation...") + + # Show BEFORE state + cur = conn.cursor() + cur.execute("SELECT id, name FROM features ORDER BY category, id") + features_before = [f"{r['id']} ({r['name']})" for r in cur.fetchall()] + print(f"[v9c Cleanup] Features BEFORE: {len(features_before)} features") + for f in features_before: + print(f" - {f}") + cur.close() + + # Read cleanup migration SQL + cleanup_path = os.path.join( + os.path.dirname(__file__), + "migrations", + "v9c_cleanup_features.sql" + ) + + if not os.path.exists(cleanup_path): + print(f"[v9c Cleanup] ⚠️ Cleanup migration file not found: {cleanup_path}") + conn.close() + return + + with open(cleanup_path, 'r', encoding='utf-8') as f: + cleanup_sql = f.read() + + # Execute cleanup migration + cur = conn.cursor() + cur.execute(cleanup_sql) + conn.commit() + cur.close() + + # Show AFTER state + cur = conn.cursor() + cur.execute("SELECT id, name, category FROM features ORDER BY category, id") + features_after = cur.fetchall() + print(f"[v9c Cleanup] Features AFTER: {len(features_after)} features") + + # Group by category + categories = {} + for f in features_after: + cat = f['category'] or 'other' + if cat not in categories: + categories[cat] = [] + categories[cat].append(f"{f['id']} ({f['name']})") + + for cat, feats in sorted(categories.items()): + print(f" {cat.upper()}:") + for f in feats: + print(f" - {f}") + + # Verify tier_limits updated + cur.execute(""" + SELECT tier_id, feature_id, limit_value + FROM tier_limits + WHERE feature_id IN ('data_export', 'data_import') + ORDER BY tier_id, feature_id + """) + limits = cur.fetchall() + print(f"[v9c Cleanup] Tier limits for data_export/data_import:") + for lim in limits: + limit_str = 'unlimited' if lim['limit_value'] is None else lim['limit_value'] + print(f" {lim['tier_id']}.{lim['feature_id']} = {limit_str}") + + cur.close() + conn.close() + + print("[v9c Cleanup] ✅ Feature cleanup completed successfully!") + + except Exception as e: + print(f"[v9c Cleanup] ❌ Error: {e}") + raise + + if __name__ == "__main__": apply_migration() diff --git a/backend/migrations/check_features.sql b/backend/migrations/check_features.sql new file mode 100644 index 0000000..70f10fe --- /dev/null +++ b/backend/migrations/check_features.sql @@ -0,0 +1,50 @@ +-- ============================================================================ +-- Feature Check Script - Diagnose vor/nach Migration +-- ============================================================================ +-- Usage: psql -U mitai_dev -d mitai_dev -f check_features.sql +-- ============================================================================ + +\echo '=== CURRENT FEATURES ===' +SELECT id, name, category, limit_type, reset_period, default_limit, active +FROM features +ORDER BY category, id; + +\echo '' +\echo '=== TIER LIMITS MATRIX ===' +SELECT + f.id as feature, + f.category, + MAX(CASE WHEN tl.tier_id = 'free' THEN COALESCE(tl.limit_value::text, '∞') END) as free, + MAX(CASE WHEN tl.tier_id = 'basic' THEN COALESCE(tl.limit_value::text, '∞') END) as basic, + MAX(CASE WHEN tl.tier_id = 'premium' THEN COALESCE(tl.limit_value::text, '∞') END) as premium, + MAX(CASE WHEN tl.tier_id = 'selfhosted' THEN COALESCE(tl.limit_value::text, '∞') END) as selfhosted +FROM features f +LEFT JOIN tier_limits tl ON f.id = tl.feature_id +GROUP BY f.id, f.category +ORDER BY f.category, f.id; + +\echo '' +\echo '=== FEATURE COUNT BY CATEGORY ===' +SELECT category, COUNT(*) as count +FROM features +WHERE active = true +GROUP BY category +ORDER BY category; + +\echo '' +\echo '=== ORPHANED TIER LIMITS (feature not exists) ===' +SELECT tl.tier_id, tl.feature_id, tl.limit_value +FROM tier_limits tl +LEFT JOIN features f ON tl.feature_id = f.id +WHERE f.id IS NULL; + +\echo '' +\echo '=== USER FEATURE USAGE (current usage tracking) ===' +SELECT + p.name as user, + ufu.feature_id, + ufu.usage_count, + ufu.reset_at +FROM user_feature_usage ufu +JOIN profiles p ON ufu.profile_id = p.id +ORDER BY p.name, ufu.feature_id; diff --git a/backend/migrations/v9c_cleanup_features.sql b/backend/migrations/v9c_cleanup_features.sql new file mode 100644 index 0000000..9acfbde --- /dev/null +++ b/backend/migrations/v9c_cleanup_features.sql @@ -0,0 +1,141 @@ +-- ============================================================================ +-- v9c Cleanup: Feature-Konsolidierung +-- ============================================================================ +-- Created: 2026-03-20 +-- Purpose: Konsolidiere Export-Features (export_csv/json/zip → data_export) +-- und Import-Features (csv_import → data_import) +-- +-- Idempotent: Kann mehrfach ausgeführt werden +-- +-- Lessons Learned: +-- "Ein Feature für Export, nicht drei (csv/json/zip)" +-- ============================================================================ + +-- ============================================================================ +-- 1. Rename csv_import to data_import +-- ============================================================================ +UPDATE features +SET + id = 'data_import', + name = 'Daten importieren', + description = 'CSV-Import (FDDB, Apple Health) + ZIP-Backup-Import' +WHERE id = 'csv_import'; + +-- Update tier_limits references +UPDATE tier_limits +SET feature_id = 'data_import' +WHERE feature_id = 'csv_import'; + +-- Update user_feature_restrictions references +UPDATE user_feature_restrictions +SET feature_id = 'data_import' +WHERE feature_id = 'csv_import'; + +-- Update user_feature_usage references +UPDATE user_feature_usage +SET feature_id = 'data_import' +WHERE feature_id = 'csv_import'; + +-- ============================================================================ +-- 2. Remove old export_csv/json/zip features +-- ============================================================================ + +-- Remove tier_limits for old features +DELETE FROM tier_limits +WHERE feature_id IN ('export_csv', 'export_json', 'export_zip'); + +-- Remove user restrictions for old features +DELETE FROM user_feature_restrictions +WHERE feature_id IN ('export_csv', 'export_json', 'export_zip'); + +-- Remove usage tracking for old features +DELETE FROM user_feature_usage +WHERE feature_id IN ('export_csv', 'export_json', 'export_zip'); + +-- Remove old feature definitions +DELETE FROM features +WHERE id IN ('export_csv', 'export_json', 'export_zip'); + +-- ============================================================================ +-- 3. Ensure data_export exists and is properly configured +-- ============================================================================ +INSERT INTO features (id, name, description, category, limit_type, reset_period, default_limit, active) +VALUES ('data_export', 'Daten exportieren', 'CSV/JSON/ZIP Export', 'export', 'count', 'monthly', 0, true) +ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + description = EXCLUDED.description, + category = EXCLUDED.category, + limit_type = EXCLUDED.limit_type, + reset_period = EXCLUDED.reset_period; + +-- ============================================================================ +-- 4. Ensure data_import exists and is properly configured +-- ============================================================================ +INSERT INTO features (id, name, description, category, limit_type, reset_period, default_limit, active) +VALUES ('data_import', 'Daten importieren', 'CSV-Import (FDDB, Apple Health) + ZIP-Backup-Import', 'import', 'count', 'monthly', 0, true) +ON CONFLICT (id) DO UPDATE SET + name = EXCLUDED.name, + description = EXCLUDED.description, + category = EXCLUDED.category, + limit_type = EXCLUDED.limit_type, + reset_period = EXCLUDED.reset_period; + +-- ============================================================================ +-- 5. Update tier_limits for data_export (consolidate from old features) +-- ============================================================================ + +-- FREE tier: no export +INSERT INTO tier_limits (tier_id, feature_id, limit_value) +VALUES ('free', 'data_export', 0) +ON CONFLICT (tier_id, feature_id) DO UPDATE SET limit_value = EXCLUDED.limit_value; + +-- BASIC tier: 5 exports/month +INSERT INTO tier_limits (tier_id, feature_id, limit_value) +VALUES ('basic', 'data_export', 5) +ON CONFLICT (tier_id, feature_id) DO UPDATE SET limit_value = EXCLUDED.limit_value; + +-- PREMIUM tier: unlimited +INSERT INTO tier_limits (tier_id, feature_id, limit_value) +VALUES ('premium', 'data_export', NULL) +ON CONFLICT (tier_id, feature_id) DO UPDATE SET limit_value = EXCLUDED.limit_value; + +-- SELFHOSTED tier: unlimited +INSERT INTO tier_limits (tier_id, feature_id, limit_value) +VALUES ('selfhosted', 'data_export', NULL) +ON CONFLICT (tier_id, feature_id) DO UPDATE SET limit_value = EXCLUDED.limit_value; + +-- ============================================================================ +-- 6. Update tier_limits for data_import +-- ============================================================================ + +-- FREE tier: no import +INSERT INTO tier_limits (tier_id, feature_id, limit_value) +VALUES ('free', 'data_import', 0) +ON CONFLICT (tier_id, feature_id) DO UPDATE SET limit_value = EXCLUDED.limit_value; + +-- BASIC tier: 3 imports/month +INSERT INTO tier_limits (tier_id, feature_id, limit_value) +VALUES ('basic', 'data_import', 3) +ON CONFLICT (tier_id, feature_id) DO UPDATE SET limit_value = EXCLUDED.limit_value; + +-- PREMIUM tier: unlimited +INSERT INTO tier_limits (tier_id, feature_id, limit_value) +VALUES ('premium', 'data_import', NULL) +ON CONFLICT (tier_id, feature_id) DO UPDATE SET limit_value = EXCLUDED.limit_value; + +-- SELFHOSTED tier: unlimited +INSERT INTO tier_limits (tier_id, feature_id, limit_value) +VALUES ('selfhosted', 'data_import', NULL) +ON CONFLICT (tier_id, feature_id) DO UPDATE SET limit_value = EXCLUDED.limit_value; + +-- ============================================================================ +-- Cleanup complete +-- ============================================================================ +-- Final feature list: +-- Data: weight_entries, circumference_entries, caliper_entries, +-- nutrition_entries, activity_entries, photos +-- AI: ai_calls, ai_pipeline +-- Export/Import: data_export, data_import +-- +-- Total: 10 features (down from 13) +-- ============================================================================