""" Activity Tracking Endpoints for Mitai Jinkendo Handles workout/activity logging, statistics, and Apple Health CSV import. """ import csv import io import uuid from typing import Optional from fastapi import APIRouter, HTTPException, UploadFile, File, Header, Depends from db import get_db, get_cursor, r2d from auth import require_auth from models import ActivityEntry from routers.profiles import get_pid router = APIRouter(prefix="/api/activity", tags=["activity"]) @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.""" 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)) return [r2d(r) for r in cur.fetchall()] @router.post("") def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Create new activity entry.""" pid = get_pid(x_profile_id) eid = str(uuid.uuid4()) d = e.model_dump() with get_db() as conn: cur = get_cursor(conn) cur.execute("""INSERT INTO activity_log (id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting, hr_avg,hr_max,distance_km,rpe,source,notes,created) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)""", (eid,pid,d['date'],d['start_time'],d['end_time'],d['activity_type'],d['duration_min'], d['kcal_active'],d['kcal_resting'],d['hr_avg'],d['hr_max'],d['distance_km'], d['rpe'],d['source'],d['notes'])) return {"id":eid,"date":e.date} @router.put("/{eid}") def update_activity(eid: str, e: ActivityEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Update existing activity entry.""" pid = get_pid(x_profile_id) with get_db() as conn: d = e.model_dump() cur = get_cursor(conn) cur.execute(f"UPDATE activity_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s", list(d.values())+[eid,pid]) return {"id":eid} @router.delete("/{eid}") def delete_activity(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Delete activity entry.""" pid = get_pid(x_profile_id) with get_db() as conn: cur = get_cursor(conn) cur.execute("DELETE FROM activity_log WHERE id=%s AND profile_id=%s", (eid,pid)) return {"ok":True} @router.get("/stats") def activity_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Get activity statistics (last 30 entries).""" 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 LIMIT 30", (pid,)) rows = [r2d(r) for r in cur.fetchall()] if not rows: return {"count":0,"total_kcal":0,"total_min":0,"by_type":{}} total_kcal=sum(float(r.get('kcal_active') or 0) for r in rows) total_min=sum(float(r.get('duration_min') or 0) for r in rows) by_type={} for r in rows: t=r['activity_type']; by_type.setdefault(t,{'count':0,'kcal':0,'min':0}) by_type[t]['count']+=1 by_type[t]['kcal']+=float(r.get('kcal_active') or 0) by_type[t]['min']+=float(r.get('duration_min') or 0) return {"count":len(rows),"total_kcal":round(total_kcal),"total_min":round(total_min),"by_type":by_type} @router.post("/import-csv") async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Import Apple Health workout CSV.""" pid = get_pid(x_profile_id) raw = await file.read() try: text = raw.decode('utf-8') except: text = raw.decode('latin-1') if text.startswith('\ufeff'): text = text[1:] if not text.strip(): raise HTTPException(400,"Leere Datei") reader = csv.DictReader(io.StringIO(text)) inserted = skipped = 0 with get_db() as conn: cur = get_cursor(conn) for row in reader: wtype = row.get('Workout Type','').strip() start = row.get('Start','').strip() if not wtype or not start: continue try: date = start[:10] except: continue dur = row.get('Duration','').strip() duration_min = None if dur: try: p = dur.split(':') duration_min = round(int(p[0])*60+int(p[1])+int(p[2])/60,1) except: pass def kj(v): try: return round(float(v)/4.184) if v else None except: return None def tf(v): try: return round(float(v),1) if v else None except: return None try: cur.execute("""INSERT INTO activity_log (id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting, hr_avg,hr_max,distance_km,source,created) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,'apple_health',CURRENT_TIMESTAMP)""", (str(uuid.uuid4()),pid,date,start,row.get('End',''),wtype,duration_min, kj(row.get('Aktive Energie (kJ)','')),kj(row.get('Ruheeinträge (kJ)','')), tf(row.get('Durchschn. Herzfrequenz (count/min)','')), tf(row.get('Max. Herzfrequenz (count/min)','')), tf(row.get('Distanz (km)','')))) inserted+=1 except: skipped+=1 return {"inserted":inserted,"skipped":skipped,"message":f"{inserted} Trainings importiert"}