Version 9b #1
|
|
@ -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:
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user