diff --git a/backend/placeholder_metadata_extractor.py b/backend/placeholder_metadata_extractor.py index 069fb58..9f6f376 100644 --- a/backend/placeholder_metadata_extractor.py +++ b/backend/placeholder_metadata_extractor.py @@ -460,21 +460,24 @@ def analyze_placeholder_usage(profile_id: str) -> Dict[str, UsedBy]: # Analyze each prompt for prompt in prompts: # Check template - template = prompt.get('template', '') - found_placeholders = re.findall(r'\{\{(\w+)\}\}', 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: - if ph_key not in usage_map: - usage_map[ph_key] = UsedBy() - if prompt['name'] not in usage_map[ph_key].prompts: - usage_map[ph_key].prompts.append(prompt['name']) + for ph_key in found_placeholders: + if ph_key not in usage_map: + usage_map[ph_key] = UsedBy() + if prompt['name'] not in usage_map[ph_key].prompts: + usage_map[ph_key].prompts.append(prompt['name']) # Check stages (pipeline prompts) stages = prompt.get('stages') 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: diff --git a/backend/routers/prompts.py b/backend/routers/prompts.py index dc4e413..157b396 100644 --- a/backend/routers/prompts.py +++ b/backend/routers/prompts.py @@ -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: ", + "```", + "", + "Returns complete metadata for all 116 placeholders.", + "", + "### ZIP Export (Admin)", + "", + "```", + "GET /api/prompts/placeholders/export-catalog-zip", + "Header: X-Auth-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: diff --git a/frontend/src/pages/AdminPanel.jsx b/frontend/src/pages/AdminPanel.jsx index 480bc97..b288158 100644 --- a/frontend/src/pages/AdminPanel.jsx +++ b/frontend/src/pages/AdminPanel.jsx @@ -502,6 +502,53 @@ export default function AdminPanel() { + + {/* Placeholder Metadata Export Section */} +
+
+ Placeholder Metadata Export (v1.0) +
+
+ Exportiere vollständige Metadaten aller 116 Placeholders. Normative Compliance v1.0.0. +
+
+ + +
+
+ JSON: Maschinenlesbare Metadaten aller Placeholders
+ ZIP: Katalog (JSON + MD), Gap Report, Export Spec (4 Dateien) +
+
) } diff --git a/frontend/src/utils/api.js b/frontend/src/utils/api.js index 9df5ef3..aa59ffc 100644 --- a/frontend/src/utils/api.js +++ b/frontend/src/utils/api.js @@ -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'), }