PHASE 2: Backend Non-Blocking Logging - KOMPLETT
Instrumentierte Endpoints (12):
- Data: weight, circumference, caliper, nutrition, activity, photos (6)
- AI: insights/run/{slug}, insights/pipeline (2)
- Export: csv, json, zip (3)
- Import: zip (1)
Pattern implementiert:
- check_feature_access() VOR Operation (non-blocking)
- [FEATURE-LIMIT] Logging wenn Limit überschritten
- increment_feature_usage() NACH Operation
- Alte Permission-Checks bleiben aktiv
Features geprüft:
- weight_entries, circumference_entries, caliper_entries
- nutrition_entries, activity_entries, photos
- ai_calls, ai_pipeline
- data_export, data_import
Monitoring: 1-2 Wochen Log-Only-Phase
Logs zeigen: Wie oft würde blockiert werden?
Nächste Phase: Frontend Display (Usage-Counter)
Phase 1 (Cleanup) + Phase 2 (Logging) vollständig!
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
102 lines
4.0 KiB
Python
102 lines
4.0 KiB
Python
"""
|
|
Weight Tracking Endpoints for Mitai Jinkendo
|
|
|
|
Handles weight log CRUD operations and statistics.
|
|
"""
|
|
import uuid
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Header, Depends
|
|
|
|
from db import get_db, get_cursor, r2d
|
|
from auth import require_auth, check_feature_access, increment_feature_usage
|
|
from models import WeightEntry
|
|
from routers.profiles import get_pid
|
|
|
|
router = APIRouter(prefix="/api/weight", tags=["weight"])
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
@router.get("")
|
|
def list_weight(limit: int=365, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
|
|
"""Get weight entries for current profile."""
|
|
pid = get_pid(x_profile_id)
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
cur.execute(
|
|
"SELECT * FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT %s", (pid,limit))
|
|
return [r2d(r) for r in cur.fetchall()]
|
|
|
|
|
|
@router.post("")
|
|
def upsert_weight(e: WeightEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
|
|
"""Create or update weight entry (upsert by date)."""
|
|
pid = get_pid(x_profile_id)
|
|
|
|
# Phase 2: Check feature access (non-blocking, log only)
|
|
access = check_feature_access(pid, 'weight_entries')
|
|
if not access['allowed']:
|
|
logger.warning(
|
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
|
f"weight_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
|
)
|
|
# NOTE: Phase 2 does NOT block - just logs!
|
|
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
cur.execute("SELECT id FROM weight_log WHERE profile_id=%s AND date=%s", (pid,e.date))
|
|
ex = cur.fetchone()
|
|
is_new_entry = not ex
|
|
|
|
if ex:
|
|
# UPDATE existing entry
|
|
cur.execute("UPDATE weight_log SET weight=%s,note=%s WHERE id=%s", (e.weight,e.note,ex['id']))
|
|
wid = ex['id']
|
|
else:
|
|
# INSERT new entry
|
|
wid = str(uuid.uuid4())
|
|
cur.execute("INSERT INTO weight_log (id,profile_id,date,weight,note,created) VALUES (%s,%s,%s,%s,%s,CURRENT_TIMESTAMP)",
|
|
(wid,pid,e.date,e.weight,e.note))
|
|
|
|
# Phase 2: Increment usage counter (only for new entries)
|
|
increment_feature_usage(pid, 'weight_entries')
|
|
|
|
return {"id":wid,"date":e.date,"weight":e.weight}
|
|
|
|
|
|
@router.put("/{wid}")
|
|
def update_weight(wid: str, e: WeightEntry, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
|
|
"""Update existing weight entry."""
|
|
pid = get_pid(x_profile_id)
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
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))
|
|
return {"id":wid}
|
|
|
|
|
|
@router.delete("/{wid}")
|
|
def delete_weight(wid: str, x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
|
|
"""Delete weight entry."""
|
|
pid = get_pid(x_profile_id)
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
cur.execute("DELETE FROM weight_log WHERE id=%s AND profile_id=%s", (wid,pid))
|
|
return {"ok":True}
|
|
|
|
|
|
@router.get("/stats")
|
|
def weight_stats(x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)):
|
|
"""Get weight statistics (last 90 days)."""
|
|
pid = get_pid(x_profile_id)
|
|
with get_db() as conn:
|
|
cur = get_cursor(conn)
|
|
cur.execute("SELECT date,weight FROM weight_log WHERE profile_id=%s ORDER BY date DESC LIMIT 90", (pid,))
|
|
rows = cur.fetchall()
|
|
if not rows: return {"count":0,"latest":None,"prev":None,"min":None,"max":None,"avg_7d":None}
|
|
w=[float(r['weight']) for r in rows]
|
|
return {"count":len(rows),"latest":{"date":rows[0]['date'],"weight":float(rows[0]['weight'])},
|
|
"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)}
|