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
|
||||
for prompt in prompts:
|
||||
# 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)
|
||||
|
||||
for ph_key in found_placeholders:
|
||||
|
|
@ -474,7 +475,9 @@ def analyze_placeholder_usage(profile_id: str) -> Dict[str, UsedBy]:
|
|||
if stages:
|
||||
for stage in stages:
|
||||
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)
|
||||
|
||||
for ph_key in found_placeholders:
|
||||
|
|
|
|||
|
|
@ -8,7 +8,8 @@ import json
|
|||
import uuid
|
||||
import httpx
|
||||
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 auth import require_auth, require_admin
|
||||
|
|
@ -436,6 +437,158 @@ def export_placeholder_values_extended(session: dict = Depends(require_auth)):
|
|||
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 ───────────────────────────────────────────
|
||||
|
||||
async def call_openrouter(prompt: str, max_tokens: int = 1500) -> str:
|
||||
|
|
|
|||
|
|
@ -502,6 +502,53 @@ export default function AdminPanel() {
|
|||
</Link>
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -390,4 +390,7 @@ export const api = {
|
|||
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'),
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user