Compare commits

...

9 Commits

Author SHA1 Message Date
ac4c6760d7 Merge pull request 'globaler Filter für Qualitätsgates von Trainings' (#41) from develop into main
All checks were successful
Deploy Production / deploy (push) Successful in 52s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 13s
Reviewed-on: #41
2026-03-24 08:44:22 +01:00
5796c6a21a refactor: replace local quality filter with info banner (Issue #31)
All checks were successful
Deploy Development / deploy (push) Successful in 50s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s
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 <noreply@anthropic.com>
2026-03-24 08:06:20 +01:00
302948a248 fix: add quality_filter_level to ProfileUpdate model (Issue #31)
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
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 <noreply@anthropic.com>
2026-03-24 06:44:05 +01:00
e3819327a9 fix: reload TrainingTypeDistribution on quality filter change (Issue #31)
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
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 <noreply@anthropic.com>
2026-03-24 06:30:39 +01:00
04306a7fef feat: global quality filter setting (Issue #31)
All checks were successful
Deploy Development / deploy (push) Successful in 44s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
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 <noreply@anthropic.com>
2026-03-23 22:29:49 +01:00
b317246bcd docs: Quality-Level Parameter für KI-Analysen notiert (#28)
All checks were successful
Deploy Development / deploy (push) Successful in 45s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
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 <noreply@anthropic.com>
2026-03-23 22:06:30 +01:00
848ba0a815 refactor: mehrstufiger Quality-Filter statt Toggle (#24)
All checks were successful
Deploy Development / deploy (push) Successful in 51s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 14s
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 <noreply@anthropic.com>
2026-03-23 22:04:29 +01:00
9ec774e956 feat: Quality-Filter für KI-Pipeline & History (#24)
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 1s
Build Test / build-frontend (push) Successful in 14s
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 <noreply@anthropic.com>
2026-03-23 21:59:02 +01:00
9210d051a8 docs: update CLAUDE.md - v9d Phase 2 deployed to production
All checks were successful
Deploy Development / deploy (push) Successful in 49s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s
2026-03-23 16:53:29 +01:00
9 changed files with 326 additions and 28 deletions

View File

@ -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

View File

@ -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 $$;

View File

@ -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 ───────────────────────────────────────────────────────────

125
backend/quality_filter.py Normal file
View File

@ -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']
}
}

View File

@ -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()]

View File

@ -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,))

View File

@ -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 (

View File

@ -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 (
<EmptySection text="Noch keine Aktivitätsdaten." to="/activity" toLabel="Aktivität erfassen"/>
)
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
<div>
<SectionHeader title="🏋️ Aktivität" to="/activity" toLabel="Alle Einträge" lastUpdated={activities[0]?.date}/>
<PeriodSelector value={period} onChange={setPeriod}/>
{/* Issue #31: Show active global quality filter */}
{globalQualityLevel && globalQualityLevel !== 'all' && (
<div style={{
marginBottom:12, padding:'8px 12px', borderRadius:8,
background:'var(--surface2)', border:'1px solid var(--border)',
fontSize:12, color:'var(--text2)', display:'flex', alignItems:'center', gap:8
}}>
<span>
{globalQualityLevel === 'quality' && '✓ Filter: Hochwertig (excellent, good, acceptable)'}
{globalQualityLevel === 'very_good' && '✓✓ Filter: Sehr gut (excellent, good)'}
{globalQualityLevel === 'excellent' && '⭐ Filter: Exzellent (nur excellent)'}
</span>
<a href="/settings" style={{
marginLeft:'auto', color:'var(--accent)', textDecoration:'none',
fontSize:11, fontWeight:500, whiteSpace:'nowrap'
}}>
Hier ändern
</a>
</div>
)}
<div style={{display:'flex',gap:6,marginBottom:12}}>
{[['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() {
</div>
{tab==='body' && <BodySection weights={weights} calipers={calipers} circs={circs} profile={profile} {...sp}/>}
{tab==='nutrition' && <NutritionSection nutrition={nutrition} weights={weights} profile={profile} {...sp}/>}
{tab==='activity' && <ActivitySection activities={activities} {...sp}/>}
{tab==='activity' && <ActivitySection activities={activities} globalQualityLevel={activeProfile?.quality_filter_level} {...sp}/>}
{tab==='correlation' && <CorrelationSection corrData={corrData} profile={profile} {...sp}/>}
{tab==='photos' && <PhotoGrid/>}
</div>

View File

@ -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() {
</p>
</div>
{/* Issue #31: Global Quality Filter */}
<div className="card section-gap">
<div className="card-title">Datenqualität</div>
<p style={{fontSize:13,color:'var(--text2)',marginBottom:12,lineHeight:1.6}}>
Qualitätsfilter wirkt auf <strong>alle Ansichten</strong>: Dashboard, Charts, Statistiken und KI-Analysen.
</p>
<div style={{marginBottom:8}}>
<label style={{fontSize:12,fontWeight:600,color:'var(--text3)',display:'block',marginBottom:6}}>
QUALITÄTSFILTER (GLOBAL)
</label>
<select
className="form-select"
value={activeProfile?.quality_filter_level || 'all'}
onChange={e => handleQualityFilterChange(e.target.value)}
style={{width:'100%',fontSize:13}}>
<option value="all">📊 Alle Activities</option>
<option value="quality"> Hochwertig (excellent, good, acceptable)</option>
<option value="very_good"> Sehr gut (excellent, good)</option>
<option value="excellent"> Exzellent (nur excellent)</option>
</select>
</div>
<div style={{padding:'10px 12px',background:'var(--surface2)',borderRadius:8,fontSize:11,
color:'var(--text3)',lineHeight:1.5}}>
<div style={{fontWeight:600,marginBottom:4,color:'var(--text2)'}}>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']
}</div>
Diese Einstellung wirkt auf:
<ul style={{margin:'6px 0 0 20px',padding:0}}>
<li>Dashboard Charts</li>
<li>Verlauf & Auswertungen</li>
<li>Trainingstyp-Verteilung</li>
<li>KI-Analysen & Pipeline</li>
<li>Alle Statistiken</li>
</ul>
</div>
</div>
{saved && (
<div style={{position:'fixed',bottom:80,left:'50%',transform:'translateX(-50%)',
background:'var(--accent)',color:'white',padding:'8px 20px',borderRadius:20,