diff --git a/CLAUDE.md b/CLAUDE.md index 3176221..164a5a8 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` @@ -181,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 @@ -237,6 +274,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 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/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 ─────────────────────────────────────────────────────────── 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 31ab4e5..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,7 +79,14 @@ 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,)) + + # Issue #31: Global quality filter (from user profile setting) + cur.execute(f""" + SELECT * FROM activity_log + WHERE profile_id=%s + {quality_filter} + 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/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 ( diff --git a/frontend/src/pages/History.jsx b/frontend/src/pages/History.jsx index 54f2aac..9e96686 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,13 +587,15 @@ 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) 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 #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) }) @@ -620,6 +623,28 @@ function ActivitySection({ activities, insights, onRequest, loadingSlug, filterA
+ + {/* 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 → + +
+ )} +
{[['Trainings',filtA.length,'var(--text1)'],['Kcal',totalKcal,'#EF9F27'], ['Stunden',Math.round(totalMin/60*10)/10,'#378ADD'], @@ -899,6 +924,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([]) @@ -967,7 +993,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 && (