Indvidual Dashboard V0.9 #67

Merged
Lars merged 27 commits from develop into main 2026-04-08 10:56:02 +02:00
7 changed files with 50 additions and 20 deletions
Showing only changes of commit 97f9aa696e - Show all commits

View File

@ -9,7 +9,7 @@ import uuid
import logging
from typing import Optional
from fastapi import APIRouter, HTTPException, UploadFile, File, Header, Depends
from fastapi import APIRouter, HTTPException, UploadFile, File, Header, Depends, Query
from db import get_db, get_cursor, r2d
from auth import require_auth, check_feature_access, increment_feature_usage
@ -32,24 +32,45 @@ logger = logging.getLogger(__name__)
@router.get("")
def list_activity(limit: int=200, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
"""Get activity entries for current profile."""
def list_activity(
limit: int = Query(200, ge=1, le=50_000),
days: Optional[int] = Query(None, ge=1, le=4000, description="Nur Einträge mit date >= HEUTE days (Kalendertage)"),
x_profile_id: Optional[str] = Header(default=None),
session: dict = Depends(require_auth),
):
"""Get activity entries for current profile. Optional *days* filter by calendar window (not the same as *limit*)."""
pid = get_pid(x_profile_id)
with get_db() as conn:
cur = get_cursor(conn)
# Issue #31: Apply global quality filter
# Issue #31: Apply global quality filter (profile from DB = saved level)
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))
if days is not None:
cur.execute(
f"""
SELECT * FROM activity_log
WHERE profile_id=%s
{quality_filter}
AND date >= (CURRENT_DATE - %s::integer)
ORDER BY date DESC, start_time DESC
LIMIT %s
""",
(pid, days, limit),
)
else:
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

@ -19,7 +19,7 @@ MODULE_VERSIONS = {
"weight": "1.0.3",
"circumference": "1.0.1",
"caliper": "1.0.1",
"activity": "1.1.0",
"activity": "1.2.0", # GET /activity: optional days= window + limit
"nutrition": "1.0.2",
"photos": "1.0.0",
"insights": "1.3.0",

View File

@ -15,8 +15,10 @@ export default function TrainingTypeDistribution({ days = 28 }) {
const [loading, setLoading] = useState(true)
useEffect(() => {
const safeDays = Math.max(1, Math.min(4000, Number(days) || 28))
const limit = Math.min(50_000, Math.max(250, safeDays * 25))
Promise.all([
api.listActivity(days),
api.listActivity(limit, safeDays),
api.getTrainingCategories()
]).then(([activities, cats]) => {
setCategories(cats)
@ -43,7 +45,7 @@ export default function TrainingTypeDistribution({ days = 28 }) {
console.error('Failed to load training type distribution:', err)
setLoading(false)
})
}, [days, activeProfile?.quality_filter_level]) // Issue #31: Reload when quality filter changes
}, [days, activeProfile?.quality_filter_level, activeProfile?.id])
if (loading) {
return (

View File

@ -21,8 +21,8 @@ export default function PilotActivitySection({ refreshTick = 0, chartDays = BODY
let cancelled = false
;(async () => {
try {
const fetchDays = Math.max(120, periodDays + 60)
const a = await api.listActivity(fetchDays)
const limit = Math.min(50_000, Math.max(200, periodDays * 25))
const a = await api.listActivity(limit, periodDays)
if (!cancelled) setActivities(Array.isArray(a) ? a : [])
} catch {
if (!cancelled) setActivities([])

View File

@ -276,7 +276,7 @@ export default function Dashboard() {
api.listCaliper(3),
api.listCirc(2),
api.listNutrition(30),
api.listActivity(30),
api.listActivity(800, 30),
api.latestInsights(),
]).then(([s,w,ca,ci,n,a,ins])=>{
setStats(s); setWeights(w); setCalipers(ca); setCircs(ci)

View File

@ -972,7 +972,7 @@ export default function History() {
const loadAll = () => Promise.all([
api.listWeight(365), api.listCaliper(), api.listCirc(),
api.listNutrition(90), api.listActivity(200),
api.listNutrition(90), api.listActivity(25_000),
api.nutritionCorrelations(), api.latestInsights(), api.getProfile(),
api.listPrompts(),
]).then(([w,ca,ci,n,a,corr,ins,p,pr])=>{
@ -983,7 +983,9 @@ export default function History() {
setLoading(false)
})
useEffect(()=>{ loadAll() },[])
useEffect(() => {
loadAll()
}, [activeProfile?.quality_filter_level])
useEffect(() => {
const t = location.state?.tab

View File

@ -118,7 +118,12 @@ export const api = {
deleteCaliper: (id) => req(`/caliper/${id}`,{method:'DELETE'}),
// Activity
listActivity: (l=200)=> req(`/activity?limit=${l}`),
/** @param {number} [limit=200] @param {number} [days] nur Einträge ab HEUTEdays (Kalendertage), backend-filtert */
listActivity: (limit=200, days)=> {
const q = new URLSearchParams({ limit: String(limit) })
if (days != null && days !== '') q.set('days', String(days))
return req(`/activity?${q}`)
},
createActivity: (d) => req('/activity',json(d)),
updateActivity: (id,d) => req(`/activity/${id}`,jput(d)),
deleteActivity: (id) => req(`/activity/${id}`,{method:'DELETE'}),