Compare commits
No commits in common. "ed057fe54556f72f735d818dc8a78dabf4679789" and "d13c2c7e2500daa56b2498846273d13c39745736" have entirely different histories.
ed057fe545
...
d13c2c7e25
|
|
@ -37,20 +37,15 @@ def create_activity(e: ActivityEntry, x_profile_id: Optional[str]=Header(default
|
||||||
"""Create new activity entry."""
|
"""Create new activity entry."""
|
||||||
pid = get_pid(x_profile_id)
|
pid = get_pid(x_profile_id)
|
||||||
|
|
||||||
# Phase 4: Check feature access and ENFORCE
|
# Phase 2: Check feature access (non-blocking, log only)
|
||||||
access = check_feature_access(pid, 'activity_entries')
|
access = check_feature_access(pid, 'activity_entries')
|
||||||
log_feature_usage(pid, 'activity_entries', access, 'create')
|
log_feature_usage(pid, 'activity_entries', access, 'create')
|
||||||
|
|
||||||
if not access['allowed']:
|
if not access['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"activity_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
f"activity_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
|
||||||
detail=f"Limit erreicht: Du hast das Kontingent für Aktivitätseinträge überschritten ({access['used']}/{access['limit']}). "
|
|
||||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
|
||||||
)
|
|
||||||
|
|
||||||
eid = str(uuid.uuid4())
|
eid = str(uuid.uuid4())
|
||||||
d = e.model_dump()
|
d = e.model_dump()
|
||||||
|
|
|
||||||
|
|
@ -33,20 +33,24 @@ def export_csv(x_profile_id: Optional[str]=Header(default=None), session: dict=D
|
||||||
"""Export all data as CSV."""
|
"""Export all data as CSV."""
|
||||||
pid = get_pid(x_profile_id)
|
pid = get_pid(x_profile_id)
|
||||||
|
|
||||||
# Phase 4: Check feature access and ENFORCE
|
# Phase 2: Check feature access (non-blocking, log only)
|
||||||
access = check_feature_access(pid, 'data_export')
|
access = check_feature_access(pid, 'data_export')
|
||||||
log_feature_usage(pid, 'data_export', access, 'export_csv')
|
log_feature_usage(pid, 'data_export', access, 'export_csv')
|
||||||
|
|
||||||
if not access['allowed']:
|
if not access['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"data_export {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
f"data_export {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
# NOTE: Phase 2 does NOT block - just logs!
|
||||||
status_code=403,
|
|
||||||
detail=f"Limit erreicht: Du hast das Kontingent für Daten-Exporte überschritten ({access['used']}/{access['limit']}). "
|
# Old permission check (keep for now)
|
||||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
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
|
# Build CSV
|
||||||
output = io.StringIO()
|
output = io.StringIO()
|
||||||
|
|
@ -100,20 +104,23 @@ def export_json(x_profile_id: Optional[str]=Header(default=None), session: dict=
|
||||||
"""Export all data as JSON."""
|
"""Export all data as JSON."""
|
||||||
pid = get_pid(x_profile_id)
|
pid = get_pid(x_profile_id)
|
||||||
|
|
||||||
# Phase 4: Check feature access and ENFORCE
|
# Phase 2: Check feature access (non-blocking, log only)
|
||||||
access = check_feature_access(pid, 'data_export')
|
access = check_feature_access(pid, 'data_export')
|
||||||
log_feature_usage(pid, 'data_export', access, 'export_json')
|
log_feature_usage(pid, 'data_export', access, 'export_json')
|
||||||
|
|
||||||
if not access['allowed']:
|
if not access['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"data_export {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
f"data_export {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
# Old permission check (keep for now)
|
||||||
detail=f"Limit erreicht: Du hast das Kontingent für Daten-Exporte überschritten ({access['used']}/{access['limit']}). "
|
with get_db() as conn:
|
||||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
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
|
# Collect all data
|
||||||
data = {}
|
data = {}
|
||||||
|
|
@ -163,26 +170,23 @@ def export_zip(x_profile_id: Optional[str]=Header(default=None), session: dict=D
|
||||||
"""Export all data as ZIP (CSV + JSON + photos) per specification."""
|
"""Export all data as ZIP (CSV + JSON + photos) per specification."""
|
||||||
pid = get_pid(x_profile_id)
|
pid = get_pid(x_profile_id)
|
||||||
|
|
||||||
# Phase 4: Check feature access and ENFORCE
|
# Phase 2: Check feature access (non-blocking, log only)
|
||||||
access = check_feature_access(pid, 'data_export')
|
access = check_feature_access(pid, 'data_export')
|
||||||
log_feature_usage(pid, 'data_export', access, 'export_zip')
|
log_feature_usage(pid, 'data_export', access, 'export_zip')
|
||||||
|
|
||||||
if not access['allowed']:
|
if not access['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"data_export {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
f"data_export {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
|
||||||
detail=f"Limit erreicht: Du hast das Kontingent für Daten-Exporte überschritten ({access['used']}/{access['limit']}). "
|
|
||||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Get profile
|
# Old permission check & get profile
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
cur = get_cursor(conn)
|
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())
|
||||||
|
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
|
# Helper: CSV writer with UTF-8 BOM + semicolon
|
||||||
def write_csv(zf, filename, rows, columns):
|
def write_csv(zf, filename, rows, columns):
|
||||||
|
|
|
||||||
|
|
@ -44,20 +44,16 @@ async def import_zip(
|
||||||
"""
|
"""
|
||||||
pid = get_pid(x_profile_id)
|
pid = get_pid(x_profile_id)
|
||||||
|
|
||||||
# Phase 4: Check feature access and ENFORCE
|
# Phase 2: Check feature access (non-blocking, log only)
|
||||||
access = check_feature_access(pid, 'data_import')
|
access = check_feature_access(pid, 'data_import')
|
||||||
log_feature_usage(pid, 'data_import', access, 'import_zip')
|
log_feature_usage(pid, 'data_import', access, 'import_zip')
|
||||||
|
|
||||||
if not access['allowed']:
|
if not access['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"data_import {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
f"data_import {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
# NOTE: Phase 2 does NOT block - just logs!
|
||||||
status_code=403,
|
|
||||||
detail=f"Limit erreicht: Du hast das Kontingent für Daten-Importe überschritten ({access['used']}/{access['limit']}). "
|
|
||||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Read uploaded file
|
# Read uploaded file
|
||||||
content = await file.read()
|
content = await file.read()
|
||||||
|
|
|
||||||
|
|
@ -255,20 +255,19 @@ async def analyze_with_prompt(slug: str, x_profile_id: Optional[str]=Header(defa
|
||||||
"""Run AI analysis with specified prompt template."""
|
"""Run AI analysis with specified prompt template."""
|
||||||
pid = get_pid(x_profile_id)
|
pid = get_pid(x_profile_id)
|
||||||
|
|
||||||
# Phase 4: Check feature access and ENFORCE
|
# Phase 2: Check feature access (non-blocking, log only)
|
||||||
access = check_feature_access(pid, 'ai_calls')
|
access = check_feature_access(pid, 'ai_calls')
|
||||||
log_feature_usage(pid, 'ai_calls', access, 'analyze')
|
log_feature_usage(pid, 'ai_calls', access, 'analyze')
|
||||||
|
|
||||||
if not access['allowed']:
|
if not access['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"ai_calls {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
f"ai_calls {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
# NOTE: Phase 2 does NOT block - just logs!
|
||||||
status_code=403,
|
|
||||||
detail=f"Limit erreicht: Du hast das Kontingent für KI-Analysen überschritten ({access['used']}/{access['limit']}). "
|
# Old check (keep for now, but will be replaced in Phase 4)
|
||||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
check_ai_limit(pid)
|
||||||
)
|
|
||||||
|
|
||||||
# Get prompt template
|
# Get prompt template
|
||||||
with get_db() as conn:
|
with get_db() as conn:
|
||||||
|
|
@ -331,19 +330,16 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses
|
||||||
"""Run 3-stage pipeline analysis."""
|
"""Run 3-stage pipeline analysis."""
|
||||||
pid = get_pid(x_profile_id)
|
pid = get_pid(x_profile_id)
|
||||||
|
|
||||||
# Phase 4: Check pipeline feature access (boolean - enabled/disabled)
|
# Phase 2: Check pipeline feature access (boolean - enabled/disabled)
|
||||||
access_pipeline = check_feature_access(pid, 'ai_pipeline')
|
access_pipeline = check_feature_access(pid, 'ai_pipeline')
|
||||||
log_feature_usage(pid, 'ai_pipeline', access_pipeline, 'pipeline')
|
log_feature_usage(pid, 'ai_pipeline', access_pipeline, 'pipeline')
|
||||||
|
|
||||||
if not access_pipeline['allowed']:
|
if not access_pipeline['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"ai_pipeline {access_pipeline['reason']}"
|
f"ai_pipeline {access_pipeline['reason']}"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
# NOTE: Phase 2 does NOT block - just logs!
|
||||||
status_code=403,
|
|
||||||
detail=f"Pipeline-Analyse ist nicht verfügbar. Bitte kontaktiere den Admin."
|
|
||||||
)
|
|
||||||
|
|
||||||
# Also check ai_calls (pipeline uses API calls too)
|
# Also check ai_calls (pipeline uses API calls too)
|
||||||
access_calls = check_feature_access(pid, 'ai_calls')
|
access_calls = check_feature_access(pid, 'ai_calls')
|
||||||
|
|
@ -351,14 +347,12 @@ async def analyze_pipeline(x_profile_id: Optional[str]=Header(default=None), ses
|
||||||
|
|
||||||
if not access_calls['allowed']:
|
if not access_calls['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"ai_calls {access_calls['reason']} (used: {access_calls['used']}, limit: {access_calls['limit']})"
|
f"ai_calls {access_calls['reason']} (used: {access_calls['used']}, limit: {access_calls['limit']})"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
# Old check (keep for now)
|
||||||
detail=f"Limit erreicht: Du hast das Kontingent für KI-Analysen überschritten ({access_calls['used']}/{access_calls['limit']}). "
|
check_ai_limit(pid)
|
||||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
|
||||||
)
|
|
||||||
|
|
||||||
data = _get_profile_data(pid)
|
data = _get_profile_data(pid)
|
||||||
vars = _prepare_template_vars(data)
|
vars = _prepare_template_vars(data)
|
||||||
|
|
|
||||||
|
|
@ -34,21 +34,16 @@ async def import_nutrition_csv(file: UploadFile=File(...), x_profile_id: Optiona
|
||||||
"""Import FDDB nutrition CSV."""
|
"""Import FDDB nutrition CSV."""
|
||||||
pid = get_pid(x_profile_id)
|
pid = get_pid(x_profile_id)
|
||||||
|
|
||||||
# Phase 4: Check feature access and ENFORCE
|
# Phase 2: Check feature access (non-blocking, log only)
|
||||||
# Note: CSV import can create many entries - we check once before import
|
# Note: CSV import can create many entries - we check once before import
|
||||||
access = check_feature_access(pid, 'nutrition_entries')
|
access = check_feature_access(pid, 'nutrition_entries')
|
||||||
log_feature_usage(pid, 'nutrition_entries', access, 'import_csv')
|
log_feature_usage(pid, 'nutrition_entries', access, 'import_csv')
|
||||||
|
|
||||||
if not access['allowed']:
|
if not access['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"nutrition_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
f"nutrition_entries {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
|
||||||
detail=f"Limit erreicht: Du hast das Kontingent für Ernährungseinträge überschritten ({access['used']}/{access['limit']}). "
|
|
||||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
|
||||||
)
|
|
||||||
|
|
||||||
raw = await file.read()
|
raw = await file.read()
|
||||||
try: text = raw.decode('utf-8')
|
try: text = raw.decode('utf-8')
|
||||||
|
|
|
||||||
|
|
@ -31,20 +31,15 @@ async def upload_photo(file: UploadFile=File(...), date: str="",
|
||||||
"""Upload progress photo."""
|
"""Upload progress photo."""
|
||||||
pid = get_pid(x_profile_id)
|
pid = get_pid(x_profile_id)
|
||||||
|
|
||||||
# Phase 4: Check feature access and ENFORCE
|
# Phase 2: Check feature access (non-blocking, log only)
|
||||||
access = check_feature_access(pid, 'photos')
|
access = check_feature_access(pid, 'photos')
|
||||||
log_feature_usage(pid, 'photos', access, 'upload')
|
log_feature_usage(pid, 'photos', access, 'upload')
|
||||||
|
|
||||||
if not access['allowed']:
|
if not access['allowed']:
|
||||||
logger.warning(
|
logger.warning(
|
||||||
f"[FEATURE-LIMIT] User {pid} blocked: "
|
f"[FEATURE-LIMIT] User {pid} would be blocked: "
|
||||||
f"photos {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
f"photos {access['reason']} (used: {access['used']}, limit: {access['limit']})"
|
||||||
)
|
)
|
||||||
raise HTTPException(
|
|
||||||
status_code=403,
|
|
||||||
detail=f"Limit erreicht: Du hast das Kontingent für Fotos überschritten ({access['used']}/{access['limit']}). "
|
|
||||||
f"Bitte kontaktiere den Admin oder warte bis zum nächsten Reset."
|
|
||||||
)
|
|
||||||
|
|
||||||
fid = str(uuid.uuid4())
|
fid = str(uuid.uuid4())
|
||||||
ext = Path(file.filename).suffix or '.jpg'
|
ext = Path(file.filename).suffix or '.jpg'
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,6 @@ import { useState, useEffect, useRef } from 'react'
|
||||||
import { Upload, Pencil, Trash2, Check, X, CheckCircle } from 'lucide-react'
|
import { Upload, Pencil, Trash2, Check, X, CheckCircle } from 'lucide-react'
|
||||||
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'
|
import { BarChart, Bar, XAxis, YAxis, Tooltip, ResponsiveContainer, CartesianGrid } from 'recharts'
|
||||||
import { api } from '../utils/api'
|
import { api } from '../utils/api'
|
||||||
import UsageBadge from '../components/UsageBadge'
|
|
||||||
import dayjs from 'dayjs'
|
import dayjs from 'dayjs'
|
||||||
import 'dayjs/locale/de'
|
import 'dayjs/locale/de'
|
||||||
dayjs.locale('de')
|
dayjs.locale('de')
|
||||||
|
|
@ -80,7 +79,7 @@ function ImportPanel({ onImported }) {
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Manual Entry ──────────────────────────────────────────────────────────────
|
// ── Manual Entry ──────────────────────────────────────────────────────────────
|
||||||
function EntryForm({ form, setForm, onSave, onCancel, saveLabel='Speichern', saving=false, error=null, usage=null }) {
|
function EntryForm({ form, setForm, onSave, onCancel, saveLabel='Speichern' }) {
|
||||||
const set = (k,v) => setForm(f=>({...f,[k]:v}))
|
const set = (k,v) => setForm(f=>({...f,[k]:v}))
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
|
@ -131,25 +130,8 @@ function EntryForm({ form, setForm, onSave, onCancel, saveLabel='Speichern', sav
|
||||||
value={form.notes||''} onChange={e=>set('notes',e.target.value)}/>
|
value={form.notes||''} onChange={e=>set('notes',e.target.value)}/>
|
||||||
<span className="form-unit"/>
|
<span className="form-unit"/>
|
||||||
</div>
|
</div>
|
||||||
{error && (
|
|
||||||
<div style={{padding:'10px',background:'var(--danger-bg)',border:'1px solid var(--danger)',borderRadius:8,fontSize:13,color:'var(--danger)',marginBottom:8}}>
|
|
||||||
{error}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div style={{display:'flex',gap:6,marginTop:8}}>
|
<div style={{display:'flex',gap:6,marginTop:8}}>
|
||||||
<div
|
<button className="btn btn-primary" style={{flex:1}} onClick={onSave}>{saveLabel}</button>
|
||||||
title={usage && !usage.allowed ? `Limit erreicht (${usage.used}/${usage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
|
||||||
style={{flex:1,display:'inline-block'}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
style={{width:'100%', cursor: (usage && !usage.allowed) ? 'not-allowed' : 'pointer'}}
|
|
||||||
onClick={onSave}
|
|
||||||
disabled={saving || (usage && !usage.allowed)}
|
|
||||||
>
|
|
||||||
{(usage && !usage.allowed) ? '🔒 Limit erreicht' : saveLabel}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
{onCancel && <button className="btn btn-secondary" style={{flex:1}} onClick={onCancel}><X size={13}/> Abbrechen</button>}
|
{onCancel && <button className="btn btn-secondary" style={{flex:1}} onClick={onCancel}><X size={13}/> Abbrechen</button>}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
@ -163,32 +145,15 @@ export default function ActivityPage() {
|
||||||
const [tab, setTab] = useState('list')
|
const [tab, setTab] = useState('list')
|
||||||
const [form, setForm] = useState(empty())
|
const [form, setForm] = useState(empty())
|
||||||
const [editing, setEditing] = useState(null)
|
const [editing, setEditing] = useState(null)
|
||||||
const [saving, setSaving] = useState(false)
|
|
||||||
const [saved, setSaved] = useState(false)
|
const [saved, setSaved] = useState(false)
|
||||||
const [error, setError] = useState(null)
|
|
||||||
const [activityUsage, setActivityUsage] = useState(null) // Phase 4: Usage badge
|
|
||||||
|
|
||||||
const load = async () => {
|
const load = async () => {
|
||||||
const [e, s] = await Promise.all([api.listActivity(), api.activityStats()])
|
const [e, s] = await Promise.all([api.listActivity(), api.activityStats()])
|
||||||
setEntries(e); setStats(s)
|
setEntries(e); setStats(s)
|
||||||
}
|
}
|
||||||
|
useEffect(()=>{ load() },[])
|
||||||
const loadUsage = () => {
|
|
||||||
api.getFeatureUsage().then(features => {
|
|
||||||
const activityFeature = features.find(f => f.feature_id === 'activity_entries')
|
|
||||||
setActivityUsage(activityFeature)
|
|
||||||
}).catch(err => console.error('Failed to load usage:', err))
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(()=>{
|
|
||||||
load()
|
|
||||||
loadUsage()
|
|
||||||
},[])
|
|
||||||
|
|
||||||
const handleSave = async () => {
|
const handleSave = async () => {
|
||||||
setSaving(true)
|
|
||||||
setError(null)
|
|
||||||
try {
|
|
||||||
const payload = {...form}
|
const payload = {...form}
|
||||||
if(payload.duration_min) payload.duration_min = parseFloat(payload.duration_min)
|
if(payload.duration_min) payload.duration_min = parseFloat(payload.duration_min)
|
||||||
if(payload.kcal_active) payload.kcal_active = parseFloat(payload.kcal_active)
|
if(payload.kcal_active) payload.kcal_active = parseFloat(payload.kcal_active)
|
||||||
|
|
@ -197,17 +162,8 @@ export default function ActivityPage() {
|
||||||
if(payload.rpe) payload.rpe = parseInt(payload.rpe)
|
if(payload.rpe) payload.rpe = parseInt(payload.rpe)
|
||||||
payload.source = 'manual'
|
payload.source = 'manual'
|
||||||
await api.createActivity(payload)
|
await api.createActivity(payload)
|
||||||
setSaved(true)
|
setSaved(true); await load()
|
||||||
await load()
|
|
||||||
await loadUsage() // Reload usage after save
|
|
||||||
setTimeout(()=>{ setSaved(false); setForm(empty()) }, 1500)
|
setTimeout(()=>{ setSaved(false); setForm(empty()) }, 1500)
|
||||||
} catch (err) {
|
|
||||||
console.error('Save failed:', err)
|
|
||||||
setError(err.message || 'Fehler beim Speichern')
|
|
||||||
setTimeout(()=>setError(null), 5000)
|
|
||||||
} finally {
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleUpdate = async () => {
|
const handleUpdate = async () => {
|
||||||
|
|
@ -269,13 +225,9 @@ export default function ActivityPage() {
|
||||||
|
|
||||||
{tab==='add' && (
|
{tab==='add' && (
|
||||||
<div className="card section-gap">
|
<div className="card section-gap">
|
||||||
<div className="card-title badge-container-right">
|
<div className="card-title">Training eintragen</div>
|
||||||
<span>Training eintragen</span>
|
|
||||||
{activityUsage && <UsageBadge {...activityUsage} />}
|
|
||||||
</div>
|
|
||||||
<EntryForm form={form} setForm={setForm}
|
<EntryForm form={form} setForm={setForm}
|
||||||
onSave={handleSave} saveLabel={saved?'✓ Gespeichert!':'Speichern'}
|
onSave={handleSave} saveLabel={saved?'✓ Gespeichert!':'Speichern'}/>
|
||||||
saving={saving} error={error} usage={activityUsage}/>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -254,22 +254,12 @@ export default function Analysis() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<button className="btn btn-primary" style={{flexShrink:0,minWidth:100}}
|
||||||
title={aiUsage && !aiUsage.allowed ? `Limit erreicht (${aiUsage.used}/${aiUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
onClick={runPipeline} disabled={!!loading||pipelineLoading}>
|
||||||
style={{display:'inline-block'}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
style={{flexShrink:0,minWidth:100, cursor: (aiUsage && !aiUsage.allowed) ? 'not-allowed' : 'pointer'}}
|
|
||||||
onClick={runPipeline}
|
|
||||||
disabled={!!loading||pipelineLoading||(aiUsage && !aiUsage.allowed)}
|
|
||||||
>
|
|
||||||
{pipelineLoading
|
{pipelineLoading
|
||||||
? <><div className="spinner" style={{width:13,height:13}}/> Läuft…</>
|
? <><div className="spinner" style={{width:13,height:13}}/> Läuft…</>
|
||||||
: (aiUsage && !aiUsage.allowed) ? '🔒 Limit'
|
|
||||||
: <><Brain size={13}/> Starten</>}
|
: <><Brain size={13}/> Starten</>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
|
||||||
{!canUseAI && <div style={{fontSize:11,color:'#D85A30',marginTop:4}}>🔒 KI nicht freigeschaltet</div>}
|
{!canUseAI && <div style={{fontSize:11,color:'#D85A30',marginTop:4}}>🔒 KI nicht freigeschaltet</div>}
|
||||||
</div>
|
</div>
|
||||||
{pipelineLoading && (
|
{pipelineLoading && (
|
||||||
|
|
@ -316,23 +306,13 @@ export default function Analysis() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div
|
<button className="btn btn-primary" style={{flexShrink:0,minWidth:90}}
|
||||||
title={aiUsage && !aiUsage.allowed ? `Limit erreicht (${aiUsage.used}/${aiUsage.limit}). Kontaktiere den Admin oder warte bis zum nächsten Reset.` : ''}
|
onClick={()=>runPrompt(p.slug)} disabled={!!loading||!canUseAI}>
|
||||||
style={{display:'inline-block'}}
|
|
||||||
>
|
|
||||||
<button
|
|
||||||
className="btn btn-primary"
|
|
||||||
style={{flexShrink:0,minWidth:90, cursor: (aiUsage && !aiUsage.allowed) ? 'not-allowed' : 'pointer'}}
|
|
||||||
onClick={()=>runPrompt(p.slug)}
|
|
||||||
disabled={!!loading||!canUseAI||(aiUsage && !aiUsage.allowed)}
|
|
||||||
>
|
|
||||||
{loading===p.slug
|
{loading===p.slug
|
||||||
? <><div className="spinner" style={{width:13,height:13}}/> Läuft…</>
|
? <><div className="spinner" style={{width:13,height:13}}/> Läuft…</>
|
||||||
: (aiUsage && !aiUsage.allowed) ? '🔒 Limit'
|
|
||||||
: <><Brain size={13}/> {existing?'Neu erstellen':'Starten'}</>}
|
: <><Brain size={13}/> {existing?'Neu erstellen':'Starten'}</>}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
{/* Show existing result collapsed */}
|
{/* Show existing result collapsed */}
|
||||||
{existing && newResult?.id !== existing.id && (
|
{existing && newResult?.id !== existing.id && (
|
||||||
<div style={{marginTop:8,borderTop:'1px solid var(--border)',paddingTop:8}}>
|
<div style={{marginTop:8,borderTop:'1px solid var(--border)',paddingTop:8}}>
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user