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>
This commit is contained in:
parent
b7afa98639
commit
087e8dd885
|
|
@ -460,7 +460,8 @@ 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 ''
|
||||||
|
if template: # Only process if template is not empty/None
|
||||||
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:
|
||||||
|
|
@ -474,7 +475,9 @@ def analyze_placeholder_usage(profile_id: str) -> Dict[str, UsedBy]:
|
||||||
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:
|
||||||
|
|
|
||||||
|
|
@ -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:
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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'),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user