From 9210d051a8998208d26c4bf8b4115f3ed47721bb Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 16:53:29 +0100 Subject: [PATCH 1/8] docs: update CLAUDE.md - v9d Phase 2 deployed to production --- CLAUDE.md | 38 +++++++++++++++++++++++++++++--------- 1 file changed, 29 insertions(+), 9 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index 3176221..bbb6ce9 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -135,7 +135,7 @@ frontend/src/ - `.claude/docs/functional/AI_PROMPTS.md` (erweitert um Fähigkeiten-Mapping) - `.claude/docs/technical/CENTRAL_SUBSCRIPTION_SYSTEM.md` -### v9d – Phase 2 ✅ (Deployed to Dev 23.03.2026) +### v9d – Phase 2 ✅ (Deployed to Production 23.03.2026) **Vitalwerte & Erholung:** @@ -154,15 +154,23 @@ frontend/src/ - Dashboard Widget mit aktuellen Ruhetagen - ✅ **Vitalwerte erweitert (v9d Phase 2d):** - - Ruhepuls + HRV (morgens) - - Blutdruck (Systolisch/Diastolisch + Puls) - - VO2 Max (Apple Watch) - - SpO2 (Blutsauerstoffsättigung) - - Atemfrequenz + - **3-Tab Architektur:** Baseline (morgens) / Blutdruck (mehrfach täglich) / Import + - **Baseline Vitals:** Ruhepuls, HRV, VO2 Max, SpO2, Atemfrequenz + - **Blutdruck:** Systolisch/Diastolisch + Puls, WHO/ISH-Klassifizierung + - **Context-Tagging:** 8 Kontexte (nüchtern, nach Essen, Training, Stress, etc.) + - **Inline-Editing:** Alle Messungen direkt in der Liste bearbeitbar + - **Smart Upsert:** Baseline lädt existierende Einträge automatisch + - **CSV Import:** Omron (Deutsch) + Apple Health (Deutsch/Englisch) + - **Mobile-optimiert:** Volle Breite Felder, Sektions-Überschriften - Unregelmäßiger Herzschlag & AFib-Warnungen - - CSV Import: Omron (Blutdruck) + Apple Health (alle Vitals) - Trend-Analyse (7d/14d/30d) +**Bugfixes (23.03.2026):** +- ✅ Import-Zählung korrigiert (skipped vs. updated) +- ✅ Deutsche Spaltennamen für CSV-Imports (Ruhepuls, Herzfrequenzvariabilität, etc.) +- ✅ Dezimalwerte-Parsing (safe_int/safe_float für Apple Health Exports) +- ✅ Error-Details in Import-Response (erste 10 Fehler im Frontend sichtbar) + - 🔲 **HF-Zonen + Erholungsstatus (v9d Phase 2e):** - HF-Zonen-Verteilung pro Training - Recovery Score basierend auf Ruhepuls + HRV + Schlaf @@ -172,8 +180,9 @@ frontend/src/ - Migration 010: sleep_log Tabelle (JSONB segments) - Migration 011: rest_days Tabelle (Kraft, Cardio, Entspannung) - Migration 012: Unique constraint rest_days (profile_id, date, rest_type) -- Migration 013: vitals_log Tabelle (Ruhepuls, HRV) -- Migration 014: Extended vitals (BP, VO2 Max, SpO2, respiratory_rate) +- Migration 013: vitals_log Tabelle (Ruhepuls, HRV) - deprecated +- Migration 014: Extended vitals (BP, VO2 Max, SpO2, respiratory_rate) - deprecated +- Migration 015: **Vitals Refactoring** - Trennung in vitals_baseline + blood_pressure_log 📚 Details: `.claude/docs/functional/TRAINING_TYPES.md` @@ -237,6 +246,17 @@ subscriptions · coupons · coupon_redemptions · features tier_limits · user_feature_restrictions · user_feature_usage access_grants · user_activity_log +v9d neu (Training & Vitals): +training_types – 29 Trainingstypen in 7 Kategorien +activity_type_mappings – Lernendes Mapping-System (Deutsch/Englisch) +sleep_log – Schlaf mit JSONB segments (Phasen) +rest_days – Multi-dimensionale Ruhetage (Kraft/Cardio/Entspannung) +vitals_baseline – Morgenmessung (RHR, HRV, VO2 Max, SpO2, resp_rate) +blood_pressure_log – Blutdruck mehrfach täglich mit Context-Tagging + +Deprecated (v9d Phase 2d): +vitals_log → vitals_log_backup_pre_015 (nach Migration 015) + Infrastruktur: schema_migrations – Tracking für automatische DB-Migrationen -- 2.43.0 From 9ec774e956598d1c087209f587280095130f9b5d Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 21:59:02 +0100 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20Quality-Filter=20f=C3=BCr=20KI-Pipe?= =?UTF-8?q?line=20&=20History=20(#24)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Backend: - insights.py: KI-Pipeline filtert activity_log nach quality_label - Nur 'excellent', 'good', 'acceptable' (poor wird ausgeschlossen) - NULL-Werte erlaubt (für alte Einträge vor Migration 014) Frontend: - History.jsx: Toggle "Nur qualitativ hochwertige Aktivitäten" - Filter wirkt auf Activity-Statistiken, Charts, Listen - Anzeige: X von Y Activities (wenn gefiltert) Dokumentation: - CLAUDE.md: Feature-Roadmap aktualisiert (Phase 0-2) Closes #24 Co-Authored-By: Claude Opus 4.6 --- CLAUDE.md | 52 ++++++++++++++++++++++++++-------- backend/routers/insights.py | 8 +++++- frontend/src/pages/History.jsx | 25 +++++++++++++++- 3 files changed, 71 insertions(+), 14 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index bbb6ce9..164a5a8 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -190,19 +190,47 @@ frontend/src/ ## Feature-Roadmap -> Vollständiges Backlog: `.claude/docs/BACKLOG.md` -> Beim Implementieren: verlinkte Dok-Datei zuerst lesen! +> 📋 **Detaillierte Roadmap:** `.claude/docs/ROADMAP.md` (Phasen 0-3, Timeline, Abhängigkeiten) +> 📚 **Vollständiges Backlog:** `.claude/docs/BACKLOG.md` +> 🎯 **Gitea Issues:** http://192.168.2.144:3000/Lars/mitai-jinkendo/issues +> +> **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` ✅ | +### Aktuelle Entwicklung (Phase 0-2, ~10-13 Wochen) + +| Phase | Fokus | Dauer | Gitea Issues | +|-------|-------|-------|--------------| +| **Phase 0** | Infrastruktur (v9f) | 4-6 Wochen | #24, #28, #29, #30 | +| **Phase 1** | Foundation (Charts, Goals) | 2-3 Wochen | #26, #25 | +| **Phase 2** | Engagement (Korrelationen, KI) | 3-4 Wochen | #27, #25 | +| **Phase 3** | Begleitung (Development Routes) | später | - | + +**Phase 0 Issues:** +- #24: Quality-Filter (3h, Quick Win) ← **Start hier** +- #28: AI-Prompts Flexibilisierung (16-20h, kritisch) +- #29: Abilities-Matrix UI (6-8h) +- #30: Responsive UI (8-10h, parallel) + +**Phase 1 Issues:** +- #26: Charts erweitern (8-10h) +- #25: Ziele-System Basis (10-12h) + +**Phase 2 Issues:** +- #27: Korrelationen (6-8h) +- #25: Goals KI-Integration (4h) + +### Versions-Übersicht + +| Version | Feature | Dokumentation | Status | +|---------|---------|---------------|--------| +| v9c | Membership (aktiv) | `technical/MEMBERSHIP_SYSTEM.md` | ✅ Production | +| v9d | Schlaf-Modul | `functional/SLEEP_MODULE.md` | ✅ Production | +| v9d | Trainingstypen + HF | `functional/TRAINING_TYPES.md` | ✅ Production | +| v9e | Ziele + Vitalwerte | `functional/GOALS_VITALS.md` | 🔲 Phase 1 | +| v9f | KI-Prompt Flexibilisierung | `functional/AI_PROMPTS.md` | 🔲 Phase 0 | +| v9g | Meditation + Selbstwahrnehmung | `functional/MEDITATION.md` | 🔲 Phase 3 | +| v9h | Connectoren + Stripe | ausstehend | 🔲 Später | +| — | Responsive UI | `functional/RESPONSIVE_UI.md` | 🔲 Phase 0 | ## Deployment diff --git a/backend/routers/insights.py b/backend/routers/insights.py index 31ab4e5..4866b83 100644 --- a/backend/routers/insights.py +++ b/backend/routers/insights.py @@ -75,7 +75,13 @@ def _get_profile_data(pid: str): caliper = [r2d(r) for r in cur.fetchall()] cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) nutrition = [r2d(r) for r in cur.fetchall()] - cur.execute("SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) + # Quality-Filter: nur hochwertige Aktivitäten für KI-Analyse (Issue #24) + cur.execute(""" + SELECT * FROM activity_log + WHERE profile_id=%s + AND (quality_label IN ('excellent', 'good', 'acceptable') OR quality_label IS NULL) + ORDER BY date DESC LIMIT 90 + """, (pid,)) activity = [r2d(r) for r in cur.fetchall()] # v9d Phase 2: Sleep, Rest Days, Vitals cur.execute("SELECT * FROM sleep_log WHERE profile_id=%s ORDER BY date DESC LIMIT 30", (pid,)) diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 54f2aac..f7e4447 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -588,11 +588,16 @@ function NutritionSection({ nutrition, weights, profile, insights, onRequest, lo // ── Activity Section ────────────────────────────────────────────────────────── function ActivitySection({ activities, insights, onRequest, loadingSlug, filterActiveSlugs }) { const [period, setPeriod] = useState(30) + const [qualityFilter, setQualityFilter] = useState(false) // Issue #24: Quality-Filter Toggle if (!activities?.length) return ( ) const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD') - const filtA = activities.filter(d=>period===9999||d.date>=cutoff) + // Issue #24: Filter nach Datum UND Quality-Label + const filtA = activities.filter(d => + (period===9999 || d.date>=cutoff) && + (!qualityFilter || ['excellent', 'good', 'acceptable'].includes(d.quality_label)) + ) const byDate={} filtA.forEach(a=>{ byDate[a.date]=(byDate[a.date]||0)+(a.kcal_active||0) }) @@ -620,6 +625,24 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA
+ + {/* Issue #24: Quality-Filter Toggle */} +
+ + {qualityFilter && ( + + ({filtA.length} von {activities.filter(d=>period===9999||d.date>=cutoff).length}) + + )} +
+
{[['Trainings',filtA.length,'var(--text1)'],['Kcal',totalKcal,'#EF9F27'], ['Stunden',Math.round(totalMin/60*10)/10,'#378ADD'], -- 2.43.0 From 848ba0a815221dca01052ccf8edad054aaa50bca Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 22:04:29 +0100 Subject: [PATCH 3/8] refactor: mehrstufiger Quality-Filter statt Toggle (#24) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Statt einfachem On/Off Toggle jetzt 4 Qualitätsstufen: - 📊 Alle (kein Filter) - ✓ Hochwertig (excellent + good + acceptable) - ✓✓ Sehr gut (excellent + good) - ⭐ Exzellent (nur excellent) UI: - Button-Group (Segmented Control) mit 4 Stufen - Beschreibung welche Labels inkludiert werden - Anzeige: X von Y Aktivitäten (wenn gefiltert) User-Feedback: Stufenweiser Filter ist flexibler als binärer Toggle Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/History.jsx | 60 ++++++++++++++++++++++------------ 1 file changed, 40 insertions(+), 20 deletions(-) diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index f7e4447..22c966a 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -588,16 +588,22 @@ function NutritionSection({ nutrition, weights, profile, insights, onRequest, lo // ── Activity Section ────────────────────────────────────────────────────────── function ActivitySection({ activities, insights, onRequest, loadingSlug, filterActiveSlugs }) { const [period, setPeriod] = useState(30) - const [qualityFilter, setQualityFilter] = useState(false) // Issue #24: Quality-Filter Toggle + const [qualityLevel, setQualityLevel] = useState('all') // Issue #24: Quality-Filter (all, quality, very_good, excellent) if (!activities?.length) return ( ) const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD') - // Issue #24: Filter nach Datum UND Quality-Label - const filtA = activities.filter(d => - (period===9999 || d.date>=cutoff) && - (!qualityFilter || ['excellent', 'good', 'acceptable'].includes(d.quality_label)) - ) + + // Issue #24: Mehrstufiger Quality-Filter + const filtA = activities.filter(d => { + if (period !== 9999 && d.date < cutoff) return false + + if (qualityLevel === 'all') return true + if (qualityLevel === 'quality') return ['excellent', 'good', 'acceptable'].includes(d.quality_label) + if (qualityLevel === 'very_good') return ['excellent', 'good'].includes(d.quality_label) + if (qualityLevel === 'excellent') return d.quality_label === 'excellent' + return true + }) const byDate={} filtA.forEach(a=>{ byDate[a.date]=(byDate[a.date]||0)+(a.kcal_active||0) }) @@ -626,20 +632,34 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA - {/* Issue #24: Quality-Filter Toggle */} -
- - {qualityFilter && ( - - ({filtA.length} von {activities.filter(d=>period===9999||d.date>=cutoff).length}) - + {/* Issue #24: Mehrstufiger Quality-Filter */} +
+
QUALITÄTSFILTER
+
+ {[ + {v:'all', l:'Alle', icon:'📊'}, + {v:'quality', l:'Hochwertig', icon:'✓'}, + {v:'very_good', l:'Sehr gut', icon:'✓✓'}, + {v:'excellent', l:'Exzellent', icon:'⭐'} + ].map(o => ( + + ))} +
+ {qualityLevel !== 'all' && ( +
+ {filtA.length} von {activities.filter(d=>period===9999||d.date>=cutoff).length} Aktivitäten + {qualityLevel === 'quality' && ' (excellent, good, acceptable)'} + {qualityLevel === 'very_good' && ' (excellent, good)'} + {qualityLevel === 'excellent' && ' (nur excellent)'} +
)}
-- 2.43.0 From b317246bcd98a6c51a474b8d153f058930fc2cc7 Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 22:06:30 +0100 Subject: [PATCH 4/8] =?UTF-8?q?docs:=20Quality-Level=20Parameter=20f=C3=BC?= =?UTF-8?q?r=20KI-Analysen=20notiert=20(#28)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Notiert an 3 Stellen: 1. insights.py: TODO-Kommentar im Code 2. ROADMAP.md: Deliverable bei M0.2 (lokal, nicht im Git) 3. Gitea Issue #28: Kommentar mit Spezifikation Zukünftig: - GET /api/insights/run/{slug}?quality_level=quality - 4 Stufen: all, quality, very_good, excellent - Frontend: Dropdown wie in History.jsx - Pipeline-Configs können Standard-Level haben User-Request: Quality-Level-Auswahl für KI-Analysen Co-Authored-By: Claude Opus 4.6 --- backend/routers/insights.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/backend/routers/insights.py b/backend/routers/insights.py index 4866b83..4643418 100644 --- a/backend/routers/insights.py +++ b/backend/routers/insights.py @@ -76,6 +76,8 @@ def _get_profile_data(pid: str): cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) nutrition = [r2d(r) for r in cur.fetchall()] # Quality-Filter: nur hochwertige Aktivitäten für KI-Analyse (Issue #24) + # TODO (#28): quality_level als Parameter (all, quality, very_good, excellent) + # Zukünftig: GET /api/insights/run/{slug}?quality_level=quality cur.execute(""" SELECT * FROM activity_log WHERE profile_id=%s -- 2.43.0 From 04306a7fef129ce3c1f234593d78d9eab7c7056d Mon Sep 17 00:00:00 2001 From: Lars Date: Mon, 23 Mar 2026 22:29:49 +0100 Subject: [PATCH 5/8] feat: global quality filter setting (Issue #31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Implemented global quality_filter_level in user profiles for consistent data filtering across all views (Dashboard, History, Charts, KI-Pipeline). Backend changes: - Migration 016: Add quality_filter_level column to profiles table - quality_filter.py: Centralized helper functions for SQL filtering - insights.py: Apply global filter in _get_profile_data() - activity.py: Apply global filter in list_activity() Frontend changes: - SettingsPage.jsx: Add Datenqualität section with 4-level selector - History.jsx: Use global quality filter from profile context Filter levels: all, quality (good+excellent+acceptable), very_good (good+excellent), excellent (only excellent) Closes #31 Co-Authored-By: Claude Opus 4.6 --- .../migrations/016_global_quality_filter.sql | 21 +++ backend/quality_filter.py | 125 ++++++++++++++++++ backend/routers/activity.py | 16 ++- backend/routers/insights.py | 13 +- frontend/src/pages/History.jsx | 9 +- frontend/src/pages/SettingsPage.jsx | 52 ++++++++ 6 files changed, 226 insertions(+), 10 deletions(-) create mode 100644 backend/migrations/016_global_quality_filter.sql create mode 100644 backend/quality_filter.py diff --git a/backend/migrations/016_global_quality_filter.sql b/backend/migrations/016_global_quality_filter.sql new file mode 100644 index 0000000..ab0a968 --- /dev/null +++ b/backend/migrations/016_global_quality_filter.sql @@ -0,0 +1,21 @@ +-- Migration 016: Global Quality Filter Setting +-- Issue: #31 +-- Date: 2026-03-23 +-- Description: Add quality_filter_level to profiles for consistent data views + +-- Add quality_filter_level column to profiles +ALTER TABLE profiles ADD COLUMN IF NOT EXISTS quality_filter_level VARCHAR(20) DEFAULT 'all'; + +COMMENT ON COLUMN profiles.quality_filter_level IS 'Global quality filter for all activity views: all, quality, very_good, excellent'; + +-- Create index for performance (if filtering becomes common) +CREATE INDEX IF NOT EXISTS idx_profiles_quality_filter ON profiles(quality_filter_level); + +-- Migration tracking +DO $$ +BEGIN + RAISE NOTICE '✓ Migration 016: Added global quality filter setting'; + RAISE NOTICE ' - Added profiles.quality_filter_level column'; + RAISE NOTICE ' - Default: all (no filter)'; + RAISE NOTICE ' - Values: all, quality, very_good, excellent'; +END $$; diff --git a/backend/quality_filter.py b/backend/quality_filter.py new file mode 100644 index 0000000..ec6e312 --- /dev/null +++ b/backend/quality_filter.py @@ -0,0 +1,125 @@ +""" +Quality Filter Helper - Data Access Layer + +Provides consistent quality filtering across all activity queries. +Issue: #31 +""" +from typing import Optional, Dict + + +def get_quality_filter_sql(profile: Dict, table_alias: str = "") -> str: + """ + Returns SQL WHERE clause fragment for quality filtering. + + Args: + profile: User profile dict with quality_filter_level + table_alias: Optional table alias (e.g., "a." for "a.quality_label") + + Returns: + SQL fragment (e.g., "AND quality_label IN (...)") or empty string + + Examples: + >>> get_quality_filter_sql({'quality_filter_level': 'all'}) + '' + >>> get_quality_filter_sql({'quality_filter_level': 'quality'}) + "AND quality_label IN ('excellent', 'good', 'acceptable')" + >>> get_quality_filter_sql({'quality_filter_level': 'excellent'}, 'a.') + "AND a.quality_label = 'excellent'" + """ + level = profile.get('quality_filter_level', 'all') + prefix = table_alias if table_alias else "" + + if level == 'all': + return '' # No filter + elif level == 'quality': + return f"AND {prefix}quality_label IN ('excellent', 'good', 'acceptable')" + elif level == 'very_good': + return f"AND {prefix}quality_label IN ('excellent', 'good')" + elif level == 'excellent': + return f"AND {prefix}quality_label = 'excellent'" + else: + # Unknown level → no filter (safe fallback) + return '' + + +def get_quality_filter_tuple(profile: Dict) -> tuple: + """ + Returns tuple of allowed quality labels for Python filtering. + + Args: + profile: User profile dict with quality_filter_level + + Returns: + Tuple of allowed quality labels or None (no filter) + + Examples: + >>> get_quality_filter_tuple({'quality_filter_level': 'all'}) + None + >>> get_quality_filter_tuple({'quality_filter_level': 'quality'}) + ('excellent', 'good', 'acceptable') + """ + level = profile.get('quality_filter_level', 'all') + + if level == 'all': + return None # No filter + elif level == 'quality': + return ('excellent', 'good', 'acceptable') + elif level == 'very_good': + return ('excellent', 'good') + elif level == 'excellent': + return ('excellent',) + else: + return None # Unknown level → no filter + + +def filter_activities_by_quality(activities: list, profile: Dict) -> list: + """ + Filters a list of activity dicts by quality_label. + + Useful for post-query filtering (e.g., when data already loaded). + + Args: + activities: List of activity dicts with quality_label field + profile: User profile dict with quality_filter_level + + Returns: + Filtered list of activities + """ + allowed_labels = get_quality_filter_tuple(profile) + + if allowed_labels is None: + return activities # No filter + + return [ + act for act in activities + if act.get('quality_label') in allowed_labels + ] + + +# Constants for frontend/documentation +QUALITY_LEVELS = { + 'all': { + 'label': 'Alle', + 'icon': '📊', + 'description': 'Alle Activities (kein Filter)', + 'includes': None + }, + 'quality': { + 'label': 'Hochwertig', + 'icon': '✓', + 'description': 'Hochwertige Activities', + 'includes': ['excellent', 'good', 'acceptable'] + }, + 'very_good': { + 'label': 'Sehr gut', + 'icon': '✓✓', + 'description': 'Sehr gute Activities', + 'includes': ['excellent', 'good'] + }, + 'excellent': { + 'label': 'Exzellent', + 'icon': '⭐', + 'description': 'Nur exzellente Activities', + 'includes': ['excellent'] + } +} diff --git a/backend/routers/activity.py b/backend/routers/activity.py index a7a2e64..c3b57a0 100644 --- a/backend/routers/activity.py +++ b/backend/routers/activity.py @@ -16,6 +16,7 @@ from auth import require_auth, check_feature_access, increment_feature_usage from models import ActivityEntry from routers.profiles import get_pid from feature_logger import log_feature_usage +from quality_filter import get_quality_filter_sql # Evaluation import with error handling (Phase 1.2) try: @@ -36,8 +37,19 @@ def list_activity(limit: int=200, x_profile_id: Optional[str]=Header(default=Non pid = get_pid(x_profile_id) with get_db() as conn: cur = get_cursor(conn) - cur.execute( - "SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date DESC, start_time DESC LIMIT %s", (pid,limit)) + + # Issue #31: Apply global quality filter + cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) + profile = r2d(cur.fetchone()) + quality_filter = get_quality_filter_sql(profile) + + cur.execute(f""" + SELECT * FROM activity_log + WHERE profile_id=%s + {quality_filter} + ORDER BY date DESC, start_time DESC + LIMIT %s + """, (pid, limit)) return [r2d(r) for r in cur.fetchall()] diff --git a/backend/routers/insights.py b/backend/routers/insights.py index 4643418..0127241 100644 --- a/backend/routers/insights.py +++ b/backend/routers/insights.py @@ -17,6 +17,7 @@ from db import get_db, get_cursor, r2d from auth import require_auth, require_admin, check_feature_access, increment_feature_usage from routers.profiles import get_pid from feature_logger import log_feature_usage +from quality_filter import get_quality_filter_sql router = APIRouter(prefix="/api", tags=["insights"]) logger = logging.getLogger(__name__) @@ -67,6 +68,9 @@ def _get_profile_data(pid: str): cur = get_cursor(conn) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) prof = r2d(cur.fetchone()) + + # Issue #31: Get global quality filter setting + quality_filter = get_quality_filter_sql(prof) cur.execute("SELECT * FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) weight = [r2d(r) for r in cur.fetchall()] cur.execute("SELECT * FROM circumference_log WHERE profile_id=%s ORDER BY date DESC LIMIT 30", (pid,)) @@ -75,13 +79,12 @@ def _get_profile_data(pid: str): caliper = [r2d(r) for r in cur.fetchall()] cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) nutrition = [r2d(r) for r in cur.fetchall()] - # Quality-Filter: nur hochwertige Aktivitäten für KI-Analyse (Issue #24) - # TODO (#28): quality_level als Parameter (all, quality, very_good, excellent) - # Zukünftig: GET /api/insights/run/{slug}?quality_level=quality - cur.execute(""" + + # Issue #31: Global quality filter (from user profile setting) + cur.execute(f""" SELECT * FROM activity_log WHERE profile_id=%s - AND (quality_label IN ('excellent', 'good', 'acceptable') OR quality_label IS NULL) + {quality_filter} ORDER BY date DESC LIMIT 90 """, (pid,)) activity = [r2d(r) for r in cur.fetchall()] diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 22c966a..cd6f36a 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -1,5 +1,6 @@ import { useState, useEffect } from 'react' import { useNavigate, useLocation } from 'react-router-dom' +import { useProfile } from '../context/ProfileContext' import { LineChart, Line, BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid, @@ -586,9 +587,10 @@ function NutritionSection({ nutrition, weights, profile, insights, onRequest, lo } // ── Activity Section ────────────────────────────────────────────────────────── -function ActivitySection({ activities, insights, onRequest, loadingSlug, filterActiveSlugs }) { +function ActivitySection({ activities, insights, onRequest, loadingSlug, filterActiveSlugs, globalQualityLevel }) { const [period, setPeriod] = useState(30) - const [qualityLevel, setQualityLevel] = useState('all') // Issue #24: Quality-Filter (all, quality, very_good, excellent) + // Issue #31: Use global quality filter from profile as default + const [qualityLevel, setQualityLevel] = useState(globalQualityLevel || 'all') if (!activities?.length) return ( ) @@ -942,6 +944,7 @@ const TABS = [ ] export default function History() { + const { activeProfile } = useProfile() // Issue #31: Get global quality filter const location = useLocation?.() || {} const [tab, setTab] = useState((location.state?.tab)||'body') const [weights, setWeights] = useState([]) @@ -1010,7 +1013,7 @@ export default function History() {
{tab==='body' && } {tab==='nutrition' && } - {tab==='activity' && } + {tab==='activity' && } {tab==='correlation' && } {tab==='photos' && }
diff --git a/frontend/src/pages/SettingsPage.jsx b/frontend/src/pages/SettingsPage.jsx index fb49dcf..01eaf8a 100644 --- a/frontend/src/pages/SettingsPage.jsx +++ b/frontend/src/pages/SettingsPage.jsx @@ -202,6 +202,16 @@ export default function SettingsPage() { } } + const handleQualityFilterChange = async (level) => { + // Issue #31: Update global quality filter + await api.updateActiveProfile({ quality_filter_level: level }) + await refreshProfiles() + const updated = profiles.find(p => p.id === activeProfile?.id) + if (updated) setActiveProfile({...updated, quality_filter_level: level}) + setSaved(true) + setTimeout(() => setSaved(false), 2000) + } + const handleSave = async (form, profileId) => { const data = {} if (form.name) data.name = form.name @@ -455,6 +465,48 @@ export default function SettingsPage() {

+ {/* Issue #31: Global Quality Filter */} +
+
Datenqualität
+

+ Qualitätsfilter wirkt auf alle Ansichten: Dashboard, Charts, Statistiken und KI-Analysen. +

+
+ + +
+
+
Aktuell: { + { + 'all': '📊 Alle Activities (kein Filter)', + 'quality': '✓ Hochwertig (excellent, good, acceptable)', + 'very_good': '✓✓ Sehr gut (excellent, good)', + 'excellent': '⭐ Exzellent (nur excellent)' + }[activeProfile?.quality_filter_level || 'all'] + }
+ Diese Einstellung wirkt auf: +
    +
  • Dashboard Charts
  • +
  • Verlauf & Auswertungen
  • +
  • Trainingstyp-Verteilung
  • +
  • KI-Analysen & Pipeline
  • +
  • Alle Statistiken
  • +
+
+
+ {saved && (
Date: Tue, 24 Mar 2026 06:30:39 +0100 Subject: [PATCH 6/8] fix: reload TrainingTypeDistribution on quality filter change (Issue #31) The component was loading data from backend (which uses global filter) but useEffect dependency didn't include quality_filter_level, so it didn't reload when user changed the filter in Settings. Added useProfile() context and activeProfile.quality_filter_level to dependency array. Co-Authored-By: Claude Opus 4.6 --- frontend/src/components/TrainingTypeDistribution.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/frontend/src/components/TrainingTypeDistribution.jsx b/frontend/src/components/TrainingTypeDistribution.jsx index 39889f8..0c07780 100644 --- a/frontend/src/components/TrainingTypeDistribution.jsx +++ b/frontend/src/components/TrainingTypeDistribution.jsx @@ -1,6 +1,7 @@ import { useState, useEffect } from 'react' import { PieChart, Pie, Cell, ResponsiveContainer, Legend, Tooltip } from 'recharts' import { api } from '../utils/api' +import { useProfile } from '../context/ProfileContext' /** * TrainingTypeDistribution - Pie chart showing activity distribution by type @@ -8,6 +9,7 @@ import { api } from '../utils/api' * @param {number} days - Number of days to analyze (default: 28) */ export default function TrainingTypeDistribution({ days = 28 }) { + const { activeProfile } = useProfile() // Issue #31: Trigger reload on quality filter change const [data, setData] = useState([]) const [categories, setCategories] = useState({}) const [loading, setLoading] = useState(true) @@ -41,7 +43,7 @@ export default function TrainingTypeDistribution({ days = 28 }) { console.error('Failed to load training type distribution:', err) setLoading(false) }) - }, [days]) + }, [days, activeProfile?.quality_filter_level]) // Issue #31: Reload when quality filter changes if (loading) { return ( -- 2.43.0 From 302948a2488a7e8c58f5c64c600fc58765025313 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 24 Mar 2026 06:44:05 +0100 Subject: [PATCH 7/8] fix: add quality_filter_level to ProfileUpdate model (Issue #31) The frontend was sending quality_filter_level to the backend, but the Pydantic ProfileUpdate model didn't include this field, so it was silently ignored. Profile updates never actually saved the filter. This is why the charts didn't react to filter changes - the backend database was never updated. Co-Authored-By: Claude Opus 4.6 --- backend/models.py | 1 + 1 file changed, 1 insertion(+) diff --git a/backend/models.py b/backend/models.py index 86bdb8c..8025ca3 100644 --- a/backend/models.py +++ b/backend/models.py @@ -27,6 +27,7 @@ class ProfileUpdate(BaseModel): height: Optional[float] = None goal_weight: Optional[float] = None goal_bf_pct: Optional[float] = None + quality_filter_level: Optional[str] = None # Issue #31: Global quality filter # ── Tracking Models ─────────────────────────────────────────────────────────── -- 2.43.0 From 5796c6a21aa17b90406c9a01e4586f19c8f6d291 Mon Sep 17 00:00:00 2001 From: Lars Date: Tue, 24 Mar 2026 08:06:20 +0100 Subject: [PATCH 8/8] refactor: replace local quality filter with info banner (Issue #31) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Removed local quality filter UI from History page since backend now handles filtering globally. Activities are already filtered when loaded. Changes: - Removed qualityLevel local state - Simplified filtA to only filter by period - Replaced filter buttons with info banner showing active global filter - Added 'Hier ändern →' link to Settings User can now only change quality filter in Settings (global), not per page. History shows which filter is active with link to change it. Co-Authored-By: Claude Opus 4.6 --- frontend/src/pages/History.jsx | 62 ++++++++++++---------------------- 1 file changed, 21 insertions(+), 41 deletions(-) diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index cd6f36a..9e96686 100644 --- a/frontend/src/pages/History.jsx +++ b/frontend/src/pages/History.jsx @@ -589,23 +589,13 @@ function NutritionSection({ nutrition, weights, profile, insights, onRequest, lo // ── Activity Section ────────────────────────────────────────────────────────── function ActivitySection({ activities, insights, onRequest, loadingSlug, filterActiveSlugs, globalQualityLevel }) { const [period, setPeriod] = useState(30) - // Issue #31: Use global quality filter from profile as default - const [qualityLevel, setQualityLevel] = useState(globalQualityLevel || 'all') if (!activities?.length) return ( ) const cutoff = dayjs().subtract(period,'day').format('YYYY-MM-DD') - // Issue #24: Mehrstufiger Quality-Filter - const filtA = activities.filter(d => { - if (period !== 9999 && d.date < cutoff) return false - - if (qualityLevel === 'all') return true - if (qualityLevel === 'quality') return ['excellent', 'good', 'acceptable'].includes(d.quality_label) - if (qualityLevel === 'very_good') return ['excellent', 'good'].includes(d.quality_label) - if (qualityLevel === 'excellent') return d.quality_label === 'excellent' - return true - }) + // Issue #31: Backend already filters by global quality level - only filter by period here + const filtA = activities.filter(d => period === 9999 || d.date >= cutoff) const byDate={} filtA.forEach(a=>{ byDate[a.date]=(byDate[a.date]||0)+(a.kcal_active||0) }) @@ -634,36 +624,26 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA - {/* Issue #24: Mehrstufiger Quality-Filter */} -
-
QUALITÄTSFILTER
-
- {[ - {v:'all', l:'Alle', icon:'📊'}, - {v:'quality', l:'Hochwertig', icon:'✓'}, - {v:'very_good', l:'Sehr gut', icon:'✓✓'}, - {v:'excellent', l:'Exzellent', icon:'⭐'} - ].map(o => ( - - ))} + {/* Issue #31: Show active global quality filter */} + {globalQualityLevel && globalQualityLevel !== 'all' && ( +
+ + {globalQualityLevel === 'quality' && '✓ Filter: Hochwertig (excellent, good, acceptable)'} + {globalQualityLevel === 'very_good' && '✓✓ Filter: Sehr gut (excellent, good)'} + {globalQualityLevel === 'excellent' && '⭐ Filter: Exzellent (nur excellent)'} + + + Hier ändern → +
- {qualityLevel !== 'all' && ( -
- {filtA.length} von {activities.filter(d=>period===9999||d.date>=cutoff).length} Aktivitäten - {qualityLevel === 'quality' && ' (excellent, good, acceptable)'} - {qualityLevel === 'very_good' && ' (excellent, good)'} - {qualityLevel === 'excellent' && ' (nur excellent)'} -
- )} -
+ )}
{[['Trainings',filtA.length,'var(--text1)'],['Kcal',totalKcal,'#EF9F27'], -- 2.43.0