feat: Enhance activity API feat: Enhance sleep data import functionality with support for multiple CSV formats and improved data parsing
- Added functions to handle Apple Health sleep data in both segment and summary formats. - Implemented robust error handling for date parsing and data conversion. - Updated documentation to reflect new CSV format support and data aggregation logic. - Bumped version in version.py to reflect the changes in the activity module.
This commit is contained in:
parent
b617212145
commit
97f9aa696e
|
|
@ -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()]
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 (
|
||||
|
|
|
|||
|
|
@ -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([])
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 HEUTE−days (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'}),
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user