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
|
import logging
|
||||||
from typing import Optional
|
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 db import get_db, get_cursor, r2d
|
||||||
from auth import require_auth, check_feature_access, increment_feature_usage
|
from auth import require_auth, check_feature_access, increment_feature_usage
|
||||||
|
|
@ -32,24 +32,45 @@ logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
@router.get("")
|
||||||
def list_activity(limit: int=200, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
|
def list_activity(
|
||||||
"""Get activity entries for current profile."""
|
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)
|
pid = get_pid(x_profile_id)
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(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,))
|
cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,))
|
||||||
profile = r2d(cur.fetchone())
|
profile = r2d(cur.fetchone())
|
||||||
quality_filter = get_quality_filter_sql(profile)
|
quality_filter = get_quality_filter_sql(profile)
|
||||||
|
|
||||||
cur.execute(f"""
|
if days is not None:
|
||||||
SELECT * FROM activity_log
|
cur.execute(
|
||||||
WHERE profile_id=%s
|
f"""
|
||||||
{quality_filter}
|
SELECT * FROM activity_log
|
||||||
ORDER BY date DESC, start_time DESC
|
WHERE profile_id=%s
|
||||||
LIMIT %s
|
{quality_filter}
|
||||||
""", (pid, limit))
|
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()]
|
return [r2d(r) for r in cur.fetchall()]
|
||||||
|
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ MODULE_VERSIONS = {
|
||||||
"weight": "1.0.3",
|
"weight": "1.0.3",
|
||||||
"circumference": "1.0.1",
|
"circumference": "1.0.1",
|
||||||
"caliper": "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",
|
"nutrition": "1.0.2",
|
||||||
"photos": "1.0.0",
|
"photos": "1.0.0",
|
||||||
"insights": "1.3.0",
|
"insights": "1.3.0",
|
||||||
|
|
|
||||||
|
|
@ -15,8 +15,10 @@ export default function TrainingTypeDistribution({ days = 28 }) {
|
||||||
const [loading, setLoading] = useState(true)
|
const [loading, setLoading] = useState(true)
|
||||||
|
|
||||||
useEffect(() => {
|
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([
|
Promise.all([
|
||||||
api.listActivity(days),
|
api.listActivity(limit, safeDays),
|
||||||
api.getTrainingCategories()
|
api.getTrainingCategories()
|
||||||
]).then(([activities, cats]) => {
|
]).then(([activities, cats]) => {
|
||||||
setCategories(cats)
|
setCategories(cats)
|
||||||
|
|
@ -43,7 +45,7 @@ export default function TrainingTypeDistribution({ days = 28 }) {
|
||||||
console.error('Failed to load training type distribution:', err)
|
console.error('Failed to load training type distribution:', err)
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
}, [days, activeProfile?.quality_filter_level]) // Issue #31: Reload when quality filter changes
|
}, [days, activeProfile?.quality_filter_level, activeProfile?.id])
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
|
|
|
||||||
|
|
@ -21,8 +21,8 @@ export default function PilotActivitySection({ refreshTick = 0, chartDays = BODY
|
||||||
let cancelled = false
|
let cancelled = false
|
||||||
;(async () => {
|
;(async () => {
|
||||||
try {
|
try {
|
||||||
const fetchDays = Math.max(120, periodDays + 60)
|
const limit = Math.min(50_000, Math.max(200, periodDays * 25))
|
||||||
const a = await api.listActivity(fetchDays)
|
const a = await api.listActivity(limit, periodDays)
|
||||||
if (!cancelled) setActivities(Array.isArray(a) ? a : [])
|
if (!cancelled) setActivities(Array.isArray(a) ? a : [])
|
||||||
} catch {
|
} catch {
|
||||||
if (!cancelled) setActivities([])
|
if (!cancelled) setActivities([])
|
||||||
|
|
|
||||||
|
|
@ -276,7 +276,7 @@ export default function Dashboard() {
|
||||||
api.listCaliper(3),
|
api.listCaliper(3),
|
||||||
api.listCirc(2),
|
api.listCirc(2),
|
||||||
api.listNutrition(30),
|
api.listNutrition(30),
|
||||||
api.listActivity(30),
|
api.listActivity(800, 30),
|
||||||
api.latestInsights(),
|
api.latestInsights(),
|
||||||
]).then(([s,w,ca,ci,n,a,ins])=>{
|
]).then(([s,w,ca,ci,n,a,ins])=>{
|
||||||
setStats(s); setWeights(w); setCalipers(ca); setCircs(ci)
|
setStats(s); setWeights(w); setCalipers(ca); setCircs(ci)
|
||||||
|
|
|
||||||
|
|
@ -972,7 +972,7 @@ export default function History() {
|
||||||
|
|
||||||
const loadAll = () => Promise.all([
|
const loadAll = () => Promise.all([
|
||||||
api.listWeight(365), api.listCaliper(), api.listCirc(),
|
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.nutritionCorrelations(), api.latestInsights(), api.getProfile(),
|
||||||
api.listPrompts(),
|
api.listPrompts(),
|
||||||
]).then(([w,ca,ci,n,a,corr,ins,p,pr])=>{
|
]).then(([w,ca,ci,n,a,corr,ins,p,pr])=>{
|
||||||
|
|
@ -983,7 +983,9 @@ export default function History() {
|
||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
|
|
||||||
useEffect(()=>{ loadAll() },[])
|
useEffect(() => {
|
||||||
|
loadAll()
|
||||||
|
}, [activeProfile?.quality_filter_level])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const t = location.state?.tab
|
const t = location.state?.tab
|
||||||
|
|
|
||||||
|
|
@ -118,7 +118,12 @@ export const api = {
|
||||||
deleteCaliper: (id) => req(`/caliper/${id}`,{method:'DELETE'}),
|
deleteCaliper: (id) => req(`/caliper/${id}`,{method:'DELETE'}),
|
||||||
|
|
||||||
// Activity
|
// 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)),
|
createActivity: (d) => req('/activity',json(d)),
|
||||||
updateActivity: (id,d) => req(`/activity/${id}`,jput(d)),
|
updateActivity: (id,d) => req(`/activity/${id}`,jput(d)),
|
||||||
deleteActivity: (id) => req(`/activity/${id}`,{method:'DELETE'}),
|
deleteActivity: (id) => req(`/activity/${id}`,{method:'DELETE'}),
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user