From 04306a7fef129ce3c1f234593d78d9eab7c7056d Mon Sep 17 00:00:00 2001
From: Lars
Date: Mon, 23 Mar 2026 22:29:49 +0100
Subject: [PATCH] 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 && (