Version 9b #1

Merged
Lars merged 34 commits from develop into main 2026-03-19 08:04:02 +01:00
Showing only changes of commit 47a268f426 - Show all commits

View File

@ -2,6 +2,7 @@ import os, csv, io, uuid, json, zipfile
from pathlib import Path from pathlib import Path
from typing import Optional from typing import Optional
from datetime import datetime from datetime import datetime
from decimal import Decimal
from fastapi import FastAPI, HTTPException, UploadFile, File, Header, Query, Depends from fastapi import FastAPI, HTTPException, UploadFile, File, Header, Query, Depends
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
@ -268,9 +269,9 @@ def weight_stats(x_profile_id: Optional[str]=Header(default=None), session: dict
cur.execute("SELECT date,weight FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) cur.execute("SELECT date,weight FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,))
rows = cur.fetchall() rows = cur.fetchall()
if not rows: return {"count":0,"latest":None,"prev":None,"min":None,"max":None,"avg_7d":None} if not rows: return {"count":0,"latest":None,"prev":None,"min":None,"max":None,"avg_7d":None}
w=[r['weight'] for r in rows] w=[float(r['weight']) for r in rows]
return {"count":len(rows),"latest":{"date":rows[0]['date'],"weight":rows[0]['weight']}, return {"count":len(rows),"latest":{"date":rows[0]['date'],"weight":float(rows[0]['weight'])},
"prev":{"date":rows[1]['date'],"weight":rows[1]['weight']} if len(rows)>1 else None, "prev":{"date":rows[1]['date'],"weight":float(rows[1]['weight'])} if len(rows)>1 else None,
"min":min(w),"max":max(w),"avg_7d":round(sum(w[:7])/min(7,len(w)),2)} "min":min(w),"max":max(w),"avg_7d":round(sum(w[:7])/min(7,len(w)),2)}
# ── Circumferences ──────────────────────────────────────────────────────────── # ── Circumferences ────────────────────────────────────────────────────────────
@ -428,13 +429,14 @@ def activity_stats(x_profile_id: Optional[str]=Header(default=None), session: di
"SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date DESC LIMIT 30", (pid,)) "SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date DESC LIMIT 30", (pid,))
rows = [r2d(r) for r in cur.fetchall()] rows = [r2d(r) for r in cur.fetchall()]
if not rows: return {"count":0,"total_kcal":0,"total_min":0,"by_type":{}} if not rows: return {"count":0,"total_kcal":0,"total_min":0,"by_type":{}}
total_kcal=sum(r.get('kcal_active') or 0 for r in rows) total_kcal=sum(float(r.get('kcal_active') or 0) for r in rows)
total_min=sum(r.get('duration_min') or 0 for r in rows) total_min=sum(float(r.get('duration_min') or 0) for r in rows)
by_type={} by_type={}
for r in rows: for r in rows:
t=r['activity_type']; by_type.setdefault(t,{'count':0,'kcal':0,'min':0}) t=r['activity_type']; by_type.setdefault(t,{'count':0,'kcal':0,'min':0})
by_type[t]['count']+=1; by_type[t]['kcal']+=r.get('kcal_active') or 0 by_type[t]['count']+=1
by_type[t]['min']+=r.get('duration_min') or 0 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} return {"count":len(rows),"total_kcal":round(total_kcal),"total_min":round(total_min),"by_type":by_type}
@app.post("/api/activity/import-csv") @app.post("/api/activity/import-csv")
@ -590,11 +592,13 @@ def nutrition_correlations(x_profile_id: Optional[str]=Header(default=None), ses
for d in all_dates: for d in all_dates:
if d not in nutr and d not in wlog: continue if d not in nutr and d not in wlog: continue
row={'date':d} row={'date':d}
if d in nutr: row.update({k:nutr[d][k] for k in ['kcal','protein_g','fat_g','carbs_g']}) if d in nutr: row.update({k:float(nutr[d][k]) if nutr[d][k] is not None else None for k in ['kcal','protein_g','fat_g','carbs_g']})
if d in wlog: row['weight']=wlog[d] if d in wlog: row['weight']=float(wlog[d])
if d in cal_by_date: if d in cal_by_date:
row['lean_mass']=cal_by_date[d].get('lean_mass') lm = cal_by_date[d].get('lean_mass')
row['body_fat_pct']=cal_by_date[d].get('body_fat_pct') bf = cal_by_date[d].get('body_fat_pct')
row['lean_mass']=float(lm) if lm is not None else None
row['body_fat_pct']=float(bf) if bf is not None else None
result.append(row) result.append(row)
return result return result
@ -613,7 +617,7 @@ def nutrition_weekly(weeks: int=16, x_profile_id: Optional[str]=Header(default=N
result=[] result=[]
for wk in sorted(wm): for wk in sorted(wm):
en=wm[wk]; n=len(en) en=wm[wk]; n=len(en)
def avg(k): return round(sum(e.get(k) or 0 for e in en)/n,1) def avg(k): return round(sum(float(e.get(k) or 0) for e in en)/n,1)
result.append({'week':wk,'days':n,'kcal':avg('kcal'),'protein_g':avg('protein_g'),'fat_g':avg('fat_g'),'carbs_g':avg('carbs_g')}) result.append({'week':wk,'days':n,'kcal':avg('kcal'),'protein_g':avg('protein_g'),'fat_g':avg('fat_g'),'carbs_g':avg('carbs_g')})
return result return result
@ -768,10 +772,10 @@ def _prepare_template_vars(data: dict) -> dict:
"name": prof.get('name', 'Nutzer'), "name": prof.get('name', 'Nutzer'),
"geschlecht": "männlich" if prof.get('sex') == 'm' else "weiblich", "geschlecht": "männlich" if prof.get('sex') == 'm' else "weiblich",
"height": prof.get('height', 178), "height": prof.get('height', 178),
"goal_weight": prof.get('goal_weight') or "nicht gesetzt", "goal_weight": float(prof.get('goal_weight')) if prof.get('goal_weight') else "nicht gesetzt",
"goal_bf_pct": prof.get('goal_bf_pct') or "nicht gesetzt", "goal_bf_pct": float(prof.get('goal_bf_pct')) if prof.get('goal_bf_pct') else "nicht gesetzt",
"weight_aktuell": weight[0]['weight'] if weight else "keine Daten", "weight_aktuell": float(weight[0]['weight']) if weight else "keine Daten",
"kf_aktuell": caliper[0]['body_fat_pct'] if caliper and caliper[0].get('body_fat_pct') else "unbekannt", "kf_aktuell": float(caliper[0]['body_fat_pct']) if caliper and caliper[0].get('body_fat_pct') else "unbekannt",
} }
# Calculate age from dob # Calculate age from dob
@ -798,7 +802,8 @@ def _prepare_template_vars(data: dict) -> dict:
# Caliper summary # Caliper summary
if caliper: if caliper:
c = caliper[0] c = caliper[0]
vars['caliper_summary'] = f"KF: {c.get('body_fat_pct','?')}%, Methode: {c.get('sf_method','?')}" bf = float(c.get('body_fat_pct')) if c.get('body_fat_pct') else '?'
vars['caliper_summary'] = f"KF: {bf}%, Methode: {c.get('sf_method','?')}"
else: else:
vars['caliper_summary'] = "keine Daten" vars['caliper_summary'] = "keine Daten"
@ -807,7 +812,7 @@ def _prepare_template_vars(data: dict) -> dict:
c = circ[0] c = circ[0]
parts = [] parts = []
for k in ['c_waist', 'c_belly', 'c_hip']: for k in ['c_waist', 'c_belly', 'c_hip']:
if c.get(k): parts.append(f"{k.split('_')[1]}: {c[k]}cm") if c.get(k): parts.append(f"{k.split('_')[1]}: {float(c[k])}cm")
vars['circ_summary'] = ", ".join(parts) if parts else "keine Daten" vars['circ_summary'] = ", ".join(parts) if parts else "keine Daten"
else: else:
vars['circ_summary'] = "keine Daten" vars['circ_summary'] = "keine Daten"
@ -1431,28 +1436,28 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D
cur = get_cursor(conn) cur = get_cursor(conn)
cur.execute("SELECT date, weight, note FROM weight_log WHERE profile_id=%s ORDER BY date", (pid,)) cur.execute("SELECT date, weight, note FROM weight_log WHERE profile_id=%s ORDER BY date", (pid,))
for r in cur.fetchall(): for r in cur.fetchall():
writer.writerow(["Gewicht", r['date'], f"{r['weight']}kg", r['note'] or ""]) writer.writerow(["Gewicht", r['date'], f"{float(r['weight'])}kg", r['note'] or ""])
# Circumferences # Circumferences
cur.execute("SELECT date, c_waist, c_belly, c_hip FROM circumference_log WHERE profile_id=%s ORDER BY date", (pid,)) cur.execute("SELECT date, c_waist, c_belly, c_hip FROM circumference_log WHERE profile_id=%s ORDER BY date", (pid,))
for r in cur.fetchall(): for r in cur.fetchall():
details = f"Taille:{r['c_waist']}cm Bauch:{r['c_belly']}cm Hüfte:{r['c_hip']}cm" details = f"Taille:{float(r['c_waist'])}cm Bauch:{float(r['c_belly'])}cm Hüfte:{float(r['c_hip'])}cm"
writer.writerow(["Umfänge", r['date'], "", details]) writer.writerow(["Umfänge", r['date'], "", details])
# Caliper # Caliper
cur.execute("SELECT date, body_fat_pct, lean_mass FROM caliper_log WHERE profile_id=%s ORDER BY date", (pid,)) cur.execute("SELECT date, body_fat_pct, lean_mass FROM caliper_log WHERE profile_id=%s ORDER BY date", (pid,))
for r in cur.fetchall(): for r in cur.fetchall():
writer.writerow(["Caliper", r['date'], f"{r['body_fat_pct']}%", f"Magermasse:{r['lean_mass']}kg"]) writer.writerow(["Caliper", r['date'], f"{float(r['body_fat_pct'])}%", f"Magermasse:{float(r['lean_mass'])}kg"])
# Nutrition # Nutrition
cur.execute("SELECT date, kcal, protein_g FROM nutrition_log WHERE profile_id=%s ORDER BY date", (pid,)) cur.execute("SELECT date, kcal, protein_g FROM nutrition_log WHERE profile_id=%s ORDER BY date", (pid,))
for r in cur.fetchall(): for r in cur.fetchall():
writer.writerow(["Ernährung", r['date'], f"{r['kcal']}kcal", f"Protein:{r['protein_g']}g"]) writer.writerow(["Ernährung", r['date'], f"{float(r['kcal'])}kcal", f"Protein:{float(r['protein_g'])}g"])
# Activity # Activity
cur.execute("SELECT date, activity_type, duration_min, kcal_active FROM activity_log WHERE profile_id=%s ORDER BY date", (pid,)) cur.execute("SELECT date, activity_type, duration_min, kcal_active FROM activity_log WHERE profile_id=%s ORDER BY date", (pid,))
for r in cur.fetchall(): for r in cur.fetchall():
writer.writerow(["Training", r['date'], r['activity_type'], f"{r['duration_min']}min {r['kcal_active']}kcal"]) writer.writerow(["Training", r['date'], r['activity_type'], f"{float(r['duration_min'])}min {float(r['kcal_active'])}kcal"])
output.seek(0) output.seek(0)
return StreamingResponse( return StreamingResponse(
@ -1500,7 +1505,12 @@ def export_json(x_profile_id: Optional[str]=Header(default=None), session: dict=
cur.execute("SELECT * FROM ai_insights WHERE profile_id=%s ORDER BY created DESC", (pid,)) cur.execute("SELECT * FROM ai_insights WHERE profile_id=%s ORDER BY created DESC", (pid,))
data['insights'] = [r2d(r) for r in cur.fetchall()] data['insights'] = [r2d(r) for r in cur.fetchall()]
json_str = json.dumps(data, indent=2, default=str) def decimal_handler(obj):
if isinstance(obj, Decimal):
return float(obj)
return str(obj)
json_str = json.dumps(data, indent=2, default=decimal_handler)
return Response( return Response(
content=json_str, content=json_str,
media_type="application/json", media_type="application/json",
@ -1549,7 +1559,12 @@ def export_zip(x_profile_id: Optional[str]=Header(default=None), session: dict=D
cur.execute("SELECT * FROM ai_insights WHERE profile_id=%s ORDER BY created DESC", (pid,)) cur.execute("SELECT * FROM ai_insights WHERE profile_id=%s ORDER BY created DESC", (pid,))
data['insights'] = [r2d(r) for r in cur.fetchall()] data['insights'] = [r2d(r) for r in cur.fetchall()]
zf.writestr("data.json", json.dumps(data, indent=2, default=str)) def decimal_handler(obj):
if isinstance(obj, Decimal):
return float(obj)
return str(obj)
zf.writestr("data.json", json.dumps(data, indent=2, default=decimal_handler))
# Add photos if they exist # Add photos if they exist
with get_db() as conn: with get_db() as conn: