fix: use RealDictCursor for PostgreSQL row access
All checks were successful
Deploy Development / deploy (push) Successful in 54s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 12s

All conn.cursor() calls replaced with get_cursor(conn) to enable
dict-like row access (prof['pin_hash'] instead of prof[column_index]).

This fixes KeyError when accessing PostgreSQL query results.

Fixes: 'tuple' object has no attribute '__getitem__' with string keys

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-18 12:42:46 +01:00
parent 124df01983
commit 9fbedb6c4b

View File

@ -14,7 +14,7 @@ from slowapi.util import get_remote_address
from slowapi.errors import RateLimitExceeded from slowapi.errors import RateLimitExceeded
from starlette.requests import Request from starlette.requests import Request
from db import get_db, r2d from db import get_db, get_cursor, r2d
DATA_DIR = Path(os.getenv("DATA_DIR", "./data")) DATA_DIR = Path(os.getenv("DATA_DIR", "./data"))
PHOTOS_DIR = Path(os.getenv("PHOTOS_DIR", "./photos")) PHOTOS_DIR = Path(os.getenv("PHOTOS_DIR", "./photos"))
@ -51,7 +51,7 @@ def get_pid(x_profile_id: Optional[str] = Header(default=None)) -> str:
if x_profile_id: if x_profile_id:
return x_profile_id return x_profile_id
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT id FROM profiles ORDER BY created LIMIT 1") cur.execute("SELECT id FROM profiles ORDER BY created LIMIT 1")
row = cur.fetchone() row = cur.fetchone()
if row: return row['id'] if row: return row['id']
@ -134,7 +134,7 @@ def make_token() -> str:
def get_session(token: str): def get_session(token: str):
if not token: return None if not token: return None
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute( cur.execute(
"SELECT s.*, p.role, p.name, p.ai_enabled, p.ai_limit_day, p.export_enabled " "SELECT s.*, p.role, p.name, p.ai_enabled, p.ai_limit_day, p.export_enabled "
"FROM sessions s JOIN profiles p ON s.profile_id=p.id " "FROM sessions s JOIN profiles p ON s.profile_id=p.id "
@ -157,7 +157,7 @@ def require_admin(x_auth_token: Optional[str]=Header(default=None)):
@app.get("/api/profiles") @app.get("/api/profiles")
def list_profiles(session=Depends(require_auth)): def list_profiles(session=Depends(require_auth)):
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM profiles ORDER BY created") cur.execute("SELECT * FROM profiles ORDER BY created")
rows = cur.fetchall() rows = cur.fetchall()
return [r2d(r) for r in rows] return [r2d(r) for r in rows]
@ -166,19 +166,19 @@ def list_profiles(session=Depends(require_auth)):
def create_profile(p: ProfileCreate, session=Depends(require_auth)): def create_profile(p: ProfileCreate, session=Depends(require_auth)):
pid = str(uuid.uuid4()) pid = str(uuid.uuid4())
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("""INSERT INTO profiles (id,name,avatar_color,sex,dob,height,goal_weight,goal_bf_pct,created,updated) cur.execute("""INSERT INTO profiles (id,name,avatar_color,sex,dob,height,goal_weight,goal_bf_pct,created,updated)
VALUES (%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)""", VALUES (%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)""",
(pid,p.name,p.avatar_color,p.sex,p.dob,p.height,p.goal_weight,p.goal_bf_pct)) (pid,p.name,p.avatar_color,p.sex,p.dob,p.height,p.goal_weight,p.goal_bf_pct))
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,))
return r2d(cur.fetchone()) return r2d(cur.fetchone())
@app.get("/api/profiles/{pid}") @app.get("/api/profiles/{pid}")
def get_profile(pid: str, session=Depends(require_auth)): def get_profile(pid: str, session=Depends(require_auth)):
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,))
row = cur.fetchone() row = cur.fetchone()
if not row: raise HTTPException(404, "Profil nicht gefunden") if not row: raise HTTPException(404, "Profil nicht gefunden")
@ -189,7 +189,7 @@ def update_profile(pid: str, p: ProfileUpdate, session=Depends(require_auth)):
with get_db() as conn: with get_db() as conn:
data = {k:v for k,v in p.model_dump().items() if v is not None} data = {k:v for k,v in p.model_dump().items() if v is not None}
data['updated'] = datetime.now().isoformat() data['updated'] = datetime.now().isoformat()
cur = conn.cursor() cur = get_cursor(conn)
cur.execute(f"UPDATE profiles SET {', '.join(f'{k}=%s' for k in data)} WHERE id=%s", cur.execute(f"UPDATE profiles SET {', '.join(f'{k}=%s' for k in data)} WHERE id=%s",
list(data.values())+[pid]) list(data.values())+[pid])
return get_profile(pid, session) return get_profile(pid, session)
@ -197,7 +197,7 @@ def update_profile(pid: str, p: ProfileUpdate, session=Depends(require_auth)):
@app.delete("/api/profiles/{pid}") @app.delete("/api/profiles/{pid}")
def delete_profile(pid: str, session=Depends(require_auth)): def delete_profile(pid: str, session=Depends(require_auth)):
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT COUNT(*) FROM profiles") cur.execute("SELECT COUNT(*) FROM profiles")
count = cur.fetchone()[0] count = cur.fetchone()[0]
if count <= 1: raise HTTPException(400, "Letztes Profil kann nicht gelöscht werden") if count <= 1: raise HTTPException(400, "Letztes Profil kann nicht gelöscht werden")
@ -222,7 +222,7 @@ def update_active_profile(p: ProfileUpdate, x_profile_id: Optional[str] = Header
def list_weight(limit: int=365, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def list_weight(limit: int=365, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute( cur.execute(
"SELECT * FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit)) "SELECT * FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit))
return [r2d(r) for r in cur.fetchall()] return [r2d(r) for r in cur.fetchall()]
@ -231,7 +231,7 @@ def list_weight(limit: int=365, x_profile_id: Optional[str]=Header(default=None)
def upsert_weight(e: WeightEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def upsert_weight(e: WeightEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT id FROM weight_log WHERE profile_id=%s AND date=%s", (pid,e.date)) cur.execute("SELECT id FROM weight_log WHERE profile_id=%s AND date=%s", (pid,e.date))
ex = cur.fetchone() ex = cur.fetchone()
if ex: if ex:
@ -247,7 +247,7 @@ def upsert_weight(e: WeightEntry, x_profile_id: Optional[str]=Header(default=Non
def update_weight(wid: str, e: WeightEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def update_weight(wid: str, e: WeightEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("UPDATE weight_log SET date=%s,weight=%s,note=%s WHERE id=%s AND profile_id=%s", cur.execute("UPDATE weight_log SET date=%s,weight=%s,note=%s WHERE id=%s AND profile_id=%s",
(e.date,e.weight,e.note,wid,pid)) (e.date,e.weight,e.note,wid,pid))
return {"id":wid} return {"id":wid}
@ -256,7 +256,7 @@ def update_weight(wid: str, e: WeightEntry, x_profile_id: Optional[str]=Header(d
def delete_weight(wid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def delete_weight(wid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("DELETE FROM weight_log WHERE id=%s AND profile_id=%s", (wid,pid)) cur.execute("DELETE FROM weight_log WHERE id=%s AND profile_id=%s", (wid,pid))
return {"ok":True} return {"ok":True}
@ -264,7 +264,7 @@ def delete_weight(wid: str, x_profile_id: Optional[str]=Header(default=None), se
def weight_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def weight_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
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}
@ -278,7 +278,7 @@ def weight_stats(x_profile_id: Optional[str]=Header(default=None), session: dict
def list_circs(limit: int=100, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def list_circs(limit: int=100, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute( cur.execute(
"SELECT * FROM circumference_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit)) "SELECT * FROM circumference_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit))
return [r2d(r) for r in cur.fetchall()] return [r2d(r) for r in cur.fetchall()]
@ -287,7 +287,7 @@ def list_circs(limit: int=100, x_profile_id: Optional[str]=Header(default=None),
def upsert_circ(e: CircumferenceEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def upsert_circ(e: CircumferenceEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT id FROM circumference_log WHERE profile_id=%s AND date=%s", (pid,e.date)) cur.execute("SELECT id FROM circumference_log WHERE profile_id=%s AND date=%s", (pid,e.date))
ex = cur.fetchone() ex = cur.fetchone()
d = e.model_dump() d = e.model_dump()
@ -310,7 +310,7 @@ def update_circ(eid: str, e: CircumferenceEntry, x_profile_id: Optional[str]=Hea
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
d = e.model_dump() d = e.model_dump()
cur = conn.cursor() cur = get_cursor(conn)
cur.execute(f"UPDATE circumference_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s", cur.execute(f"UPDATE circumference_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s",
list(d.values())+[eid,pid]) list(d.values())+[eid,pid])
return {"id":eid} return {"id":eid}
@ -319,7 +319,7 @@ def update_circ(eid: str, e: CircumferenceEntry, x_profile_id: Optional[str]=Hea
def delete_circ(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def delete_circ(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("DELETE FROM circumference_log WHERE id=%s AND profile_id=%s", (eid,pid)) cur.execute("DELETE FROM circumference_log WHERE id=%s AND profile_id=%s", (eid,pid))
return {"ok":True} return {"ok":True}
@ -328,7 +328,7 @@ def delete_circ(eid: str, x_profile_id: Optional[str]=Header(default=None), sess
def list_caliper(limit: int=100, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def list_caliper(limit: int=100, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute( cur.execute(
"SELECT * FROM caliper_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit)) "SELECT * FROM caliper_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit))
return [r2d(r) for r in cur.fetchall()] return [r2d(r) for r in cur.fetchall()]
@ -337,7 +337,7 @@ def list_caliper(limit: int=100, x_profile_id: Optional[str]=Header(default=None
def upsert_caliper(e: CaliperEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def upsert_caliper(e: CaliperEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT id FROM caliper_log WHERE profile_id=%s AND date=%s", (pid,e.date)) cur.execute("SELECT id FROM caliper_log WHERE profile_id=%s AND date=%s", (pid,e.date))
ex = cur.fetchone() ex = cur.fetchone()
d = e.model_dump() d = e.model_dump()
@ -362,7 +362,7 @@ def update_caliper(eid: str, e: CaliperEntry, x_profile_id: Optional[str]=Header
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
d = e.model_dump() d = e.model_dump()
cur = conn.cursor() cur = get_cursor(conn)
cur.execute(f"UPDATE caliper_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s", cur.execute(f"UPDATE caliper_log SET {', '.join(f'{k}=%s' for k in d)} WHERE id=%s AND profile_id=%s",
list(d.values())+[eid,pid]) list(d.values())+[eid,pid])
return {"id":eid} return {"id":eid}
@ -371,7 +371,7 @@ def update_caliper(eid: str, e: CaliperEntry, x_profile_id: Optional[str]=Header
def delete_caliper(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def delete_caliper(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("DELETE FROM caliper_log WHERE id=%s AND profile_id=%s", (eid,pid)) cur.execute("DELETE FROM caliper_log WHERE id=%s AND profile_id=%s", (eid,pid))
return {"ok":True} return {"ok":True}
@ -380,7 +380,7 @@ def delete_caliper(eid: str, x_profile_id: Optional[str]=Header(default=None), s
def list_activity(limit: int=200, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def list_activity(limit: int=200, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute( cur.execute(
"SELECT * FROM activity_log WHERE profile_id=%s ORDER BY date DESC, start_time DESC LIMIT %s", (pid,limit)) "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()] return [r2d(r) for r in cur.fetchall()]
@ -391,7 +391,7 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default
eid = str(uuid.uuid4()) eid = str(uuid.uuid4())
d = e.model_dump() d = e.model_dump()
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("""INSERT INTO activity_log cur.execute("""INSERT INTO activity_log
(id,profile_id,date,start_time,end_time,activity_type,duration_min,kcal_active,kcal_resting, (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) hr_avg,hr_max,distance_km,rpe,source,notes,created)
@ -406,7 +406,7 @@ def update_activity(eid: str, e: ActivityEntry, x_profile_id: Optional[str]=Head
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
d = e.model_dump() d = e.model_dump()
cur = conn.cursor() 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", 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]) list(d.values())+[eid,pid])
return {"id":eid} return {"id":eid}
@ -415,7 +415,7 @@ def update_activity(eid: str, e: ActivityEntry, x_profile_id: Optional[str]=Head
def delete_activity(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def delete_activity(eid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("DELETE FROM activity_log WHERE id=%s AND profile_id=%s", (eid,pid)) cur.execute("DELETE FROM activity_log WHERE id=%s AND profile_id=%s", (eid,pid))
return {"ok":True} return {"ok":True}
@ -423,7 +423,7 @@ def delete_activity(eid: str, x_profile_id: Optional[str]=Header(default=None),
def activity_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def activity_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute( cur.execute(
"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()]
@ -448,7 +448,7 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional
reader = csv.DictReader(io.StringIO(text)) reader = csv.DictReader(io.StringIO(text))
inserted = skipped = 0 inserted = skipped = 0
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
for row in reader: for row in reader:
wtype = row.get('Workout Type','').strip() wtype = row.get('Workout Type','').strip()
start = row.get('Start','').strip() start = row.get('Start','').strip()
@ -492,7 +492,7 @@ async def upload_photo(file: UploadFile=File(...), date: str="",
path = PHOTOS_DIR / f"{fid}{ext}" path = PHOTOS_DIR / f"{fid}{ext}"
async with aiofiles.open(path,'wb') as f: await f.write(await file.read()) async with aiofiles.open(path,'wb') as f: await f.write(await file.read())
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("INSERT INTO photos (id,profile_id,date,path,created) VALUES (%s,%s,%s,%s,CURRENT_TIMESTAMP)", cur.execute("INSERT INTO photos (id,profile_id,date,path,created) VALUES (%s,%s,%s,%s,CURRENT_TIMESTAMP)",
(fid,pid,date,str(path))) (fid,pid,date,str(path)))
return {"id":fid,"date":date} return {"id":fid,"date":date}
@ -500,7 +500,7 @@ async def upload_photo(file: UploadFile=File(...), date: str="",
@app.get("/api/photos/{fid}") @app.get("/api/photos/{fid}")
def get_photo(fid: str, session: dict=Depends(require_auth)): def get_photo(fid: str, session: dict=Depends(require_auth)):
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT path FROM photos WHERE id=%s", (fid,)) cur.execute("SELECT path FROM photos WHERE id=%s", (fid,))
row = cur.fetchone() row = cur.fetchone()
if not row: raise HTTPException(404) if not row: raise HTTPException(404)
@ -510,7 +510,7 @@ def get_photo(fid: str, session: dict=Depends(require_auth)):
def list_photos(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def list_photos(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute( cur.execute(
"SELECT * FROM photos WHERE profile_id=%s ORDER BY created DESC LIMIT 100", (pid,)) "SELECT * FROM photos WHERE profile_id=%s ORDER BY created DESC LIMIT 100", (pid,))
return [r2d(r) for r in cur.fetchall()] return [r2d(r) for r in cur.fetchall()]
@ -546,7 +546,7 @@ async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optiona
count+=1 count+=1
inserted=0 inserted=0
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
for iso,vals in days.items(): for iso,vals in days.items():
kcal=round(vals['kcal'],1); fat=round(vals['fat_g'],1) kcal=round(vals['kcal'],1); fat=round(vals['fat_g'],1)
carbs=round(vals['carbs_g'],1); prot=round(vals['protein_g'],1) carbs=round(vals['carbs_g'],1); prot=round(vals['protein_g'],1)
@ -565,7 +565,7 @@ async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optiona
def list_nutrition(limit: int=365, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def list_nutrition(limit: int=365, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute( cur.execute(
"SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit)) "SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit))
return [r2d(r) for r in cur.fetchall()] return [r2d(r) for r in cur.fetchall()]
@ -574,7 +574,7 @@ def list_nutrition(limit: int=365, x_profile_id: Optional[str]=Header(default=No
def nutrition_correlations(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def nutrition_correlations(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date",(pid,)) cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date",(pid,))
nutr={r['date']:r2d(r) for r in cur.fetchall()} nutr={r['date']:r2d(r) for r in cur.fetchall()}
cur.execute("SELECT date,weight FROM weight_log WHERE profile_id=%s ORDER BY date",(pid,)) cur.execute("SELECT date,weight FROM weight_log WHERE profile_id=%s ORDER BY date",(pid,))
@ -602,7 +602,7 @@ def nutrition_correlations(x_profile_id: Optional[str]=Header(default=None), ses
def nutrition_weekly(weeks: int=16, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def nutrition_weekly(weeks: int=16, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s",(pid,weeks*7)) cur.execute("SELECT * FROM nutrition_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s",(pid,weeks*7))
rows=[r2d(r) for r in cur.fetchall()] rows=[r2d(r) for r in cur.fetchall()]
if not rows: return [] if not rows: return []
@ -622,7 +622,7 @@ def nutrition_weekly(weeks: int=16, x_profile_id: Optional[str]=Header(default=N
def get_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def get_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT COUNT(*) FROM weight_log WHERE profile_id=%s",(pid,)) cur.execute("SELECT COUNT(*) FROM weight_log WHERE profile_id=%s",(pid,))
weight_count = cur.fetchone()[0] weight_count = cur.fetchone()[0]
cur.execute("SELECT COUNT(*) FROM circumference_log WHERE profile_id=%s",(pid,)) cur.execute("SELECT COUNT(*) FROM circumference_log WHERE profile_id=%s",(pid,))
@ -648,7 +648,7 @@ import httpx, json
def get_ai_insight(scope: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def get_ai_insight(scope: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM ai_insights WHERE profile_id=%s AND scope=%s ORDER BY created DESC LIMIT 1", (pid,scope)) cur.execute("SELECT * FROM ai_insights WHERE profile_id=%s AND scope=%s ORDER BY created DESC LIMIT 1", (pid,scope))
row = cur.fetchone() row = cur.fetchone()
if not row: return None if not row: return None
@ -658,14 +658,14 @@ def get_ai_insight(scope: str, x_profile_id: Optional[str]=Header(default=None),
def delete_ai_insight(scope: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): def delete_ai_insight(scope: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("DELETE FROM ai_insights WHERE profile_id=%s AND scope=%s", (pid,scope)) cur.execute("DELETE FROM ai_insights WHERE profile_id=%s AND scope=%s", (pid,scope))
return {"ok":True} return {"ok":True}
def check_ai_limit(pid: str): def check_ai_limit(pid: str):
"""Check if profile has reached daily AI limit. Returns (allowed, limit, used).""" """Check if profile has reached daily AI limit. Returns (allowed, limit, used)."""
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT ai_enabled, ai_limit_day FROM profiles WHERE id=%s", (pid,)) cur.execute("SELECT ai_enabled, ai_limit_day FROM profiles WHERE id=%s", (pid,))
prof = cur.fetchone() prof = cur.fetchone()
if not prof or not prof['ai_enabled']: if not prof or not prof['ai_enabled']:
@ -685,7 +685,7 @@ def inc_ai_usage(pid: str):
"""Increment AI usage counter for today.""" """Increment AI usage counter for today."""
today = datetime.now().date().isoformat() today = datetime.now().date().isoformat()
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT id, call_count FROM ai_usage WHERE profile_id=%s AND date=%s", (pid, today)) cur.execute("SELECT id, call_count FROM ai_usage WHERE profile_id=%s AND date=%s", (pid, today))
row = cur.fetchone() row = cur.fetchone()
if row: if row:
@ -697,7 +697,7 @@ def inc_ai_usage(pid: str):
def _get_profile_data(pid: str): def _get_profile_data(pid: str):
"""Fetch all relevant data for AI analysis.""" """Fetch all relevant data for AI analysis."""
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,))
prof = r2d(cur.fetchone()) prof = r2d(cur.fetchone())
cur.execute("SELECT * FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,)) cur.execute("SELECT * FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,))
@ -831,7 +831,7 @@ async def analyze_with_prompt(slug: str, x_profile_id: Optional[str]=Header(defa
# Get prompt template # Get prompt template
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM ai_prompts WHERE slug=%s AND active=1", (slug,)) cur.execute("SELECT * FROM ai_prompts WHERE slug=%s AND active=1", (slug,))
prompt_row = cur.fetchone() prompt_row = cur.fetchone()
if not prompt_row: if not prompt_row:
@ -872,7 +872,7 @@ async def analyze_with_prompt(slug: str, x_profile_id: Optional[str]=Header(defa
# Save insight # Save insight
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("DELETE FROM ai_insights WHERE profile_id=%s AND scope=%s", (pid, slug)) cur.execute("DELETE FROM ai_insights WHERE profile_id=%s AND scope=%s", (pid, slug))
cur.execute("INSERT INTO ai_insights (id, profile_id, scope, content, created) VALUES (%s,%s,%s,%s,CURRENT_TIMESTAMP)", cur.execute("INSERT INTO ai_insights (id, profile_id, scope, content, created) VALUES (%s,%s,%s,%s,CURRENT_TIMESTAMP)",
(str(uuid.uuid4()), pid, slug, content)) (str(uuid.uuid4()), pid, slug, content))
@ -891,7 +891,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses
# Stage 1: Parallel JSON analyses # Stage 1: Parallel JSON analyses
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT slug, template FROM ai_prompts WHERE slug LIKE 'pipeline_%' AND slug NOT IN ('pipeline_synthesis','pipeline_goals') AND active=1") cur.execute("SELECT slug, template FROM ai_prompts WHERE slug LIKE 'pipeline_%' AND slug NOT IN ('pipeline_synthesis','pipeline_goals') AND active=1")
stage1_prompts = [r2d(r) for r in cur.fetchall()] stage1_prompts = [r2d(r) for r in cur.fetchall()]
@ -936,7 +936,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses
vars['stage1_activity'] = json.dumps(stage1_results.get('pipeline_activity', {}), ensure_ascii=False) vars['stage1_activity'] = json.dumps(stage1_results.get('pipeline_activity', {}), ensure_ascii=False)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT template FROM ai_prompts WHERE slug='pipeline_synthesis' AND active=1") cur.execute("SELECT template FROM ai_prompts WHERE slug='pipeline_synthesis' AND active=1")
synth_row = cur.fetchone() synth_row = cur.fetchone()
if not synth_row: if not synth_row:
@ -973,7 +973,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses
prof = data['profile'] prof = data['profile']
if prof.get('goal_weight') or prof.get('goal_bf_pct'): if prof.get('goal_weight') or prof.get('goal_bf_pct'):
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT template FROM ai_prompts WHERE slug='pipeline_goals' AND active=1") cur.execute("SELECT template FROM ai_prompts WHERE slug='pipeline_goals' AND active=1")
goals_row = cur.fetchone() goals_row = cur.fetchone()
if goals_row: if goals_row:
@ -1008,7 +1008,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses
# Save as 'gesamt' scope # Save as 'gesamt' scope
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("DELETE FROM ai_insights WHERE profile_id=%s AND scope='gesamt'", (pid,)) cur.execute("DELETE FROM ai_insights WHERE profile_id=%s AND scope='gesamt'", (pid,))
cur.execute("INSERT INTO ai_insights (id, profile_id, scope, content, created) VALUES (%s,%s,'gesamt',%s,CURRENT_TIMESTAMP)", cur.execute("INSERT INTO ai_insights (id, profile_id, scope, content, created) VALUES (%s,%s,'gesamt',%s,CURRENT_TIMESTAMP)",
(str(uuid.uuid4()), pid, final_content)) (str(uuid.uuid4()), pid, final_content))
@ -1020,7 +1020,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses
def list_prompts(session: dict=Depends(require_auth)): def list_prompts(session: dict=Depends(require_auth)):
"""List all available AI prompts.""" """List all available AI prompts."""
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM ai_prompts WHERE active=1 AND slug NOT LIKE 'pipeline_%' ORDER BY sort_order") cur.execute("SELECT * FROM ai_prompts WHERE active=1 AND slug NOT LIKE 'pipeline_%' ORDER BY sort_order")
return [r2d(r) for r in cur.fetchall()] return [r2d(r) for r in cur.fetchall()]
@ -1029,7 +1029,7 @@ def get_ai_usage(x_profile_id: Optional[str]=Header(default=None), session: dict
"""Get AI usage stats for current profile.""" """Get AI usage stats for current profile."""
pid = get_pid(x_profile_id) pid = get_pid(x_profile_id)
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT ai_limit_day FROM profiles WHERE id=%s", (pid,)) cur.execute("SELECT ai_limit_day FROM profiles WHERE id=%s", (pid,))
prof = cur.fetchone() prof = cur.fetchone()
limit = prof['ai_limit_day'] if prof else None limit = prof['ai_limit_day'] if prof else None
@ -1066,7 +1066,7 @@ class PasswordResetConfirm(BaseModel):
async def login(req: LoginRequest, request: Request): async def login(req: LoginRequest, request: Request):
"""Login with email + password.""" """Login with email + password."""
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM profiles WHERE email=%s", (req.email.lower().strip(),)) cur.execute("SELECT * FROM profiles WHERE email=%s", (req.email.lower().strip(),))
prof = cur.fetchone() prof = cur.fetchone()
if not prof: if not prof:
@ -1101,7 +1101,7 @@ def logout(x_auth_token: Optional[str]=Header(default=None)):
"""Logout (delete session).""" """Logout (delete session)."""
if x_auth_token: if x_auth_token:
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("DELETE FROM sessions WHERE token=%s", (x_auth_token,)) cur.execute("DELETE FROM sessions WHERE token=%s", (x_auth_token,))
return {"ok": True} return {"ok": True}
@ -1117,7 +1117,7 @@ async def password_reset_request(req: PasswordResetRequest, request: Request):
"""Request password reset email.""" """Request password reset email."""
email = req.email.lower().strip() email = req.email.lower().strip()
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT id, name FROM profiles WHERE email=%s", (email,)) cur.execute("SELECT id, name FROM profiles WHERE email=%s", (email,))
prof = cur.fetchone() prof = cur.fetchone()
if not prof: if not prof:
@ -1173,7 +1173,7 @@ Dein Mitai Jinkendo Team
def password_reset_confirm(req: PasswordResetConfirm): def password_reset_confirm(req: PasswordResetConfirm):
"""Confirm password reset with token.""" """Confirm password reset with token."""
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT profile_id FROM sessions WHERE token=%s AND expires_at > CURRENT_TIMESTAMP", cur.execute("SELECT profile_id FROM sessions WHERE token=%s AND expires_at > CURRENT_TIMESTAMP",
(f"reset_{req.token}",)) (f"reset_{req.token}",))
sess = cur.fetchone() sess = cur.fetchone()
@ -1198,7 +1198,7 @@ class AdminProfileUpdate(BaseModel):
def admin_list_profiles(session: dict=Depends(require_admin)): def admin_list_profiles(session: dict=Depends(require_admin)):
"""Admin: List all profiles with stats.""" """Admin: List all profiles with stats."""
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT * FROM profiles ORDER BY created") cur.execute("SELECT * FROM profiles ORDER BY created")
profs = [r2d(r) for r in cur.fetchall()] profs = [r2d(r) for r in cur.fetchall()]
@ -1224,7 +1224,7 @@ def admin_update_profile(pid: str, data: AdminProfileUpdate, session: dict=Depen
if not updates: if not updates:
return {"ok": True} return {"ok": True}
cur = conn.cursor() cur = get_cursor(conn)
cur.execute(f"UPDATE profiles SET {', '.join(f'{k}=%s' for k in updates)} WHERE id=%s", cur.execute(f"UPDATE profiles SET {', '.join(f'{k}=%s' for k in updates)} WHERE id=%s",
list(updates.values()) + [pid]) list(updates.values()) + [pid])
@ -1267,7 +1267,7 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D
# Check export permission # Check export permission
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() cur = get_cursor(conn)
cur.execute("SELECT export_enabled FROM profiles WHERE id=%s", (pid,)) cur.execute("SELECT export_enabled FROM profiles WHERE id=%s", (pid,))
prof = cur.fetchone() prof = cur.fetchone()
if not prof or not prof['export_enabled']: if not prof or not prof['export_enabled']:
@ -1282,7 +1282,7 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D
# Weight # Weight
with get_db() as conn: with get_db() as conn:
cur = conn.cursor() 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"{r['weight']}kg", r['note'] or ""])