feat: Add Placeholder Metadata Export to Admin Panel
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

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>
This commit is contained in:
Lars 2026-03-29 20:37:52 +02:00
parent b7afa98639
commit 087e8dd885
4 changed files with 215 additions and 9 deletions

View File

@ -460,21 +460,24 @@ def analyze_placeholder_usage(profile_id: str) -> Dict[str, UsedBy]:
# Analyze each prompt # Analyze each prompt
for prompt in prompts: for prompt in prompts:
# Check template # Check template
template = prompt.get('template', '') template = prompt.get('template') or ''
found_placeholders = re.findall(r'\{\{(\w+)\}\}', template) if template: # Only process if template is not empty/None
found_placeholders = re.findall(r'\{\{(\w+)\}\}', template)
for ph_key in found_placeholders: for ph_key in found_placeholders:
if ph_key not in usage_map: if ph_key not in usage_map:
usage_map[ph_key] = UsedBy() usage_map[ph_key] = UsedBy()
if prompt['name'] not in usage_map[ph_key].prompts: if prompt['name'] not in usage_map[ph_key].prompts:
usage_map[ph_key].prompts.append(prompt['name']) usage_map[ph_key].prompts.append(prompt['name'])
# Check stages (pipeline prompts) # Check stages (pipeline prompts)
stages = prompt.get('stages') stages = prompt.get('stages')
if stages: if stages:
for stage in stages: for stage in stages:
for stage_prompt in stage.get('prompts', []): for stage_prompt in stage.get('prompts', []):
template = stage_prompt.get('template', '') template = stage_prompt.get('template') or ''
if not template: # Skip if template is None/empty
continue
found_placeholders = re.findall(r'\{\{(\w+)\}\}', template) found_placeholders = re.findall(r'\{\{(\w+)\}\}', template)
for ph_key in found_placeholders: for ph_key in found_placeholders:

View File

@ -8,7 +8,8 @@ import json
import uuid import uuid
import httpx import httpx
from typing import Optional from typing import Optional
from fastapi import APIRouter, Depends, HTTPException from fastapi import APIRouter, Depends, HTTPException, Query
from fastapi.responses import StreamingResponse
from db import get_db, get_cursor, r2d from db import get_db, get_cursor, r2d
from auth import require_auth, require_admin from auth import require_auth, require_admin
@ -436,6 +437,158 @@ def export_placeholder_values_extended(session: dict = Depends(require_auth)):
return export_data return export_data
@router.get("/placeholders/export-catalog-zip")
def export_placeholder_catalog_zip(
token: Optional[str] = Query(None),
session: dict = Depends(require_admin)
):
"""
Export complete placeholder catalog as ZIP file.
Includes:
- PLACEHOLDER_CATALOG_EXTENDED.json
- PLACEHOLDER_CATALOG_EXTENDED.md
- PLACEHOLDER_GAP_REPORT.md
- PLACEHOLDER_EXPORT_SPEC.md
This generates the files on-the-fly and returns as ZIP.
Admin only.
"""
import io
import zipfile
from datetime import datetime
from generate_placeholder_catalog import (
generate_json_catalog,
generate_markdown_catalog,
generate_gap_report_md,
generate_export_spec_md
)
from placeholder_metadata_extractor import build_complete_metadata_registry
from generate_complete_metadata import apply_manual_corrections, generate_gap_report
profile_id = session['profile_id']
try:
# Build registry
registry = build_complete_metadata_registry(profile_id)
registry = apply_manual_corrections(registry)
gaps = generate_gap_report(registry)
# Create in-memory ZIP
zip_buffer = io.BytesIO()
with zipfile.ZipFile(zip_buffer, 'w', zipfile.ZIP_DEFLATED) as zip_file:
# Generate each file content in memory and add to ZIP
# 1. JSON Catalog
all_metadata = registry.get_all()
json_catalog = {
"schema_version": "1.0.0",
"generated_at": datetime.now().isoformat(),
"normative_standard": "PLACEHOLDER_METADATA_REQUIREMENTS_V2_NORMATIVE.md",
"total_placeholders": len(all_metadata),
"placeholders": {key: meta.to_dict() for key, meta in sorted(all_metadata.items())}
}
zip_file.writestr(
'PLACEHOLDER_CATALOG_EXTENDED.json',
json.dumps(json_catalog, indent=2, ensure_ascii=False)
)
# 2. Markdown Catalog (simplified version)
by_category = registry.get_by_category()
md_lines = [
"# Placeholder Catalog (Extended)",
"",
f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"**Total Placeholders:** {len(all_metadata)}",
"",
"## Placeholders by Category",
""
]
for category, metadata_list in sorted(by_category.items()):
md_lines.append(f"### {category} ({len(metadata_list)} placeholders)")
md_lines.append("")
for metadata in sorted(metadata_list, key=lambda m: m.key):
md_lines.append(f"- `{{{{{metadata.key}}}}}` - {metadata.description}")
md_lines.append("")
zip_file.writestr('PLACEHOLDER_CATALOG_EXTENDED.md', '\n'.join(md_lines))
# 3. Gap Report
gap_lines = [
"# Placeholder Metadata Gap Report",
"",
f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
f"**Total Placeholders:** {len(all_metadata)}",
"",
"## Gaps Summary",
""
]
for gap_type, placeholders in sorted(gaps.items()):
if placeholders:
gap_lines.append(f"### {gap_type.replace('_', ' ').title()}")
gap_lines.append(f"Count: {len(placeholders)}")
gap_lines.append("")
for ph in placeholders[:10]: # Max 10 per type
gap_lines.append(f"- {{{{{ph}}}}}")
if len(placeholders) > 10:
gap_lines.append(f"- ... and {len(placeholders) - 10} more")
gap_lines.append("")
zip_file.writestr('PLACEHOLDER_GAP_REPORT.md', '\n'.join(gap_lines))
# 4. Export Spec (simplified)
spec_lines = [
"# Placeholder Export Specification",
"",
f"**Version:** 1.0.0",
f"**Generated:** {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}",
"",
"## API Endpoints",
"",
"### Extended Export",
"",
"```",
"GET /api/prompts/placeholders/export-values-extended",
"Header: X-Auth-Token: <token>",
"```",
"",
"Returns complete metadata for all 116 placeholders.",
"",
"### ZIP Export (Admin)",
"",
"```",
"GET /api/prompts/placeholders/export-catalog-zip",
"Header: X-Auth-Token: <token>",
"```",
"",
"Returns ZIP with all catalog files.",
]
zip_file.writestr('PLACEHOLDER_EXPORT_SPEC.md', '\n'.join(spec_lines))
# Prepare ZIP for download
zip_buffer.seek(0)
filename = f"placeholder-catalog-{datetime.now().strftime('%Y-%m-%d')}.zip"
return StreamingResponse(
io.BytesIO(zip_buffer.read()),
media_type="application/zip",
headers={
"Content-Disposition": f"attachment; filename={filename}"
}
)
except Exception as e:
raise HTTPException(
status_code=500,
detail=f"Failed to generate ZIP: {str(e)}"
)
# ── KI-Assisted Prompt Engineering ─────────────────────────────────────────── # ── KI-Assisted Prompt Engineering ───────────────────────────────────────────
async def call_openrouter(prompt: str, max_tokens: int = 1500) -> str: async def call_openrouter(prompt: str, max_tokens: int = 1500) -> str:

View File

@ -502,6 +502,53 @@ export default function AdminPanel() {
</Link> </Link>
</div> </div>
</div> </div>
{/* Placeholder Metadata Export Section */}
<div className="card section-gap" style={{marginTop:16}}>
<div style={{fontWeight:700,fontSize:14,marginBottom:12,display:'flex',alignItems:'center',gap:6}}>
<Settings size={16} color="var(--accent)"/> Placeholder Metadata Export (v1.0)
</div>
<div style={{fontSize:12,color:'var(--text3)',marginBottom:12,lineHeight:1.5}}>
Exportiere vollständige Metadaten aller 116 Placeholders. Normative Compliance v1.0.0.
</div>
<div style={{display:'grid',gap:8}}>
<button className="btn btn-secondary btn-full"
onClick={async()=>{
try {
const data = await api.exportPlaceholdersExtendedJson()
const blob = new Blob([JSON.stringify(data, null, 2)], {type:'application/json'})
const url = window.URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `placeholder-metadata-extended-${new Date().toISOString().split('T')[0]}.json`
a.click()
window.URL.revokeObjectURL(url)
} catch(e) {
alert('Fehler beim Export: '+e.message)
}
}}>
📄 Complete JSON exportieren
</button>
<button className="btn btn-secondary btn-full"
onClick={async()=>{
try {
const token = localStorage.getItem('bodytrack_token')
const a = document.createElement('a')
a.href = `/api/prompts/placeholders/export-catalog-zip?token=${token}`
a.download = `placeholder-catalog-${new Date().toISOString().split('T')[0]}.zip`
a.click()
} catch(e) {
alert('Fehler beim Export: '+e.message)
}
}}>
📦 Complete ZIP (JSON + Markdown + Reports)
</button>
</div>
<div style={{fontSize:11,color:'var(--text3)',marginTop:8,lineHeight:1.5}}>
<strong>JSON:</strong> Maschinenlesbare Metadaten aller Placeholders<br/>
<strong>ZIP:</strong> Katalog (JSON + MD), Gap Report, Export Spec (4 Dateien)
</div>
</div>
</div> </div>
) )
} }

View File

@ -390,4 +390,7 @@ export const api = {
getSleepDurationQualityChart: (days=28) => req(`/charts/sleep-duration-quality?days=${days}`), getSleepDurationQualityChart: (days=28) => req(`/charts/sleep-duration-quality?days=${days}`),
getSleepDebtChart: (days=28) => req(`/charts/sleep-debt?days=${days}`), getSleepDebtChart: (days=28) => req(`/charts/sleep-debt?days=${days}`),
getVitalSignsMatrixChart: (days=7) => req(`/charts/vital-signs-matrix?days=${days}`), getVitalSignsMatrixChart: (days=7) => req(`/charts/vital-signs-matrix?days=${days}`),
// Placeholder Metadata Export (v1.0)
exportPlaceholdersExtendedJson: () => req('/prompts/placeholders/export-values-extended'),
} }