mitai-jinkendo/frontend/src/utils/api.js
Lars c91317df8e
All checks were successful
Deploy Development / deploy (push) Successful in 55s
Build Test / pytest-backend (push) Successful in 5s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 24s
feat: implement photo management features in the application
- Added a new `resolve_photo_path` function to handle legacy and new photo paths effectively.
- Updated the photo upload process to store only filenames in the database, improving path resolution.
- Enhanced the photo retrieval and deletion processes to utilize the new path resolution logic.
- Introduced a dedicated PhotosCapturePage for managing photo uploads and viewing.
- Updated the dashboard and navigation to include links to the new photo management features.
- Improved the photo grid display with sorting and deletion capabilities for better user experience.
2026-04-19 09:20:28 +02:00

721 lines
34 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { getToken } from '../context/AuthContext'
let _profileId = null
export function setProfileId(id) { _profileId = id }
const BASE = '/api'
/**
* FastAPI-Fehler: `detail` kann String, Objekt oder Validierungs-Array sein.
*/
export function formatFastApiDetail(detail, fallback = '') {
if (detail == null || detail === '') {
return fallback || 'Anfrage fehlgeschlagen'
}
if (typeof detail === 'string') {
return detail
}
if (Array.isArray(detail)) {
const parts = detail.map((e) => {
if (typeof e === 'string') return e
if (e && typeof e === 'object') {
const loc = Array.isArray(e.loc) ? e.loc.filter((x) => x != null && x !== '').join('.') : ''
const msg = e.msg || e.message || ''
if (loc && msg) return `${loc}: ${msg}`
return msg || loc || ''
}
return String(e)
}).filter(Boolean)
return parts.length ? parts.join(' · ') : fallback || 'Validierungsfehler'
}
if (typeof detail === 'object') {
if (Array.isArray(detail.errors) && detail.errors.length > 0) {
const parts = detail.errors
.map((e) => {
if (e && typeof e === 'object') return e.message || e.msg || ''
return String(e)
})
.filter(Boolean)
if (parts.length) return parts.join(' · ')
}
if (typeof detail.msg === 'string') return detail.msg
if (typeof detail.message === 'string') return detail.message
}
return fallback || 'Anfrage fehlgeschlagen'
}
function hdrs(extra={}) {
const h = {...extra}
if (_profileId) h['X-Profile-Id'] = _profileId
const token = getToken()
if (token) h['X-Auth-Token'] = token
return h
}
async function req(path, opts={}) {
const res = await fetch(BASE+path, {...opts, headers:hdrs(opts.headers||{})})
if (!res.ok) {
const errText = await res.text()
let parsed = null
try {
parsed = JSON.parse(errText)
} catch {
throw new Error(errText.trim() || `HTTP ${res.status}`)
}
throw new Error(formatFastApiDetail(parsed.detail, errText.trim() || `HTTP ${res.status}`))
}
return res.json()
}
const json=(d)=>({method:'POST',headers:{'Content-Type':'application/json'},body:JSON.stringify(d)})
const jput=(d)=>({method:'PUT', headers:{'Content-Type':'application/json'},body:JSON.stringify(d)})
export const api = {
// Profiles
getActiveProfile: () => req('/profile'),
listProfiles: () => req('/profiles'),
createProfile: (d) => req('/profiles', json(d)),
updateProfile: (id,d) => req(`/profiles/${id}`, jput(d)),
deleteProfile: (id) => req(`/profiles/${id}`, {method:'DELETE'}),
getProfile: () => req('/profile'),
updateActiveProfile:(d)=> req('/profile', jput(d)),
// App-Bereich: Dashboard-Lab (Layout JSON, Issue #65) + Widget-Katalog
getAppWidgetsCatalog: () => req('/app/widgets/catalog'),
getAppDashboardLayout: () => req('/app/dashboard-layout'),
putAppDashboardLayout: (layout) => req('/app/dashboard-layout', jput(layout)),
resetAppDashboardLayout: () => req('/app/dashboard-layout/reset', { method: 'POST' }),
adminGetWidgetsCatalogFull: () => req('/admin/widgets/catalog-full'),
adminGetDashboardProductDefault: () => req('/admin/dashboard-product-default'),
adminPutDashboardProductDefault: (layout) =>
req('/admin/dashboard-product-default', jput(layout)),
adminDeleteDashboardProductDefault: () =>
req('/admin/dashboard-product-default', { method: 'DELETE' }),
adminGetWidgetFeatureAssignments: () => req('/admin/widget-feature-assignments'),
adminPutWidgetFeatureAssignment: (widgetId, body) =>
req(`/admin/widget-feature-assignments/${encodeURIComponent(widgetId)}`, jput(body)),
// Persönliche Referenzwerte (Profil, historisch)
listReferenceValueTypes: () => req('/reference-value-types'),
listReferenceValueMetaEnums: () => req('/reference-value-meta/enums'),
listProfileReferenceValues: (typeKey) =>
req(`/profile-reference-values?type_key=${encodeURIComponent(typeKey)}`),
listProfileReferenceValuesSummary: () => req('/profile-reference-values/summary'),
createProfileReferenceValue: (d) => req('/profile-reference-values', json(d)),
updateProfileReferenceValue: (id, d) => req(`/profile-reference-values/${id}`, jput(d)),
deleteProfileReferenceValue: (id) => req(`/profile-reference-values/${id}`, { method: 'DELETE' }),
// Admin: Referenzwert-Typen (Katalog)
adminListReferenceValueTypes: () => req('/admin/reference-value-types'),
adminCreateReferenceValueType: (d) => req('/admin/reference-value-types', json(d)),
adminUpdateReferenceValueType: (id, d) => req(`/admin/reference-value-types/${id}`, jput(d)),
adminDeleteReferenceValueType: (id) => req(`/admin/reference-value-types/${id}`, { method: 'DELETE' }),
adminReorderReferenceValueTypes: (orderedIds) =>
req('/admin/reference-value-types/reorder', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ordered_ids: orderedIds }),
}),
// Weight
listWeight: (l=365) => req(`/weight?limit=${l}`),
upsertWeight: (date,weight,note='') => req('/weight',json({date,weight,note})),
updateWeight: (id,date,weight,note='') => req(`/weight/${id}`,jput({date,weight,note})),
deleteWeight: (id) => req(`/weight/${id}`,{method:'DELETE'}),
weightStats: () => req('/weight/stats'),
// Circumferences
listCirc: (l=100) => req(`/circumferences?limit=${l}`),
upsertCirc: (d) => req('/circumferences',json(d)),
updateCirc: (id,d) => req(`/circumferences/${id}`,jput(d)),
deleteCirc: (id) => req(`/circumferences/${id}`,{method:'DELETE'}),
// Caliper
listCaliper: (l=100) => req(`/caliper?limit=${l}`),
upsertCaliper: (d) => req('/caliper',json(d)),
updateCaliper: (id,d) => req(`/caliper/${id}`,jput(d)),
deleteCaliper: (id) => req(`/caliper/${id}`,{method:'DELETE'}),
// Activity
/**
* @param {number} [limit=200]
* @param {number} [days] nur Einträge ab HEUTEdays (Kalendertage), backend-filtert
* @param {{ offset?: number, skipQualityFilter?: boolean, month?: string, collapseDuplicateSessions?: boolean }} [opts] month = YYYY-MM (schließt days/offset aus)
*/
listActivity: (limit=200, days, opts={})=> {
const q = new URLSearchParams({ limit: String(limit) })
if (days != null && days !== '') q.set('days', String(days))
if (opts.month) q.set('month', String(opts.month))
if (opts.offset != null && opts.offset > 0) q.set('offset', String(opts.offset))
if (opts.collapseDuplicateSessions) q.set('collapse_duplicate_sessions', 'true')
if (opts.skipQualityFilter) q.set('skip_quality_filter', 'true')
return req(`/activity?${q}`)
},
createActivity: (d) => req('/activity',json(d)),
updateActivity: (id,d) => req(`/activity/${id}`,jput(d)),
deleteActivity: (id) => req(`/activity/${id}`,{method:'DELETE'}),
/** @param {{ skipQualityFilter?: boolean }} [opts] */
activityStats: (opts={}) => {
const q = new URLSearchParams()
if (opts.skipQualityFilter) q.set('skip_quality_filter', 'true')
const qs = q.toString()
return req(`/activity/stats${qs ? `?${qs}` : ''}`)
},
listUncategorizedActivities: () => req('/activity/uncategorized'),
bulkCategorizeActivities: (d) => req('/activity/bulk-categorize', json(d)),
importActivityCsv: async(file)=>{
const fd=new FormData();fd.append('file',file)
const r=await fetch(`${BASE}/activity/import-csv`,{method:'POST',body:fd,headers:hdrs()})
const d=await r.json();if(!r.ok)throw new Error(formatFastApiDetail(d.detail, JSON.stringify(d)));return d
},
// Photos
uploadPhoto: async (file, date = '') => {
const fd = new FormData()
fd.append('file', file)
fd.append('date', date)
const r = await fetch(`${BASE}/photos`, { method: 'POST', body: fd, headers: hdrs() })
const d = await r.json().catch(() => ({}))
if (!r.ok) throw new Error(formatFastApiDetail(d.detail, JSON.stringify(d)))
return d
},
listPhotos: () => req('/photos'),
deletePhoto: (id) => req(`/photos/${id}`, { method: 'DELETE' }),
/** Bild-URL; Query heißt `ssetoken` (require_auth_flexible), nicht `token`. */
photoUrl: (pid) => {
const token = getToken()
return `${BASE}/photos/${pid}${token ? `?ssetoken=${encodeURIComponent(token)}` : ''}`
},
// Nutrition
importCsv: async(file)=>{
const fd=new FormData();fd.append('file',file)
const r=await fetch(`${BASE}/nutrition/import-csv`,{method:'POST',body:fd,headers:hdrs()})
const d=await r.json();if(!r.ok)throw new Error(formatFastApiDetail(d.detail, JSON.stringify(d)));return d
},
listNutrition: (l=365) => req(`/nutrition?limit=${l}`),
nutritionCorrelations: () => req('/nutrition/correlations'),
nutritionWeekly: (w=16) => req(`/nutrition/weekly?weeks=${w}`),
nutritionImportHistory: () => req('/nutrition/import-history'),
getNutritionByDate: (date) => req(`/nutrition/by-date/${date}`),
createNutrition: (date,kcal,protein,fat,carbs) => req(`/nutrition?date=${date}&kcal=${kcal}&protein_g=${protein}&fat_g=${fat}&carbs_g=${carbs}`,{method:'POST'}),
updateNutrition: (id,kcal,protein,fat,carbs) => req(`/nutrition/${id}?kcal=${kcal}&protein_g=${protein}&fat_g=${fat}&carbs_g=${carbs}`,{method:'PUT'}),
deleteNutrition: (id) => req(`/nutrition/${id}`,{method:'DELETE'}),
// Stats & AI
getStats: () => req('/stats'),
insightTrend: () => req('/insights/trend',{method:'POST'}),
listPrompts: () => req('/prompts'),
runInsight: (slug) => req(`/insights/run/${slug}`,{method:'POST'}),
insightPipeline: () => req('/insights/pipeline',{method:'POST'}),
listInsights: () => req('/insights'),
latestInsights: () => req('/insights/latest'),
deleteInsight: (id) => req(`/insights/${id}`, {method:'DELETE'}),
exportZip: async () => {
const res = await fetch(`${BASE}/export/zip`, {headers: hdrs()})
if (!res.ok) throw new Error('Export failed')
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `mitai-export-${new Date().toISOString().split('T')[0]}.zip`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
},
exportJson: async () => {
const res = await fetch(`${BASE}/export/json`, {headers: hdrs()})
if (!res.ok) throw new Error('Export failed')
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `mitai-export-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
},
exportCsv: async () => {
const res = await fetch(`${BASE}/export/csv`, {headers: hdrs()})
if (!res.ok) throw new Error('Export failed')
const blob = await res.blob()
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `mitai-export-${new Date().toISOString().split('T')[0]}.csv`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
window.URL.revokeObjectURL(url)
},
// Admin
adminListProfiles: () => req('/admin/profiles'),
adminCreateProfile: (d) => req('/admin/profiles',json(d)),
adminDeleteProfile: (id) => req(`/admin/profiles/${id}`,{method:'DELETE'}),
adminSetPermissions: (id,d) => req(`/admin/profiles/${id}/permissions`,jput(d)),
changePin: (pin) => req('/auth/pin',json({pin})),
register: (name,email,password) => req('/auth/register',json({name,email,password})),
verifyEmail: (token) => req(`/auth/verify/${token}`),
resendVerification: (email) => req('/auth/resend-verification',json({email})),
// v9c Subscription System
// User-facing
getMySubscription: () => req('/subscription/me'),
getMyUsage: () => req('/subscription/usage'),
getMyLimits: () => req('/subscription/limits'),
redeemCoupon: (code) => req('/coupons/redeem',json({code})),
getFeatureUsage: () => req('/features/usage'), // Phase 3: Usage overview
// Admin: Features
listFeatures: () => req('/features'),
createFeature: (d) => req('/features',json(d)),
updateFeature: (id,d) => req(`/features/${id}`,jput(d)),
deleteFeature: (id) => req(`/features/${id}`,{method:'DELETE'}),
// Admin: Tiers
listTiers: () => req('/tiers'),
createTier: (d) => req('/tiers',json(d)),
updateTier: (id,d) => req(`/tiers/${id}`,jput(d)),
deleteTier: (id) => req(`/tiers/${id}`,{method:'DELETE'}),
// Admin: Tier Limits (Matrix)
getTierLimitsMatrix: () => req('/tier-limits'),
updateTierLimit: (d) => req('/tier-limits',jput(d)),
updateTierLimitsBatch:(updates) => req('/tier-limits/batch',jput({updates})),
// Admin: User Restrictions
listUserRestrictions: (pid) => req(`/user-restrictions${pid?'?profile_id='+pid:''}`),
createUserRestriction:(d) => req('/user-restrictions',json(d)),
updateUserRestriction:(id,d) => req(`/user-restrictions/${id}`,jput(d)),
deleteUserRestriction:(id) => req(`/user-restrictions/${id}`,{method:'DELETE'}),
// Admin: Coupons
listCoupons: () => req('/coupons'),
createCoupon: (d) => req('/coupons',json(d)),
updateCoupon: (id,d) => req(`/coupons/${id}`,jput(d)),
deleteCoupon: (id) => req(`/coupons/${id}`,{method:'DELETE'}),
getCouponRedemptions: (id) => req(`/coupons/${id}/redemptions`),
// Admin: Access Grants
listAccessGrants: (pid,active)=> req(`/access-grants${pid?'?profile_id='+pid:''}${active?'&active_only=true':''}`),
createAccessGrant: (d) => req('/access-grants',json(d)),
updateAccessGrant: (id,d) => req(`/access-grants/${id}`,jput(d)),
revokeAccessGrant: (id) => req(`/access-grants/${id}`,{method:'DELETE'}),
// v9d: Training Types
listTrainingTypes: () => req('/training-types'), // Grouped by category
listTrainingTypesFlat:() => req('/training-types/flat'), // Flat list
getTrainingCategories:() => req('/training-types/categories'), // Category metadata
// Admin: Training Types (v9d Phase 1b)
adminListTrainingTypes: () => req('/admin/training-types'),
adminGetTrainingType: (id) => req(`/admin/training-types/${id}`),
adminCreateTrainingType: (d) => req('/admin/training-types', json(d)),
adminUpdateTrainingType: (id,d) => req(`/admin/training-types/${id}`, jput(d)),
adminDeleteTrainingType: (id) => req(`/admin/training-types/${id}`, {method:'DELETE'}),
getAbilitiesTaxonomy: () => req('/admin/training-types/taxonomy/abilities'),
// Admin: Training Type Profiles (Phase 2 #15)
getProfileStats: () => req('/admin/training-types/profiles/stats'),
getProfileTemplates: () => req('/admin/training-types/profiles/templates'),
getProfileTemplate: (key) => req(`/admin/training-types/profiles/templates/${key}`),
applyProfileTemplate: (id,templateKey) => req(`/admin/training-types/${id}/profile/apply-template`, json({template_key: templateKey})),
getTrainingParameters: () => req('/evaluation/parameters'),
batchEvaluateActivities: () => req('/evaluation/batch', {method:'POST'}),
// Admin: Activity Type Mappings (v9d Phase 1b - Learnable System)
adminListActivityMappings: (profileId, globalOnly) => req(`/admin/activity-mappings${profileId?'?profile_id='+profileId:''}${globalOnly?'?global_only=true':''}`),
adminGetActivityMapping: (id) => req(`/admin/activity-mappings/${id}`),
adminCreateActivityMapping: (d) => req('/admin/activity-mappings', json(d)),
adminUpdateActivityMapping: (id,d) => req(`/admin/activity-mappings/${id}`, jput(d)),
adminDeleteActivityMapping: (id) => req(`/admin/activity-mappings/${id}`, {method:'DELETE'}),
adminGetMappingCoverage: () => req('/admin/activity-mappings/stats/coverage'),
// Admin: Training session metrics (EAV) & attribute profiles (Migration 054)
adminListTrainingParameters: (includeInactive = false) =>
req(`/admin/training-parameters${includeInactive ? '?include_inactive=true' : ''}`),
adminCreateTrainingParameter: (d) => req('/admin/training-parameters', json(d)),
adminUpdateTrainingParameter: (id, d) => req(`/admin/training-parameters/${id}`, jput(d)),
adminDeleteTrainingParameter: (id) =>
req(`/admin/training-parameters/${id}`, { method: 'DELETE' }),
adminListTrainingCategoryParameters: (category = '') =>
req(
`/admin/training-category-parameters${category ? `?category=${encodeURIComponent(category)}` : ''}`,
),
adminAddTrainingCategoryParameter: (d) => req('/admin/training-category-parameters', json(d)),
adminUpdateTrainingCategoryParameter: (id, d) =>
req(`/admin/training-category-parameters/${id}`, jput(d)),
adminDeleteTrainingCategoryParameter: (id) =>
req(`/admin/training-category-parameters/${id}`, { method: 'DELETE' }),
adminListTrainingTypeParameters: (trainingTypeId) =>
req(`/admin/training-type-parameters?training_type_id=${encodeURIComponent(trainingTypeId)}`),
adminAddTrainingTypeParameter: (d) => req('/admin/training-type-parameters', json(d)),
adminUpdateTrainingTypeParameter: (id, d) =>
req(`/admin/training-type-parameters/${id}`, jput(d)),
adminDeleteTrainingTypeParameter: (id) =>
req(`/admin/training-type-parameters/${id}`, { method: 'DELETE' }),
/**
* @param {string} id
* @param {{ useFormSchema?: boolean, training_category?: string | null, training_type_id?: number | null }} [opts]
*/
getActivitySession: (id, opts = {}) => {
const q = new URLSearchParams()
if (opts.useFormSchema) {
q.set('use_form_schema', 'true')
if (opts.training_category != null && opts.training_category !== '') {
q.set('training_category', String(opts.training_category))
}
if (opts.training_type_id != null && opts.training_type_id !== '') {
q.set('training_type_id', String(opts.training_type_id))
}
}
const qs = q.toString()
return req(`/activity/${encodeURIComponent(id)}${qs ? `?${qs}` : ''}`)
},
/**
* Attributprofil ohne Session (manuelle Erfassung / Vorschau).
* @param {{ training_category?: string | null, training_type_id?: number | null }} [params]
*/
getActivityAttributeSchema: (params = {}) => {
const q = new URLSearchParams()
if (params.training_category != null && params.training_category !== '') {
q.set('training_category', String(params.training_category))
}
if (params.training_type_id != null && params.training_type_id !== '') {
q.set('training_type_id', String(params.training_type_id))
}
const qs = q.toString()
return req(`/activity/attribute-schema${qs ? `?${qs}` : ''}`)
},
putActivityMetrics: (id, body) =>
req(`/activity/${encodeURIComponent(id)}/metrics`, jput(body)),
// Sleep Module (v9d Phase 2b)
listSleep: (l=90) => req(`/sleep?limit=${l}`),
getSleepByDate: (date) => req(`/sleep/by-date/${date}`),
createSleep: (d) => req('/sleep', json(d)),
updateSleep: (id,d) => req(`/sleep/${id}`, jput(d)),
deleteSleep: (id) => req(`/sleep/${id}`, {method:'DELETE'}),
getSleepStats: (days=7) => req(`/sleep/stats?days=${days}`),
getSleepDebt: (days=14) => req(`/sleep/debt?days=${days}`),
getSleepTrend: (days=30) => req(`/sleep/trend?days=${days}`),
getSleepPhases: (days=30) => req(`/sleep/phases?days=${days}`),
// Sleep Import (v9d Phase 2c)
importAppleHealthSleep: (file) => {
const fd = new FormData()
fd.append('file', file)
return req('/sleep/import/apple-health', {method:'POST', body:fd})
},
// Rest Days (v9d Phase 2a)
listRestDays: (l=90) => req(`/rest-days?limit=${l}`),
createRestDay: (d) => req('/rest-days', json(d)),
getRestDay: (id) => req(`/rest-days/${id}`),
updateRestDay: (id,d) => req(`/rest-days/${id}`, jput(d)),
deleteRestDay: (id) => req(`/rest-days/${id}`, {method:'DELETE'}),
getRestDaysStats: (weeks=4) => req(`/rest-days/stats?weeks=${weeks}`),
validateActivity: (date, activityType) => req('/rest-days/validate-activity', json({date, activity_type: activityType})),
// Vitals Baseline (v9d Phase 2d Refactored - once daily, morning)
listBaseline: (l=90) => req(`/vitals/baseline?limit=${l}`),
getBaselineByDate: (date) => req(`/vitals/baseline/by-date/${date}`),
createBaseline: (d) => req('/vitals/baseline', json(d)),
updateBaseline: (id,d) => req(`/vitals/baseline/${id}`, jput(d)),
deleteBaseline: (id) => req(`/vitals/baseline/${id}`, {method:'DELETE'}),
getBaselineStats: (days=30) => req(`/vitals/baseline/stats?days=${days}`),
importBaselineAppleHealth: (file) => {
const fd = new FormData()
fd.append('file', file)
return req('/vitals/baseline/import/apple-health', {method:'POST', body:fd})
},
// Blood Pressure (v9d Phase 2d Refactored - multiple daily, context-aware)
listBloodPressure: (l=90) => req(`/blood-pressure?limit=${l}`),
getBPByDate: (date) => req(`/blood-pressure/by-date/${date}`),
createBloodPressure:(d) => req('/blood-pressure', json(d)),
updateBloodPressure:(id,d) => req(`/blood-pressure/${id}`, jput(d)),
deleteBloodPressure:(id) => req(`/blood-pressure/${id}`, {method:'DELETE'}),
getBPStats: (days=30) => req(`/blood-pressure/stats?days=${days}`),
importBPOmron: (file) => {
const fd = new FormData()
fd.append('file', file)
return req('/blood-pressure/import/omron', {method:'POST', body:fd})
},
// AI Prompts Management (Issue #28)
listAdminPrompts: () => req('/prompts'),
getPrompt: (id) => req(`/prompts/${id}`),
createPrompt: (d) => req('/prompts', json(d)),
updatePrompt: (id,d) => req(`/prompts/${id}`, jput(d)),
deletePrompt: (id) => req(`/prompts/${id}`, {method:'DELETE'}),
duplicatePrompt: (id) => req(`/prompts/${id}/duplicate`, json({})),
reorderPrompts: (order) => req('/prompts/reorder', jput(order)),
previewPrompt: (tpl) => req('/prompts/preview', json({template:tpl})),
generatePrompt: (d) => req('/prompts/generate', json(d)),
optimizePrompt: (id) => req(`/prompts/${id}/optimize`, json({})),
listPlaceholders: () => req('/prompts/placeholders'),
resetPromptToDefault: (id) => req(`/prompts/${id}/reset-to-default`, json({})),
// Pipeline Configs Management (Issue #28 Phase 2)
listPipelineConfigs: () => req('/prompts/pipeline-configs'),
createPipelineConfig: (d) => req('/prompts/pipeline-configs', json(d)),
updatePipelineConfig: (id,d) => req(`/prompts/pipeline-configs/${id}`, jput(d)),
deletePipelineConfig: (id) => req(`/prompts/pipeline-configs/${id}`, {method:'DELETE'}),
setDefaultPipelineConfig: (id) => req(`/prompts/pipeline-configs/${id}/set-default`, json({})),
// Pipeline Execution (Issue #28 Phase 2)
executePipeline: (configId=null) => req('/insights/pipeline' + (configId ? `?config_id=${configId}` : ''), json({})),
// Unified Prompt System (Issue #28 Phase 2)
executeUnifiedPrompt: (slug, modules=null, timeframes=null, debug=false, save=false) => {
const params = new URLSearchParams({ prompt_slug: slug })
if (debug) params.append('debug', 'true')
if (save) params.append('save', 'true')
const body = {}
if (modules) body.modules = modules
if (timeframes) body.timeframes = timeframes
return req('/prompts/execute?' + params, json(body))
},
// NEW: SSE-based execution for long-running workflows
executeUnifiedPromptStream: (slug, modules=null, timeframes=null, debug=false, save=false, onProgress=null) => {
const params = new URLSearchParams({ prompt_slug: slug })
if (debug) params.append('debug', 'true')
if (save) params.append('save', 'true')
// TODO: Security improvement - use session cookie instead of token in URL
// For now, send token as query param since EventSource doesn't support custom headers
// Using 'ssetoken' to avoid conflicts with endpoint 'token' parameters
const token = getToken()
if (token) params.append('ssetoken', token)
if (modules) {
Object.entries(modules).forEach(([k, v]) => params.append(`modules[${k}]`, v))
}
if (timeframes) {
Object.entries(timeframes).forEach(([k, v]) => params.append(`timeframes[${k}]`, v))
}
// Return a Promise that resolves with final result
return new Promise((resolve, reject) => {
const url = `${BASE}/prompts/execute-stream?${params}`
const eventSource = new EventSource(url)
let finalResult = null
eventSource.onmessage = (event) => {
try {
const data = JSON.parse(event.data)
// Call progress callback if provided
if (onProgress) {
onProgress(data)
}
// Check for final result (/execute-stream liefert volles POST-/execute-Payload unter result)
if (data.type === 'execution_complete') {
finalResult = data.result
? data.result
: {
type: 'workflow',
execution_id: data.execution_id,
status: data.status,
aggregated_result: data.aggregated_result,
debug: { node_states: [] },
}
eventSource.close()
resolve(finalResult)
} else if (data.type === 'execution_failed') {
eventSource.close()
reject(new Error(data.error || 'Workflow execution failed'))
}
} catch (e) {
console.error('Error parsing SSE event:', e)
}
}
eventSource.onerror = (error) => {
console.error('SSE connection error:', error)
eventSource.close()
reject(new Error('Connection to server lost'))
}
})
},
// Workflow Execution (Part 2: Frontend Execute Integration)
executeWorkflow: (slug, variables=null, debug=true, save=false) => {
const params = new URLSearchParams({ prompt_slug: slug })
if (debug) params.append('debug', 'true')
if (save) params.append('save', 'true')
const body = variables ? { variables } : {}
return req('/prompts/execute?' + params, json(body))
},
createUnifiedPrompt: (d) => req('/prompts/unified', json(d)),
updateUnifiedPrompt: (id,d) => req(`/prompts/unified/${id}`, jput(d)),
// Batch Import/Export
exportAllPrompts: () => req('/prompts/export-all'),
importPrompts: (data, overwrite=false) => {
const params = new URLSearchParams()
if (overwrite) params.append('overwrite', 'true')
return req(`/prompts/import?${params}`, json(data))
},
// Placeholder Export
exportPlaceholderValues: () => req('/prompts/placeholders/export-values'),
// v9e: Goals System (Strategic + Tactical)
getGoalMode: () => req('/goals/mode'),
updateGoalMode: (mode) => req('/goals/mode', jput({goal_mode: mode})),
// Focus Areas (v2.0)
getFocusAreas: () => req('/goals/focus-areas'),
updateFocusAreas: (d) => req('/goals/focus-areas', jput(d)),
listGoals: () => req('/goals/list'),
listGoalsGrouped: () => req('/goals/grouped'),
createGoal: (d) => req('/goals/create', json(d)),
updateGoal: (id,d) => req(`/goals/${id}`, jput(d)),
deleteGoal: (id) => req(`/goals/${id}`, {method:'DELETE'}),
// Goal Progress (v2.1)
listGoalProgress: (id) => req(`/goals/${id}/progress`),
createGoalProgress: (id,d) => req(`/goals/${id}/progress`, json(d)),
deleteGoalProgress: (gid,pid) => req(`/goals/${gid}/progress/${pid}`, {method:'DELETE'}),
// Goal Type Definitions (Phase 1.5)
listGoalTypeDefinitions: () => req('/goals/goal-types'),
createGoalType: (d) => req('/goals/goal-types', json(d)),
updateGoalType: (id,d) => req(`/goals/goal-types/${id}`, jput(d)),
deleteGoalType: (id) => req(`/goals/goal-types/${id}`, {method:'DELETE'}),
getSchemaInfo: () => req('/goals/schema-info'),
// Training Phases
listTrainingPhases: () => req('/goals/phases'),
createTrainingPhase: (d) => req('/goals/phases', json(d)),
updatePhaseStatus: (id,status) => req(`/goals/phases/${id}/status?status=${status}`, jput({})),
// Fitness Tests
listFitnessTests: () => req('/goals/tests'),
createFitnessTest: (d) => req('/goals/tests', json(d)),
// Focus Areas (v2.0)
listFocusAreaDefinitions: (includeInactive=false) => req(`/focus-areas/definitions?include_inactive=${includeInactive}`),
createFocusAreaDefinition: (d) => req('/focus-areas/definitions', json(d)),
updateFocusAreaDefinition: (id,d) => req(`/focus-areas/definitions/${id}`, jput(d)),
deleteFocusAreaDefinition: (id) => req(`/focus-areas/definitions/${id}`, {method:'DELETE'}),
getUserFocusPreferences: () => req('/focus-areas/user-preferences'),
updateUserFocusPreferences: (d) => req('/focus-areas/user-preferences', jput(d)),
getFocusAreaStats: () => req('/focus-areas/stats'),
listFocusAreaUsageTypes: () => req('/focus-areas/usage-types'),
setFocusAreaUsageTypes: (id, usageTypeKeys) =>
req(`/focus-areas/definitions/${id}/usage-types`, jput({ usage_type_keys: usageTypeKeys })),
// Chart Endpoints (Phase 0c - Phase 1: Nutrition + Recovery)
// Nutrition Charts (E1-E5)
getEnergyBalanceChart: (days=28) => req(`/charts/energy-balance?days=${days}`),
getProteinAdequacyChart: (days=28) => req(`/charts/protein-adequacy?days=${days}`),
getNutritionConsistencyChart: (days=28) => req(`/charts/nutrition-consistency?days=${days}`),
getWeeklyMacroDistributionChart: (weeks=12) => req(`/charts/weekly-macro-distribution?weeks=${weeks}`),
getNutritionAdherenceScore: (days=28) => req(`/charts/nutrition-adherence-score?days=${days}`),
getEnergyAvailabilityWarning: (days=14) => req(`/charts/energy-availability-warning?days=${days}`),
// Recovery Charts (R1-R5)
getRecoveryScoreChart: (days=28) => req(`/charts/recovery-score?days=${days}`),
getHrvRhrBaselineChart: (days=28) => req(`/charts/hrv-rhr-baseline?days=${days}`),
getSleepDurationQualityChart: (days=28) => req(`/charts/sleep-duration-quality?days=${days}`),
getSleepDebtChart: (days=28) => req(`/charts/sleep-debt?days=${days}`),
getVitalSignsMatrixChart: (days=7) => req(`/charts/vital-signs-matrix?days=${days}`),
// Placeholder Metadata Export (v1.0)
exportPlaceholdersExtendedJson: () => req('/prompts/placeholders/export-values-extended'),
// Universal CSV Import (Issue #21)
getCsvModules: () => req('/csv/modules'),
getCsvLimits: () => req('/csv/limits'),
getCsvMappings: (module = null) =>
req(module ? `/csv/mappings?module=${encodeURIComponent(module)}` : '/csv/mappings'),
copyCsvMapping: (mappingId, body = null) =>
req(`/csv/mappings/${mappingId}/copy`, body ? json(body) : { method: 'POST' }),
/** Universal-CSV (Issue #21): Zielmodul steckt in der Vorlage; nur file + mapping_id */
/** Import-Diagnose: keine Datenbank-Schreibung, erste Zeilen + Mapping-Auflösung */
diagnoseUniversalCsv: async (file, mappingId, module = null) => {
const fd = new FormData()
fd.append('file', file)
fd.append('mapping_id', String(mappingId))
if (module) fd.append('module', module)
const res = await fetch(BASE + '/csv/import-diagnose', { method: 'POST', headers: hdrs(), body: fd })
if (!res.ok) {
const errText = await res.text()
let parsed = null
try {
parsed = JSON.parse(errText)
} catch { /* ignore */ }
throw new Error(formatFastApiDetail(parsed?.detail, errText.trim() || `HTTP ${res.status}`))
}
return res.json()
},
importUniversalCsv: async (file, mappingId) => {
const fd = new FormData()
fd.append('file', file)
fd.append('mapping_id', String(mappingId))
const res = await fetch(BASE + '/csv/import', { method: 'POST', headers: hdrs(), body: fd })
if (!res.ok) {
const errText = await res.text()
let parsed = null
try {
parsed = JSON.parse(errText)
} catch { /* ignore */ }
throw new Error(formatFastApiDetail(parsed?.detail, errText.trim() || `HTTP ${res.status}`))
}
return res.json()
},
/** optional module = nur diese Vorlagen-Gruppe (Abwärtskompatibilität); ohne = globale Erkennung */
analyzeCsv: async (file, module = null, delimiter = null) => {
const fd = new FormData()
fd.append('file', file)
if (module) fd.append('module', module)
if (delimiter) fd.append('delimiter', delimiter)
const res = await fetch(BASE + '/csv/analyze', { method: 'POST', headers: hdrs(), body: fd })
if (!res.ok) {
const errText = await res.text()
let parsed = null
try {
parsed = JSON.parse(errText)
} catch { /* ignore */ }
throw new Error(formatFastApiDetail(parsed?.detail, errText.trim() || `HTTP ${res.status}`))
}
return res.json()
},
adminListCsvTemplates: (module = null) =>
req(module ? `/admin/csv-templates?module=${encodeURIComponent(module)}` : '/admin/csv-templates'),
/** CSV für Admin-Editor analysieren (Vorschlags-Mappings, wie Nutzer-Import) */
adminAnalyzeCsvTemplate: async (file, module, delimiter = null, seedTemplateId = null) => {
const fd = new FormData()
fd.append('file', file)
fd.append('module', module)
if (delimiter) fd.append('delimiter', delimiter)
if (seedTemplateId != null && seedTemplateId !== '') fd.append('seed_template_id', String(seedTemplateId))
const res = await fetch(BASE + '/admin/csv-templates/analyze-upload', { method: 'POST', headers: hdrs(), body: fd })
const j = await res.json().catch(() => ({}))
if (!res.ok) throw new Error(j.detail || res.statusText || 'Analyse fehlgeschlagen')
return j
},
adminGetCsvTemplate: (id) => req(`/admin/csv-templates/${id}`),
/** Formatprüfung (ohne Speichern); body wie Create, import_row_processing optional */
adminValidateCsvTemplate: (d) => req('/admin/csv-templates/validate', json(d)),
adminCreateCsvTemplate: (d) => req('/admin/csv-templates', json(d)),
adminUpdateCsvTemplate: (id, d) => req(`/admin/csv-templates/${id}`, jput(d)),
adminDeleteCsvTemplate: (id) => req(`/admin/csv-templates/${id}`, { method: 'DELETE' }),
adminGetCsvImportLimits: () => req('/admin/csv-templates/import-limits'),
adminPutCsvImportLimits: (d) => req('/admin/csv-templates/import-limits', jput(d)),
}