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 (
+ Qualitätsfilter wirkt auf alle Ansichten: Dashboard, Charts, Statistiken und KI-Analysen. +
+