mitai-jinkendo/frontend/src/utils/api.js
Lars 087e8dd885
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 19s
feat: Add Placeholder Metadata Export to Admin Panel
Adds download functionality for complete placeholder metadata catalog.

Backend:
- Fix: None-template handling in placeholder_metadata_extractor.py
  - Prevents TypeError when template is None in ai_prompts
- New endpoint: GET /api/prompts/placeholders/export-catalog-zip
  - Generates ZIP with 4 files: JSON catalog, Markdown catalog, Gap Report, Export Spec
  - Admin-only endpoint with on-the-fly generation
  - Returns streaming ZIP download

Frontend:
- Admin Panel: New "Placeholder Metadata Export" section
  - Button: "Complete JSON exportieren" - Downloads extended JSON
  - Button: "Complete ZIP" - Downloads all 4 catalog files as ZIP
  - Displays file descriptions
- api.js: Added exportPlaceholdersExtendedJson() function

Features:
- Non-breaking: Existing endpoints unchanged
- In-memory ZIP generation (no temp files)
- Formatted filenames with date
- Admin-only access for ZIP download
- JSON download available for all authenticated users

Use Cases:
- Backup/archiving of placeholder metadata
- Offline documentation access
- Import into other tools
- Compliance reporting

Files in ZIP:
1. PLACEHOLDER_CATALOG_EXTENDED.json - Machine-readable metadata
2. PLACEHOLDER_CATALOG_EXTENDED.md - Human-readable catalog
3. PLACEHOLDER_GAP_REPORT.md - Unresolved fields analysis
4. PLACEHOLDER_EXPORT_SPEC.md - API specification

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-29 20:37:52 +02:00

397 lines
20 KiB
JavaScript

import { getToken } from '../context/AuthContext'
let _profileId = null
export function setProfileId(id) { _profileId = id }
const BASE = '/api'
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 err = await res.text()
// Try to parse JSON error with detail field
try {
const parsed = JSON.parse(err)
throw new Error(parsed.detail || err)
} catch {
throw new Error(err)
}
}
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)),
// 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
listActivity: (l=200)=> req(`/activity?limit=${l}`),
createActivity: (d) => req('/activity',json(d)),
updateActivity: (id,d) => req(`/activity/${id}`,jput(d)),
deleteActivity: (id) => req(`/activity/${id}`,{method:'DELETE'}),
activityStats: () => req('/activity/stats'),
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(d.detail||JSON.stringify(d));return d
},
// Photos
uploadPhoto: (file,date='')=>{
const fd=new FormData();fd.append('file',file);fd.append('date',date)
return fetch(`${BASE}/photos`,{method:'POST',body:fd,headers:hdrs()}).then(r=>r.json())
},
listPhotos: () => req('/photos'),
photoUrl: (pid) => {
const token = getToken()
return `${BASE}/photos/${pid}${token ? `?token=${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(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'),
// 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'),
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))
},
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'),
// 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'),
}