diff --git a/backend/auth.py b/backend/auth.py index d170058..21b0042 100644 --- a/backend/auth.py +++ b/backend/auth.py @@ -318,9 +318,8 @@ def increment_feature_usage(profile_id: str, feature_id: str) -> None: ON CONFLICT (profile_id, feature_id) DO UPDATE SET usage_count = user_feature_usage.usage_count + 1, - reset_at = %s, updated = CURRENT_TIMESTAMP - """, (profile_id, feature_id, next_reset, next_reset)) + """, (profile_id, feature_id, next_reset)) conn.commit() diff --git a/backend/routers/activity.py b/backend/routers/activity.py index 86fa6ad..8ff768d 100644 --- a/backend/routers/activity.py +++ b/backend/routers/activity.py @@ -11,7 +11,7 @@ from typing import Optional from fastapi import APIRouter, HTTPException, UploadFile, File, Header, Depends from db import get_db, get_cursor, r2d -from auth import require_auth, check_feature_access, increment_feature_usage +from auth import require_auth from models import ActivityEntry from routers.profiles import get_pid @@ -94,17 +94,6 @@ def activity_stats(x_profile_id: Optional[str]=Header(default=None), session: di async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Import Apple Health workout CSV.""" pid = get_pid(x_profile_id) - - # Check feature access (v9c feature system) - access = check_feature_access(pid, 'csv_import') - if not access['allowed']: - if access['reason'] == 'feature_disabled': - raise HTTPException(403, "CSV-Import ist für dein Tier nicht verfügbar") - elif access['reason'] == 'limit_exceeded': - raise HTTPException(429, f"Monatliches Import-Limit erreicht ({access['limit']} Imports)") - else: - raise HTTPException(403, f"Zugriff verweigert: {access['reason']}") - raw = await file.read() try: text = raw.decode('utf-8') except: text = raw.decode('latin-1') @@ -145,8 +134,4 @@ async def import_activity_csv(file: UploadFile=File(...), x_profile_id: Optional tf(row.get('Distanz (km)','')))) inserted+=1 except: skipped+=1 - - # Increment import usage counter (v9c feature system) - increment_feature_usage(pid, 'csv_import') - return {"inserted":inserted,"skipped":skipped,"message":f"{inserted} Trainings importiert"} diff --git a/backend/routers/exportdata.py b/backend/routers/exportdata.py index b0269ce..02109ea 100644 --- a/backend/routers/exportdata.py +++ b/backend/routers/exportdata.py @@ -17,7 +17,7 @@ from fastapi import APIRouter, HTTPException, Header, Depends from fastapi.responses import StreamingResponse, Response from db import get_db, get_cursor, r2d -from auth import require_auth, check_feature_access, increment_feature_usage +from auth import require_auth from routers.profiles import get_pid router = APIRouter(prefix="/api/export", tags=["export"]) @@ -30,15 +30,13 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D """Export all data as CSV.""" pid = get_pid(x_profile_id) - # Check feature access (v9c feature system) - access = check_feature_access(pid, 'data_export') - if not access['allowed']: - if access['reason'] == 'feature_disabled': - raise HTTPException(403, "Export ist für dein Tier nicht verfügbar") - elif access['reason'] == 'limit_exceeded': - raise HTTPException(429, f"Monatliches Export-Limit erreicht ({access['limit']} Exporte)") - else: - raise HTTPException(403, f"Zugriff verweigert: {access['reason']}") + # Check export permission + with get_db() as conn: + cur = get_cursor(conn) + cur.execute("SELECT export_enabled FROM profiles WHERE id=%s", (pid,)) + prof = cur.fetchone() + if not prof or not prof['export_enabled']: + raise HTTPException(403, "Export ist für dieses Profil deaktiviert") # Build CSV output = io.StringIO() @@ -76,10 +74,6 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D writer.writerow(["Training", r['date'], r['activity_type'], f"{float(r['duration_min'])}min {float(r['kcal_active'])}kcal"]) output.seek(0) - - # Increment export usage counter (v9c feature system) - increment_feature_usage(pid, 'data_export') - return StreamingResponse( iter([output.getvalue()]), media_type="text/csv", @@ -92,15 +86,13 @@ def export_json(x_profile_id: Optional[str]=Header(default=None), session: dict= """Export all data as JSON.""" pid = get_pid(x_profile_id) - # Check feature access (v9c feature system) - access = check_feature_access(pid, 'data_export') - if not access['allowed']: - if access['reason'] == 'feature_disabled': - raise HTTPException(403, "Export ist für dein Tier nicht verfügbar") - elif access['reason'] == 'limit_exceeded': - raise HTTPException(429, f"Monatliches Export-Limit erreicht ({access['limit']} Exporte)") - else: - raise HTTPException(403, f"Zugriff verweigert: {access['reason']}") + # Check export permission + with get_db() as conn: + cur = get_cursor(conn) + cur.execute("SELECT export_enabled FROM profiles WHERE id=%s", (pid,)) + prof = cur.fetchone() + if not prof or not prof['export_enabled']: + raise HTTPException(403, "Export ist für dieses Profil deaktiviert") # Collect all data data = {} @@ -134,10 +126,6 @@ def export_json(x_profile_id: Optional[str]=Header(default=None), session: dict= return str(obj) json_str = json.dumps(data, indent=2, default=decimal_handler) - - # Increment export usage counter (v9c feature system) - increment_feature_usage(pid, 'data_export') - return Response( content=json_str, media_type="application/json", @@ -150,21 +138,13 @@ def export_zip(x_profile_id: Optional[str]=Header(default=None), session: dict=D """Export all data as ZIP (CSV + JSON + photos) per specification.""" pid = get_pid(x_profile_id) - # Check feature access (v9c feature system) - access = check_feature_access(pid, 'data_export') - if not access['allowed']: - if access['reason'] == 'feature_disabled': - raise HTTPException(403, "Export ist für dein Tier nicht verfügbar") - elif access['reason'] == 'limit_exceeded': - raise HTTPException(429, f"Monatliches Export-Limit erreicht ({access['limit']} Exporte)") - else: - raise HTTPException(403, f"Zugriff verweigert: {access['reason']}") - - # Get profile + # Check export permission & get profile with get_db() as conn: cur = get_cursor(conn) cur.execute("SELECT * FROM profiles WHERE id=%s", (pid,)) prof = r2d(cur.fetchone()) + if not prof or not prof.get('export_enabled'): + raise HTTPException(403, "Export ist für dieses Profil deaktiviert") # Helper: CSV writer with UTF-8 BOM + semicolon def write_csv(zf, filename, rows, columns): @@ -317,10 +297,6 @@ Datumsformat: YYYY-MM-DD zip_buffer.seek(0) filename = f"mitai-export-{profile_name.replace(' ','-')}-{export_date}.zip" - - # Increment export usage counter (v9c feature system) - increment_feature_usage(pid, 'data_export') - return StreamingResponse( iter([zip_buffer.getvalue()]), media_type="application/zip", diff --git a/backend/routers/importdata.py b/backend/routers/importdata.py index f2ced57..bd78e46 100644 --- a/backend/routers/importdata.py +++ b/backend/routers/importdata.py @@ -16,7 +16,7 @@ from datetime import datetime from fastapi import APIRouter, HTTPException, UploadFile, File, Header, Depends from db import get_db, get_cursor -from auth import require_auth, check_feature_access, increment_feature_usage +from auth import require_auth from routers.profiles import get_pid router = APIRouter(prefix="/api/import", tags=["import"]) @@ -41,16 +41,6 @@ async def import_zip( """ pid = get_pid(x_profile_id) - # Check feature access (v9c feature system) - access = check_feature_access(pid, 'csv_import') - if not access['allowed']: - if access['reason'] == 'feature_disabled': - raise HTTPException(403, "Import ist für dein Tier nicht verfügbar") - elif access['reason'] == 'limit_exceeded': - raise HTTPException(429, f"Monatliches Import-Limit erreicht ({access['limit']} Imports)") - else: - raise HTTPException(403, f"Zugriff verweigert: {access['reason']}") - # Read uploaded file content = await file.read() zip_buffer = io.BytesIO(content) @@ -264,9 +254,6 @@ async def import_zip( conn.rollback() raise HTTPException(500, f"Import fehlgeschlagen: {str(e)}") - # Increment import usage counter (v9c feature system) - increment_feature_usage(pid, 'csv_import') - return { "ok": True, "message": "Import erfolgreich", diff --git a/backend/routers/insights.py b/backend/routers/insights.py index 50ecbe9..b325bf0 100644 --- a/backend/routers/insights.py +++ b/backend/routers/insights.py @@ -13,7 +13,7 @@ from datetime import datetime from fastapi import APIRouter, HTTPException, Header, Depends from db import get_db, get_cursor, r2d -from auth import require_auth, require_admin, check_feature_access, increment_feature_usage +from auth import require_auth, require_admin from routers.profiles import get_pid router = APIRouter(prefix="/api", tags=["insights"]) @@ -251,16 +251,7 @@ def delete_ai_insight(scope: str, x_profile_id: Optional[str]=Header(default=Non 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) - - # Check feature access (v9c feature system) - access = check_feature_access(pid, 'ai_calls') - if not access['allowed']: - if access['reason'] == 'feature_disabled': - raise HTTPException(403, "KI-Analysen sind für dein Tier nicht verfügbar") - elif access['reason'] == 'limit_exceeded': - raise HTTPException(429, f"Monatliches KI-Limit erreicht ({access['limit']} Analysen)") - else: - raise HTTPException(403, f"Zugriff verweigert: {access['reason']}") + check_ai_limit(pid) # Get prompt template with get_db() as conn: @@ -310,8 +301,7 @@ async def analyze_with_prompt(slug: str, x_profile_id: Optional[str]=Header(defa 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)) - # Increment usage counter (v9c feature system) - increment_feature_usage(pid, 'ai_calls') + inc_ai_usage(pid) return {"scope": slug, "content": content} @@ -319,16 +309,7 @@ async def analyze_with_prompt(slug: str, x_profile_id: Optional[str]=Header(defa 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) - - # Check feature access for pipeline (v9c feature system) - access = check_feature_access(pid, 'ai_pipeline') - if not access['allowed']: - if access['reason'] == 'feature_disabled': - raise HTTPException(403, "KI-Pipeline ist für dein Tier nicht verfügbar") - elif access['reason'] == 'limit_exceeded': - raise HTTPException(429, f"Monatliches Pipeline-Limit erreicht ({access['limit']} Pipelines)") - else: - raise HTTPException(403, f"Zugriff verweigert: {access['reason']}") + check_ai_limit(pid) data = _get_profile_data(pid) vars = _prepare_template_vars(data) @@ -457,8 +438,7 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses 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)) - # Increment pipeline usage counter (v9c feature system) - increment_feature_usage(pid, 'ai_pipeline') + inc_ai_usage(pid) return {"scope": "gesamt", "content": final_content, "stage1": stage1_results} diff --git a/backend/routers/nutrition.py b/backend/routers/nutrition.py index d50bf75..e12b4c0 100644 --- a/backend/routers/nutrition.py +++ b/backend/routers/nutrition.py @@ -12,7 +12,7 @@ from datetime import datetime from fastapi import APIRouter, HTTPException, UploadFile, File, Header, Depends from db import get_db, get_cursor, r2d -from auth import require_auth, check_feature_access, increment_feature_usage +from auth import require_auth from routers.profiles import get_pid router = APIRouter(prefix="/api/nutrition", tags=["nutrition"]) @@ -30,17 +30,6 @@ def _pf(s): async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optional[str]=Header(default=None), session: dict=Depends(require_auth)): """Import FDDB nutrition CSV.""" pid = get_pid(x_profile_id) - - # Check feature access (v9c feature system) - access = check_feature_access(pid, 'csv_import') - if not access['allowed']: - if access['reason'] == 'feature_disabled': - raise HTTPException(403, "CSV-Import ist für dein Tier nicht verfügbar") - elif access['reason'] == 'limit_exceeded': - raise HTTPException(429, f"Monatliches Import-Limit erreicht ({access['limit']} Imports)") - else: - raise HTTPException(403, f"Zugriff verweigert: {access['reason']}") - raw = await file.read() try: text = raw.decode('utf-8') except: text = raw.decode('latin-1') @@ -76,10 +65,6 @@ async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optiona cur.execute("INSERT INTO nutrition_log (id,profile_id,date,kcal,protein_g,fat_g,carbs_g,source,created) VALUES (%s,%s,%s,%s,%s,%s,%s,'csv',CURRENT_TIMESTAMP)", (str(uuid.uuid4()),pid,iso,kcal,prot,fat,carbs)) inserted+=1 - - # Increment import usage counter (v9c feature system) - increment_feature_usage(pid, 'csv_import') - return {"rows_parsed":count,"days_imported":inserted, "date_range":{"from":min(days) if days else None,"to":max(days) if days else None}} diff --git a/frontend/src/components/FeatureGate.jsx b/frontend/src/components/FeatureGate.jsx deleted file mode 100644 index d4594a2..0000000 --- a/frontend/src/components/FeatureGate.jsx +++ /dev/null @@ -1,80 +0,0 @@ -import { useFeatureAccess } from '../hooks/useFeatureAccess' - -/** - * Feature Gate Component - * - * Hides children if feature is not available. - * Optionally shows upgrade prompt or message. - * - * @param {string} feature - Feature slug (e.g. 'ai_calls', 'data_export') - * @param {ReactNode} children - Content to gate - * @param {ReactNode} fallback - Optional fallback when not allowed - * @param {boolean} showUpgradePrompt - Show upgrade message instead of hiding - */ -export function FeatureGate({ feature, children, fallback = null, showUpgradePrompt = false }) { - const { canUse, reason, loading, limit, remaining } = useFeatureAccess(feature) - - // While loading, show children optimistically (better UX) - if (loading) return <>{children}> - - // If allowed, render children - if (canUse) return <>{children}> - - // Not allowed - if (showUpgradePrompt) { - return ( -
- Oder wähle eine Einzelanalyse:
-
+ Oder wähle eine Einzelanalyse: +
} - {activePrompts.map(p => { + {activePrompts.map(p => { // Show latest existing insight for this prompt const existing = allInsights.find(i=>i.scope===p.slug) return ( @@ -313,11 +305,10 @@ export default function Analysis() { )}Keine aktiven Prompts. Aktiviere im Tab "Prompts".
Keine aktiven Prompts. Aktiviere im Tab "Prompts".
- Exportiert alle Daten von {activeProfile?.name}: - Gewicht, Umfänge, Caliper, Ernährung, Aktivität und KI-Auswertungen. -
-+ Exportiert alle Daten von {activeProfile?.name}: + Gewicht, Umfänge, Caliper, Ernährung, Aktivität und KI-Auswertungen. +
+- Der ZIP-Export enthält separate Dateien für Excel und eine lesbare KI-Auswertungsdatei. -
+ >}+ Der ZIP-Export enthält separate Dateien für Excel und eine lesbare KI-Auswertungsdatei. +
+- Importiere einen ZIP-Export zurück in {activeProfile?.name}. - Vorhandene Einträge werden nicht überschrieben. -
-