2026-03-19 11:15:35 +01:00
"""
AI Insights Endpoints for Mitai Jinkendo
Handles AI analysis execution , prompt management , and usage tracking .
"""
import os
import json
import uuid
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
import logging
2026-03-19 11:15:35 +01:00
import httpx
from typing import Optional
from datetime import datetime
from fastapi import APIRouter , HTTPException , Header , Depends
from db import get_db , get_cursor , r2d
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
from auth import require_auth , require_admin , check_feature_access , increment_feature_usage
2026-03-19 11:15:35 +01:00
from routers . profiles import get_pid
2026-03-20 22:18:12 +01:00
from feature_logger import log_feature_usage
2026-03-19 11:15:35 +01:00
router = APIRouter ( prefix = " /api " , tags = [ " insights " ] )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
logger = logging . getLogger ( __name__ )
2026-03-19 11:15:35 +01:00
OPENROUTER_KEY = os . getenv ( " OPENROUTER_API_KEY " , " " )
OPENROUTER_MODEL = os . getenv ( " OPENROUTER_MODEL " , " anthropic/claude-sonnet-4 " )
ANTHROPIC_KEY = os . getenv ( " ANTHROPIC_API_KEY " , " " )
# ── Helper Functions ──────────────────────────────────────────────────────────
def check_ai_limit ( pid : str ) :
""" Check if profile has reached daily AI limit. Returns (allowed, limit, used). """
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " SELECT ai_enabled, ai_limit_day FROM profiles WHERE id= %s " , ( pid , ) )
prof = cur . fetchone ( )
if not prof or not prof [ ' ai_enabled ' ] :
raise HTTPException ( 403 , " KI ist für dieses Profil deaktiviert " )
limit = prof [ ' ai_limit_day ' ]
if limit is None :
return ( True , None , 0 )
today = datetime . now ( ) . date ( ) . isoformat ( )
cur . execute ( " SELECT call_count FROM ai_usage WHERE profile_id= %s AND date= %s " , ( pid , today ) )
usage = cur . fetchone ( )
used = usage [ ' call_count ' ] if usage else 0
if used > = limit :
raise HTTPException ( 429 , f " Tägliches KI-Limit erreicht ( { limit } Calls) " )
return ( True , limit , used )
def inc_ai_usage ( pid : str ) :
""" Increment AI usage counter for today. """
today = datetime . now ( ) . date ( ) . isoformat ( )
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " SELECT id, call_count FROM ai_usage WHERE profile_id= %s AND date= %s " , ( pid , today ) )
row = cur . fetchone ( )
if row :
cur . execute ( " UPDATE ai_usage SET call_count= %s WHERE id= %s " , ( row [ ' call_count ' ] + 1 , row [ ' id ' ] ) )
else :
cur . execute ( " INSERT INTO ai_usage (id, profile_id, date, call_count) VALUES ( %s , %s , %s ,1) " ,
( str ( uuid . uuid4 ( ) ) , pid , today ) )
def _get_profile_data ( pid : str ) :
""" Fetch all relevant data for AI analysis. """
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " SELECT * FROM profiles WHERE id= %s " , ( pid , ) )
prof = r2d ( cur . fetchone ( ) )
cur . execute ( " SELECT * FROM weight_log WHERE profile_id= %s ORDER BY date DESC LIMIT 90 " , ( pid , ) )
weight = [ r2d ( r ) for r in cur . fetchall ( ) ]
cur . execute ( " SELECT * FROM circumference_log WHERE profile_id= %s ORDER BY date DESC LIMIT 30 " , ( pid , ) )
circ = [ r2d ( r ) for r in cur . fetchall ( ) ]
cur . execute ( " SELECT * FROM caliper_log WHERE profile_id= %s ORDER BY date DESC LIMIT 30 " , ( pid , ) )
caliper = [ r2d ( r ) for r in cur . fetchall ( ) ]
cur . execute ( " SELECT * FROM nutrition_log WHERE profile_id= %s ORDER BY date DESC LIMIT 90 " , ( pid , ) )
nutrition = [ r2d ( r ) for r in cur . fetchall ( ) ]
cur . execute ( " SELECT * FROM activity_log WHERE profile_id= %s ORDER BY date DESC LIMIT 90 " , ( pid , ) )
activity = [ r2d ( r ) for r in cur . fetchall ( ) ]
feat: add AI evaluation placeholders for v9d Phase 2 modules
**Backend (insights.py):**
- Extended _get_profile_data() to fetch sleep, rest_days, vitals
- Added template variables for Sleep Module:
{{sleep_summary}}, {{sleep_detail}}, {{sleep_avg_duration}}, {{sleep_avg_quality}}
- Added template variables for Rest Days:
{{rest_days_summary}}, {{rest_days_count}}, {{rest_days_types}}
- Added template variables for Vitals:
{{vitals_summary}}, {{vitals_detail}}, {{vitals_avg_hr}}, {{vitals_avg_hrv}},
{{vitals_avg_bp}}, {{vitals_vo2_max}}
**Frontend (Analysis.jsx):**
- Added 12 new template variables to VARS list in PromptEditor
- Enables AI prompt creation for Sleep, Rest Days, and Vitals analysis
All modules now have AI evaluation support for future prompt creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 15:30:17 +01:00
# v9d Phase 2: Sleep, Rest Days, Vitals
cur . execute ( " SELECT * FROM sleep_log WHERE profile_id= %s ORDER BY date DESC LIMIT 30 " , ( pid , ) )
sleep = [ r2d ( r ) for r in cur . fetchall ( ) ]
cur . execute ( " SELECT * FROM rest_days WHERE profile_id= %s ORDER BY date DESC LIMIT 30 " , ( pid , ) )
rest_days = [ r2d ( r ) for r in cur . fetchall ( ) ]
refactor: vitals architecture - separate baseline vs blood pressure
BREAKING CHANGE: vitals_log split into vitals_baseline + blood_pressure_log
**Architektur-Änderung:**
- Baseline-Vitals (langsam veränderlich, 1x täglich morgens)
→ vitals_baseline (RHR, HRV, VO2 Max, SpO2, Atemfrequenz)
- Kontext-abhängige Vitals (mehrfach täglich, situativ)
→ blood_pressure_log (Blutdruck + Kontext-Tagging)
**Migration 015:**
- CREATE TABLE vitals_baseline (once daily, morning measurements)
- CREATE TABLE blood_pressure_log (multiple daily, context-aware)
- Migrate data from vitals_log → new tables
- Rename vitals_log → vitals_log_backup_pre_015 (safety)
- Prepared for future: glucose_log, temperature_log (commented)
**Backend:**
- NEW: routers/vitals_baseline.py (CRUD + Apple Health import)
- NEW: routers/blood_pressure.py (CRUD + Omron import + context)
- UPDATED: main.py (register new routers, remove old vitals)
- UPDATED: insights.py (query new tables, split template vars)
**Frontend:**
- UPDATED: api.js (new endpoints für baseline + BP)
- UPDATED: Analysis.jsx (add {{bp_summary}} variable)
**Nächster Schritt:**
- Frontend: VitalsPage.jsx refactoren (3 Tabs: Morgenmessung, Blutdruck, Import)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:02:40 +01:00
# v9d Phase 2d Refactored: separate baseline and BP tables
cur . execute ( " SELECT * FROM vitals_baseline WHERE profile_id= %s ORDER BY date DESC LIMIT 30 " , ( pid , ) )
vitals_baseline = [ r2d ( r ) for r in cur . fetchall ( ) ]
cur . execute ( " SELECT * FROM blood_pressure_log WHERE profile_id= %s ORDER BY measured_at DESC LIMIT 90 " , ( pid , ) )
blood_pressure = [ r2d ( r ) for r in cur . fetchall ( ) ]
2026-03-19 11:15:35 +01:00
return {
" profile " : prof ,
" weight " : weight ,
" circumference " : circ ,
" caliper " : caliper ,
" nutrition " : nutrition ,
feat: add AI evaluation placeholders for v9d Phase 2 modules
**Backend (insights.py):**
- Extended _get_profile_data() to fetch sleep, rest_days, vitals
- Added template variables for Sleep Module:
{{sleep_summary}}, {{sleep_detail}}, {{sleep_avg_duration}}, {{sleep_avg_quality}}
- Added template variables for Rest Days:
{{rest_days_summary}}, {{rest_days_count}}, {{rest_days_types}}
- Added template variables for Vitals:
{{vitals_summary}}, {{vitals_detail}}, {{vitals_avg_hr}}, {{vitals_avg_hrv}},
{{vitals_avg_bp}}, {{vitals_vo2_max}}
**Frontend (Analysis.jsx):**
- Added 12 new template variables to VARS list in PromptEditor
- Enables AI prompt creation for Sleep, Rest Days, and Vitals analysis
All modules now have AI evaluation support for future prompt creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 15:30:17 +01:00
" activity " : activity ,
" sleep " : sleep ,
" rest_days " : rest_days ,
refactor: vitals architecture - separate baseline vs blood pressure
BREAKING CHANGE: vitals_log split into vitals_baseline + blood_pressure_log
**Architektur-Änderung:**
- Baseline-Vitals (langsam veränderlich, 1x täglich morgens)
→ vitals_baseline (RHR, HRV, VO2 Max, SpO2, Atemfrequenz)
- Kontext-abhängige Vitals (mehrfach täglich, situativ)
→ blood_pressure_log (Blutdruck + Kontext-Tagging)
**Migration 015:**
- CREATE TABLE vitals_baseline (once daily, morning measurements)
- CREATE TABLE blood_pressure_log (multiple daily, context-aware)
- Migrate data from vitals_log → new tables
- Rename vitals_log → vitals_log_backup_pre_015 (safety)
- Prepared for future: glucose_log, temperature_log (commented)
**Backend:**
- NEW: routers/vitals_baseline.py (CRUD + Apple Health import)
- NEW: routers/blood_pressure.py (CRUD + Omron import + context)
- UPDATED: main.py (register new routers, remove old vitals)
- UPDATED: insights.py (query new tables, split template vars)
**Frontend:**
- UPDATED: api.js (new endpoints für baseline + BP)
- UPDATED: Analysis.jsx (add {{bp_summary}} variable)
**Nächster Schritt:**
- Frontend: VitalsPage.jsx refactoren (3 Tabs: Morgenmessung, Blutdruck, Import)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:02:40 +01:00
" vitals_baseline " : vitals_baseline ,
" blood_pressure " : blood_pressure
2026-03-19 11:15:35 +01:00
}
def _render_template ( template : str , data : dict ) - > str :
""" Simple template variable replacement. """
result = template
for k , v in data . items ( ) :
result = result . replace ( f " {{ {{ { k } }} }} " , str ( v ) if v is not None else " " )
return result
def _prepare_template_vars ( data : dict ) - > dict :
""" Prepare template variables from profile data. """
prof = data [ ' profile ' ]
weight = data [ ' weight ' ]
circ = data [ ' circumference ' ]
caliper = data [ ' caliper ' ]
nutrition = data [ ' nutrition ' ]
activity = data [ ' activity ' ]
feat: add AI evaluation placeholders for v9d Phase 2 modules
**Backend (insights.py):**
- Extended _get_profile_data() to fetch sleep, rest_days, vitals
- Added template variables for Sleep Module:
{{sleep_summary}}, {{sleep_detail}}, {{sleep_avg_duration}}, {{sleep_avg_quality}}
- Added template variables for Rest Days:
{{rest_days_summary}}, {{rest_days_count}}, {{rest_days_types}}
- Added template variables for Vitals:
{{vitals_summary}}, {{vitals_detail}}, {{vitals_avg_hr}}, {{vitals_avg_hrv}},
{{vitals_avg_bp}}, {{vitals_vo2_max}}
**Frontend (Analysis.jsx):**
- Added 12 new template variables to VARS list in PromptEditor
- Enables AI prompt creation for Sleep, Rest Days, and Vitals analysis
All modules now have AI evaluation support for future prompt creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 15:30:17 +01:00
sleep = data . get ( ' sleep ' , [ ] )
rest_days = data . get ( ' rest_days ' , [ ] )
refactor: vitals architecture - separate baseline vs blood pressure
BREAKING CHANGE: vitals_log split into vitals_baseline + blood_pressure_log
**Architektur-Änderung:**
- Baseline-Vitals (langsam veränderlich, 1x täglich morgens)
→ vitals_baseline (RHR, HRV, VO2 Max, SpO2, Atemfrequenz)
- Kontext-abhängige Vitals (mehrfach täglich, situativ)
→ blood_pressure_log (Blutdruck + Kontext-Tagging)
**Migration 015:**
- CREATE TABLE vitals_baseline (once daily, morning measurements)
- CREATE TABLE blood_pressure_log (multiple daily, context-aware)
- Migrate data from vitals_log → new tables
- Rename vitals_log → vitals_log_backup_pre_015 (safety)
- Prepared for future: glucose_log, temperature_log (commented)
**Backend:**
- NEW: routers/vitals_baseline.py (CRUD + Apple Health import)
- NEW: routers/blood_pressure.py (CRUD + Omron import + context)
- UPDATED: main.py (register new routers, remove old vitals)
- UPDATED: insights.py (query new tables, split template vars)
**Frontend:**
- UPDATED: api.js (new endpoints für baseline + BP)
- UPDATED: Analysis.jsx (add {{bp_summary}} variable)
**Nächster Schritt:**
- Frontend: VitalsPage.jsx refactoren (3 Tabs: Morgenmessung, Blutdruck, Import)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:02:40 +01:00
vitals_baseline = data . get ( ' vitals_baseline ' , [ ] )
blood_pressure = data . get ( ' blood_pressure ' , [ ] )
2026-03-19 11:15:35 +01:00
vars = {
" name " : prof . get ( ' name ' , ' Nutzer ' ) ,
" geschlecht " : " männlich " if prof . get ( ' sex ' ) == ' m ' else " weiblich " ,
" height " : prof . get ( ' height ' , 178 ) ,
" goal_weight " : float ( prof . get ( ' goal_weight ' ) ) if prof . get ( ' goal_weight ' ) else " nicht gesetzt " ,
" goal_bf_pct " : float ( prof . get ( ' goal_bf_pct ' ) ) if prof . get ( ' goal_bf_pct ' ) else " nicht gesetzt " ,
" weight_aktuell " : float ( weight [ 0 ] [ ' weight ' ] ) if weight else " keine Daten " ,
" kf_aktuell " : float ( caliper [ 0 ] [ ' body_fat_pct ' ] ) if caliper and caliper [ 0 ] . get ( ' body_fat_pct ' ) else " unbekannt " ,
}
# Calculate age from dob
if prof . get ( ' dob ' ) :
try :
from datetime import date
dob = datetime . strptime ( prof [ ' dob ' ] , ' % Y- % m- %d ' ) . date ( )
today = date . today ( )
age = today . year - dob . year - ( ( today . month , today . day ) < ( dob . month , dob . day ) )
vars [ ' age ' ] = age
except :
vars [ ' age ' ] = " unbekannt "
else :
vars [ ' age ' ] = " unbekannt "
# Weight trend summary
if len ( weight ) > = 2 :
recent = weight [ : 30 ]
delta = float ( recent [ 0 ] [ ' weight ' ] ) - float ( recent [ - 1 ] [ ' weight ' ] )
vars [ ' weight_trend ' ] = f " { len ( recent ) } Einträge, Δ30d: { delta : +.1f } kg "
else :
vars [ ' weight_trend ' ] = " zu wenig Daten "
# Caliper summary
if caliper :
c = caliper [ 0 ]
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 :
vars [ ' caliper_summary ' ] = " keine Daten "
# Circumference summary
if circ :
c = circ [ 0 ]
parts = [ ]
for k in [ ' c_waist ' , ' c_belly ' , ' c_hip ' ] :
if c . get ( k ) : parts . append ( f " { k . split ( ' _ ' ) [ 1 ] } : { float ( c [ k ] ) } cm " )
vars [ ' circ_summary ' ] = " , " . join ( parts ) if parts else " keine Daten "
else :
vars [ ' circ_summary ' ] = " keine Daten "
# Nutrition summary
if nutrition :
n = len ( nutrition )
avg_kcal = sum ( float ( d . get ( ' kcal ' , 0 ) or 0 ) for d in nutrition ) / n
avg_prot = sum ( float ( d . get ( ' protein_g ' , 0 ) or 0 ) for d in nutrition ) / n
vars [ ' nutrition_summary ' ] = f " { n } Tage, Ø { avg_kcal : .0f } kcal, { avg_prot : .0f } g Protein "
vars [ ' nutrition_detail ' ] = vars [ ' nutrition_summary ' ]
vars [ ' nutrition_days ' ] = n
vars [ ' kcal_avg ' ] = round ( avg_kcal )
vars [ ' protein_avg ' ] = round ( avg_prot , 1 )
vars [ ' fat_avg ' ] = round ( sum ( float ( d . get ( ' fat_g ' , 0 ) or 0 ) for d in nutrition ) / n , 1 )
vars [ ' carb_avg ' ] = round ( sum ( float ( d . get ( ' carbs_g ' , 0 ) or 0 ) for d in nutrition ) / n , 1 )
else :
vars [ ' nutrition_summary ' ] = " keine Daten "
vars [ ' nutrition_detail ' ] = " keine Daten "
vars [ ' nutrition_days ' ] = 0
vars [ ' kcal_avg ' ] = 0
vars [ ' protein_avg ' ] = 0
vars [ ' fat_avg ' ] = 0
vars [ ' carb_avg ' ] = 0
# Protein targets
w = weight [ 0 ] [ ' weight ' ] if weight else prof . get ( ' height ' , 178 ) - 100
w = float ( w ) # Convert Decimal to float for math operations
vars [ ' protein_ziel_low ' ] = round ( w * 1.6 )
vars [ ' protein_ziel_high ' ] = round ( w * 2.2 )
# Activity summary
if activity :
n = len ( activity )
total_kcal = sum ( float ( a . get ( ' kcal_active ' , 0 ) or 0 ) for a in activity )
vars [ ' activity_summary ' ] = f " { n } Trainings, { total_kcal : .0f } kcal gesamt "
vars [ ' activity_detail ' ] = vars [ ' activity_summary ' ]
vars [ ' activity_kcal_summary ' ] = f " Ø { total_kcal / n : .0f } kcal/Training "
else :
vars [ ' activity_summary ' ] = " keine Daten "
vars [ ' activity_detail ' ] = " keine Daten "
vars [ ' activity_kcal_summary ' ] = " keine Daten "
feat: add AI evaluation placeholders for v9d Phase 2 modules
**Backend (insights.py):**
- Extended _get_profile_data() to fetch sleep, rest_days, vitals
- Added template variables for Sleep Module:
{{sleep_summary}}, {{sleep_detail}}, {{sleep_avg_duration}}, {{sleep_avg_quality}}
- Added template variables for Rest Days:
{{rest_days_summary}}, {{rest_days_count}}, {{rest_days_types}}
- Added template variables for Vitals:
{{vitals_summary}}, {{vitals_detail}}, {{vitals_avg_hr}}, {{vitals_avg_hrv}},
{{vitals_avg_bp}}, {{vitals_vo2_max}}
**Frontend (Analysis.jsx):**
- Added 12 new template variables to VARS list in PromptEditor
- Enables AI prompt creation for Sleep, Rest Days, and Vitals analysis
All modules now have AI evaluation support for future prompt creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 15:30:17 +01:00
# Sleep summary (v9d Phase 2b)
if sleep :
n = len ( sleep )
avg_duration = sum ( float ( s . get ( ' duration_minutes ' , 0 ) or 0 ) for s in sleep ) / n
avg_quality = sum ( int ( s . get ( ' quality ' , 0 ) or 0 ) for s in sleep if s . get ( ' quality ' ) ) / max ( sum ( 1 for s in sleep if s . get ( ' quality ' ) ) , 1 )
deep_data = [ s for s in sleep if s . get ( ' deep_minutes ' ) ]
avg_deep = sum ( float ( s . get ( ' deep_minutes ' , 0 ) ) for s in deep_data ) / len ( deep_data ) if deep_data else 0
vars [ ' sleep_summary ' ] = f " { n } Nächte, Ø { avg_duration / 60 : .1f } h Schlafdauer, Qualität { avg_quality : .1f } /5 "
vars [ ' sleep_detail ' ] = f " Ø { avg_duration : .0f } min gesamt, { avg_deep : .0f } min Tiefschlaf "
vars [ ' sleep_avg_duration ' ] = round ( avg_duration )
vars [ ' sleep_avg_quality ' ] = round ( avg_quality , 1 )
vars [ ' sleep_nights ' ] = n
else :
vars [ ' sleep_summary ' ] = " keine Daten "
vars [ ' sleep_detail ' ] = " keine Daten "
vars [ ' sleep_avg_duration ' ] = 0
vars [ ' sleep_avg_quality ' ] = 0
vars [ ' sleep_nights ' ] = 0
# Rest Days summary (v9d Phase 2a)
if rest_days :
n = len ( rest_days )
types = { }
for rd in rest_days :
rt = rd . get ( ' rest_type ' , ' unknown ' )
types [ rt ] = types . get ( rt , 0 ) + 1
type_summary = " , " . join ( [ f " { k } : { v } x " for k , v in types . items ( ) ] )
vars [ ' rest_days_summary ' ] = f " { n } Ruhetage (letzte 30d): { type_summary } "
vars [ ' rest_days_count ' ] = n
vars [ ' rest_days_types ' ] = type_summary
else :
vars [ ' rest_days_summary ' ] = " keine Daten "
vars [ ' rest_days_count ' ] = 0
vars [ ' rest_days_types ' ] = " keine "
refactor: vitals architecture - separate baseline vs blood pressure
BREAKING CHANGE: vitals_log split into vitals_baseline + blood_pressure_log
**Architektur-Änderung:**
- Baseline-Vitals (langsam veränderlich, 1x täglich morgens)
→ vitals_baseline (RHR, HRV, VO2 Max, SpO2, Atemfrequenz)
- Kontext-abhängige Vitals (mehrfach täglich, situativ)
→ blood_pressure_log (Blutdruck + Kontext-Tagging)
**Migration 015:**
- CREATE TABLE vitals_baseline (once daily, morning measurements)
- CREATE TABLE blood_pressure_log (multiple daily, context-aware)
- Migrate data from vitals_log → new tables
- Rename vitals_log → vitals_log_backup_pre_015 (safety)
- Prepared for future: glucose_log, temperature_log (commented)
**Backend:**
- NEW: routers/vitals_baseline.py (CRUD + Apple Health import)
- NEW: routers/blood_pressure.py (CRUD + Omron import + context)
- UPDATED: main.py (register new routers, remove old vitals)
- UPDATED: insights.py (query new tables, split template vars)
**Frontend:**
- UPDATED: api.js (new endpoints für baseline + BP)
- UPDATED: Analysis.jsx (add {{bp_summary}} variable)
**Nächster Schritt:**
- Frontend: VitalsPage.jsx refactoren (3 Tabs: Morgenmessung, Blutdruck, Import)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:02:40 +01:00
# Vitals Baseline summary (v9d Phase 2d Refactored)
if vitals_baseline :
n = len ( vitals_baseline )
hr_data = [ v for v in vitals_baseline if v . get ( ' resting_hr ' ) ]
hrv_data = [ v for v in vitals_baseline if v . get ( ' hrv ' ) ]
vo2_data = [ v for v in vitals_baseline if v . get ( ' vo2_max ' ) ]
feat: add AI evaluation placeholders for v9d Phase 2 modules
**Backend (insights.py):**
- Extended _get_profile_data() to fetch sleep, rest_days, vitals
- Added template variables for Sleep Module:
{{sleep_summary}}, {{sleep_detail}}, {{sleep_avg_duration}}, {{sleep_avg_quality}}
- Added template variables for Rest Days:
{{rest_days_summary}}, {{rest_days_count}}, {{rest_days_types}}
- Added template variables for Vitals:
{{vitals_summary}}, {{vitals_detail}}, {{vitals_avg_hr}}, {{vitals_avg_hrv}},
{{vitals_avg_bp}}, {{vitals_vo2_max}}
**Frontend (Analysis.jsx):**
- Added 12 new template variables to VARS list in PromptEditor
- Enables AI prompt creation for Sleep, Rest Days, and Vitals analysis
All modules now have AI evaluation support for future prompt creation.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 15:30:17 +01:00
avg_hr = sum ( int ( v . get ( ' resting_hr ' ) ) for v in hr_data ) / len ( hr_data ) if hr_data else 0
avg_hrv = sum ( int ( v . get ( ' hrv ' ) ) for v in hrv_data ) / len ( hrv_data ) if hrv_data else 0
latest_vo2 = float ( vo2_data [ 0 ] . get ( ' vo2_max ' ) ) if vo2_data else 0
parts = [ ]
if avg_hr : parts . append ( f " Ruhepuls Ø { avg_hr : .0f } bpm " )
if avg_hrv : parts . append ( f " HRV Ø { avg_hrv : .0f } ms " )
if latest_vo2 : parts . append ( f " VO2 Max { latest_vo2 : .1f } " )
vars [ ' vitals_summary ' ] = f " { n } Messungen: " + " , " . join ( parts ) if parts else " keine verwertbaren Daten "
vars [ ' vitals_detail ' ] = vars [ ' vitals_summary ' ]
vars [ ' vitals_avg_hr ' ] = round ( avg_hr )
vars [ ' vitals_avg_hrv ' ] = round ( avg_hrv )
vars [ ' vitals_vo2_max ' ] = round ( latest_vo2 , 1 ) if latest_vo2 else " k.A. "
else :
vars [ ' vitals_summary ' ] = " keine Daten "
vars [ ' vitals_detail ' ] = " keine Daten "
vars [ ' vitals_avg_hr ' ] = 0
vars [ ' vitals_avg_hrv ' ] = 0
vars [ ' vitals_vo2_max ' ] = " k.A. "
refactor: vitals architecture - separate baseline vs blood pressure
BREAKING CHANGE: vitals_log split into vitals_baseline + blood_pressure_log
**Architektur-Änderung:**
- Baseline-Vitals (langsam veränderlich, 1x täglich morgens)
→ vitals_baseline (RHR, HRV, VO2 Max, SpO2, Atemfrequenz)
- Kontext-abhängige Vitals (mehrfach täglich, situativ)
→ blood_pressure_log (Blutdruck + Kontext-Tagging)
**Migration 015:**
- CREATE TABLE vitals_baseline (once daily, morning measurements)
- CREATE TABLE blood_pressure_log (multiple daily, context-aware)
- Migrate data from vitals_log → new tables
- Rename vitals_log → vitals_log_backup_pre_015 (safety)
- Prepared for future: glucose_log, temperature_log (commented)
**Backend:**
- NEW: routers/vitals_baseline.py (CRUD + Apple Health import)
- NEW: routers/blood_pressure.py (CRUD + Omron import + context)
- UPDATED: main.py (register new routers, remove old vitals)
- UPDATED: insights.py (query new tables, split template vars)
**Frontend:**
- UPDATED: api.js (new endpoints für baseline + BP)
- UPDATED: Analysis.jsx (add {{bp_summary}} variable)
**Nächster Schritt:**
- Frontend: VitalsPage.jsx refactoren (3 Tabs: Morgenmessung, Blutdruck, Import)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:02:40 +01:00
# Blood Pressure summary (v9d Phase 2d Refactored)
if blood_pressure :
n = len ( blood_pressure )
bp_data = [ bp for bp in blood_pressure if bp . get ( ' systolic ' ) and bp . get ( ' diastolic ' ) ]
avg_bp_sys = sum ( int ( bp . get ( ' systolic ' ) ) for bp in bp_data ) / len ( bp_data ) if bp_data else 0
avg_bp_dia = sum ( int ( bp . get ( ' diastolic ' ) ) for bp in bp_data ) / len ( bp_data ) if bp_data else 0
vars [ ' vitals_avg_bp ' ] = f " { round ( avg_bp_sys ) } / { round ( avg_bp_dia ) } " if avg_bp_sys else " k.A. "
vars [ ' bp_summary ' ] = f " { n } Messungen, Ø { avg_bp_sys : .0f } / { avg_bp_dia : .0f } mmHg " if avg_bp_sys else " keine Daten "
else :
vars [ ' vitals_avg_bp ' ] = " k.A. "
vars [ ' bp_summary ' ] = " keine Daten "
2026-03-19 11:15:35 +01:00
return vars
# ── Endpoints ─────────────────────────────────────────────────────────────────
@router.get ( " /insights " )
def get_all_insights ( x_profile_id : Optional [ str ] = Header ( default = None ) , session : dict = Depends ( require_auth ) ) :
""" Get all AI insights for profile. """
pid = get_pid ( x_profile_id )
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " SELECT * FROM ai_insights WHERE profile_id= %s ORDER BY created DESC " , ( pid , ) )
rows = cur . fetchall ( )
return [ r2d ( r ) for r in rows ]
@router.get ( " /insights/latest " )
def get_latest_insights ( x_profile_id : Optional [ str ] = Header ( default = None ) , session : dict = Depends ( require_auth ) ) :
""" Get latest AI insights across all scopes. """
pid = get_pid ( x_profile_id )
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " SELECT * FROM ai_insights WHERE profile_id= %s ORDER BY created DESC LIMIT 10 " , ( pid , ) )
rows = cur . fetchall ( )
return [ r2d ( r ) for r in rows ]
@router.get ( " /ai/insights/ {scope} " )
def get_ai_insight ( scope : str , x_profile_id : Optional [ str ] = Header ( default = None ) , session : dict = Depends ( require_auth ) ) :
""" Get latest insight for specific scope. """
pid = get_pid ( x_profile_id )
with get_db ( ) as conn :
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 ) )
row = cur . fetchone ( )
if not row : return None
return r2d ( row )
@router.delete ( " /insights/ {insight_id} " )
def delete_insight_by_id ( insight_id : str , x_profile_id : Optional [ str ] = Header ( default = None ) , session : dict = Depends ( require_auth ) ) :
""" Delete a specific insight by ID. """
pid = get_pid ( x_profile_id )
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " DELETE FROM ai_insights WHERE id= %s AND profile_id= %s " , ( insight_id , pid ) )
return { " ok " : True }
@router.delete ( " /ai/insights/ {scope} " )
def delete_ai_insight ( scope : str , x_profile_id : Optional [ str ] = Header ( default = None ) , session : dict = Depends ( require_auth ) ) :
""" Delete all insights for specific scope. """
pid = get_pid ( x_profile_id )
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " DELETE FROM ai_insights WHERE profile_id= %s AND scope= %s " , ( pid , scope ) )
return { " ok " : True }
@router.post ( " /insights/run/ {slug} " )
async def analyze_with_prompt ( slug : str , x_profile_id : Optional [ str ] = Header ( default = None ) , session : dict = Depends ( require_auth ) ) :
""" Run AI analysis with specified prompt template. """
pid = get_pid ( x_profile_id )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
2026-03-21 07:40:37 +01:00
# Phase 4: Check feature access and ENFORCE
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
access = check_feature_access ( pid , ' ai_calls ' )
2026-03-20 22:18:12 +01:00
log_feature_usage ( pid , ' ai_calls ' , access , ' analyze ' )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
if not access [ ' allowed ' ] :
logger . warning (
2026-03-21 07:40:37 +01:00
f " [FEATURE-LIMIT] User { pid } blocked: "
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
f " ai_calls { access [ ' reason ' ] } (used: { access [ ' used ' ] } , limit: { access [ ' limit ' ] } ) "
)
2026-03-21 07:40:37 +01:00
raise HTTPException (
status_code = 403 ,
detail = f " Limit erreicht: Du hast das Kontingent für KI-Analysen überschritten ( { access [ ' used ' ] } / { access [ ' limit ' ] } ). "
f " Bitte kontaktiere den Admin oder warte bis zum nächsten Reset. "
)
2026-03-19 11:15:35 +01:00
# Get prompt template
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " SELECT * FROM ai_prompts WHERE slug= %s AND active=true " , ( slug , ) )
prompt_row = cur . fetchone ( )
if not prompt_row :
raise HTTPException ( 404 , f " Prompt ' { slug } ' nicht gefunden " )
prompt_tmpl = prompt_row [ ' template ' ]
data = _get_profile_data ( pid )
vars = _prepare_template_vars ( data )
final_prompt = _render_template ( prompt_tmpl , vars )
# Call AI
if ANTHROPIC_KEY :
# Use Anthropic SDK
import anthropic
client = anthropic . Anthropic ( api_key = ANTHROPIC_KEY )
response = client . messages . create (
model = " claude-sonnet-4-20250514 " ,
max_tokens = 2000 ,
messages = [ { " role " : " user " , " content " : final_prompt } ]
)
content = response . content [ 0 ] . text
elif OPENROUTER_KEY :
async with httpx . AsyncClient ( ) as client :
resp = await client . post ( " https://openrouter.ai/api/v1/chat/completions " ,
headers = { " Authorization " : f " Bearer { OPENROUTER_KEY } " } ,
json = {
" model " : OPENROUTER_MODEL ,
" messages " : [ { " role " : " user " , " content " : final_prompt } ] ,
" max_tokens " : 2000
} ,
timeout = 60.0
)
if resp . status_code != 200 :
raise HTTPException ( 500 , f " KI-Fehler: { resp . text } " )
content = resp . json ( ) [ ' choices ' ] [ 0 ] [ ' message ' ] [ ' content ' ]
else :
raise HTTPException ( 500 , " Keine KI-API konfiguriert " )
2026-03-20 15:35:33 +01:00
# Save insight (with history - no DELETE)
2026-03-19 11:15:35 +01:00
with get_db ( ) as conn :
cur = get_cursor ( conn )
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 ) )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
# Phase 2: Increment new feature usage counter
increment_feature_usage ( pid , ' ai_calls ' )
# Old usage tracking (keep for now)
ROLLBACK: complete removal of broken feature enforcement system
Reverts all feature enforcement changes (commits 3745ebd, cbad50a, cd4d912, 8415509)
to restore original working functionality.
Issues caused by feature enforcement implementation:
- Export buttons disappeared and never reappeared
- KI analysis counter not incrementing
- New analyses not saving
- Pipeline appearing twice
- Many core features broken
Restored files to working state before enforcement implementation (commit 0210844):
- Backend: auth.py, insights.py, exportdata.py, importdata.py, nutrition.py, activity.py
- Frontend: Analysis.jsx, SettingsPage.jsx, api.js
- Removed: FeatureGate.jsx, useFeatureAccess.js
The original simple AI limit system (ai_enabled, ai_limit_day) is now active again.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:19:56 +01:00
inc_ai_usage ( pid )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
2026-03-19 11:15:35 +01:00
return { " scope " : slug , " content " : content }
@router.post ( " /insights/pipeline " )
async def analyze_pipeline ( x_profile_id : Optional [ str ] = Header ( default = None ) , session : dict = Depends ( require_auth ) ) :
""" Run 3-stage pipeline analysis. """
pid = get_pid ( x_profile_id )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
2026-03-21 07:40:37 +01:00
# Phase 4: Check pipeline feature access (boolean - enabled/disabled)
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
access_pipeline = check_feature_access ( pid , ' ai_pipeline ' )
2026-03-20 22:18:12 +01:00
log_feature_usage ( pid , ' ai_pipeline ' , access_pipeline , ' pipeline ' )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
if not access_pipeline [ ' allowed ' ] :
logger . warning (
2026-03-21 07:40:37 +01:00
f " [FEATURE-LIMIT] User { pid } blocked: "
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
f " ai_pipeline { access_pipeline [ ' reason ' ] } "
)
2026-03-21 07:40:37 +01:00
raise HTTPException (
status_code = 403 ,
detail = f " Pipeline-Analyse ist nicht verfügbar. Bitte kontaktiere den Admin. "
)
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
# Also check ai_calls (pipeline uses API calls too)
access_calls = check_feature_access ( pid , ' ai_calls ' )
2026-03-20 22:18:12 +01:00
log_feature_usage ( pid , ' ai_calls ' , access_calls , ' pipeline_calls ' )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
if not access_calls [ ' allowed ' ] :
logger . warning (
2026-03-21 07:40:37 +01:00
f " [FEATURE-LIMIT] User { pid } blocked: "
2026-03-20 22:32:09 +01:00
f " ai_calls { access_calls [ ' reason ' ] } (used: { access_calls [ ' used ' ] } , limit: { access_calls [ ' limit ' ] } ) "
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
)
2026-03-21 07:40:37 +01:00
raise HTTPException (
status_code = 403 ,
detail = f " Limit erreicht: Du hast das Kontingent für KI-Analysen überschritten ( { access_calls [ ' used ' ] } / { access_calls [ ' limit ' ] } ). "
f " Bitte kontaktiere den Admin oder warte bis zum nächsten Reset. "
)
2026-03-19 11:15:35 +01:00
data = _get_profile_data ( pid )
vars = _prepare_template_vars ( data )
# Stage 1: Parallel JSON analyses
with get_db ( ) as conn :
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=true " )
stage1_prompts = [ r2d ( r ) for r in cur . fetchall ( ) ]
stage1_results = { }
for p in stage1_prompts :
slug = p [ ' slug ' ]
final_prompt = _render_template ( p [ ' template ' ] , vars )
if ANTHROPIC_KEY :
import anthropic
client = anthropic . Anthropic ( api_key = ANTHROPIC_KEY )
response = client . messages . create (
model = " claude-sonnet-4-20250514 " ,
max_tokens = 1000 ,
messages = [ { " role " : " user " , " content " : final_prompt } ]
)
content = response . content [ 0 ] . text . strip ( )
elif OPENROUTER_KEY :
async with httpx . AsyncClient ( ) as client :
resp = await client . post ( " https://openrouter.ai/api/v1/chat/completions " ,
headers = { " Authorization " : f " Bearer { OPENROUTER_KEY } " } ,
json = {
" model " : OPENROUTER_MODEL ,
" messages " : [ { " role " : " user " , " content " : final_prompt } ] ,
" max_tokens " : 1000
} ,
timeout = 60.0
)
content = resp . json ( ) [ ' choices ' ] [ 0 ] [ ' message ' ] [ ' content ' ] . strip ( )
else :
raise HTTPException ( 500 , " Keine KI-API konfiguriert " )
# Try to parse JSON, fallback to raw text
try :
stage1_results [ slug ] = json . loads ( content )
except :
stage1_results [ slug ] = content
# Stage 2: Synthesis
vars [ ' stage1_body ' ] = json . dumps ( stage1_results . get ( ' pipeline_body ' , { } ) , ensure_ascii = False )
vars [ ' stage1_nutrition ' ] = json . dumps ( stage1_results . get ( ' pipeline_nutrition ' , { } ) , ensure_ascii = False )
vars [ ' stage1_activity ' ] = json . dumps ( stage1_results . get ( ' pipeline_activity ' , { } ) , ensure_ascii = False )
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " SELECT template FROM ai_prompts WHERE slug= ' pipeline_synthesis ' AND active=true " )
synth_row = cur . fetchone ( )
if not synth_row :
raise HTTPException ( 500 , " Pipeline synthesis prompt not found " )
synth_prompt = _render_template ( synth_row [ ' template ' ] , vars )
if ANTHROPIC_KEY :
import anthropic
client = anthropic . Anthropic ( api_key = ANTHROPIC_KEY )
response = client . messages . create (
model = " claude-sonnet-4-20250514 " ,
max_tokens = 2000 ,
messages = [ { " role " : " user " , " content " : synth_prompt } ]
)
synthesis = response . content [ 0 ] . text
elif OPENROUTER_KEY :
async with httpx . AsyncClient ( ) as client :
resp = await client . post ( " https://openrouter.ai/api/v1/chat/completions " ,
headers = { " Authorization " : f " Bearer { OPENROUTER_KEY } " } ,
json = {
" model " : OPENROUTER_MODEL ,
" messages " : [ { " role " : " user " , " content " : synth_prompt } ] ,
" max_tokens " : 2000
} ,
timeout = 60.0
)
synthesis = resp . json ( ) [ ' choices ' ] [ 0 ] [ ' message ' ] [ ' content ' ]
else :
raise HTTPException ( 500 , " Keine KI-API konfiguriert " )
# Stage 3: Goals (only if goals are set)
goals_text = None
prof = data [ ' profile ' ]
if prof . get ( ' goal_weight ' ) or prof . get ( ' goal_bf_pct ' ) :
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " SELECT template FROM ai_prompts WHERE slug= ' pipeline_goals ' AND active=true " )
goals_row = cur . fetchone ( )
if goals_row :
goals_prompt = _render_template ( goals_row [ ' template ' ] , vars )
if ANTHROPIC_KEY :
import anthropic
client = anthropic . Anthropic ( api_key = ANTHROPIC_KEY )
response = client . messages . create (
model = " claude-sonnet-4-20250514 " ,
max_tokens = 800 ,
messages = [ { " role " : " user " , " content " : goals_prompt } ]
)
goals_text = response . content [ 0 ] . text
elif OPENROUTER_KEY :
async with httpx . AsyncClient ( ) as client :
resp = await client . post ( " https://openrouter.ai/api/v1/chat/completions " ,
headers = { " Authorization " : f " Bearer { OPENROUTER_KEY } " } ,
json = {
" model " : OPENROUTER_MODEL ,
" messages " : [ { " role " : " user " , " content " : goals_prompt } ] ,
" max_tokens " : 800
} ,
timeout = 60.0
)
goals_text = resp . json ( ) [ ' choices ' ] [ 0 ] [ ' message ' ] [ ' content ' ]
# Combine synthesis + goals
final_content = synthesis
if goals_text :
final_content + = " \n \n " + goals_text
2026-03-20 15:35:33 +01:00
# Save as 'pipeline' scope (with history - no DELETE)
2026-03-19 11:15:35 +01:00
with get_db ( ) as conn :
cur = get_cursor ( conn )
2026-03-20 15:35:33 +01:00
cur . execute ( " INSERT INTO ai_insights (id, profile_id, scope, content, created) VALUES ( %s , %s , ' pipeline ' , %s ,CURRENT_TIMESTAMP) " ,
2026-03-19 11:15:35 +01:00
( str ( uuid . uuid4 ( ) ) , pid , final_content ) )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
# Phase 2: Increment ai_calls usage (pipeline uses multiple API calls)
# Note: We increment once per pipeline run, not per individual call
increment_feature_usage ( pid , ' ai_calls ' )
# Old usage tracking (keep for now)
ROLLBACK: complete removal of broken feature enforcement system
Reverts all feature enforcement changes (commits 3745ebd, cbad50a, cd4d912, 8415509)
to restore original working functionality.
Issues caused by feature enforcement implementation:
- Export buttons disappeared and never reappeared
- KI analysis counter not incrementing
- New analyses not saving
- Pipeline appearing twice
- Many core features broken
Restored files to working state before enforcement implementation (commit 0210844):
- Backend: auth.py, insights.py, exportdata.py, importdata.py, nutrition.py, activity.py
- Frontend: Analysis.jsx, SettingsPage.jsx, api.js
- Removed: FeatureGate.jsx, useFeatureAccess.js
The original simple AI limit system (ai_enabled, ai_limit_day) is now active again.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-20 15:19:56 +01:00
inc_ai_usage ( pid )
feat: v9c Phase 2 - Backend Non-Blocking Logging (12 Endpoints)
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>
2026-03-20 21:59:33 +01:00
2026-03-20 15:35:33 +01:00
return { " scope " : " pipeline " , " content " : final_content , " stage1 " : stage1_results }
2026-03-19 11:15:35 +01:00
@router.get ( " /ai/usage " )
def get_ai_usage ( x_profile_id : Optional [ str ] = Header ( default = None ) , session : dict = Depends ( require_auth ) ) :
""" Get AI usage stats for current profile. """
pid = get_pid ( x_profile_id )
with get_db ( ) as conn :
cur = get_cursor ( conn )
cur . execute ( " SELECT ai_limit_day FROM profiles WHERE id= %s " , ( pid , ) )
prof = cur . fetchone ( )
limit = prof [ ' ai_limit_day ' ] if prof else None
today = datetime . now ( ) . date ( ) . isoformat ( )
cur . execute ( " SELECT call_count FROM ai_usage WHERE profile_id= %s AND date= %s " , ( pid , today ) )
usage = cur . fetchone ( )
used = usage [ ' call_count ' ] if usage else 0
return { " limit " : limit , " used " : used , " remaining " : ( limit - used ) if limit else None }