feat: batch import/export for prompts (Issue #28 Debug B)
All checks were successful
Deploy Development / deploy (push) Successful in 43s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 13s

Dev→Prod Sync in 2 Klicks: Export → Import

Backend:
- GET /api/prompts/export-all → JSON mit allen Prompts
- POST /api/prompts/import?overwrite=true/false → Import + Create/Update
  - Returns: created, updated, skipped counts
  - Validates JSON structure
  - Handles stages JSON conversion

Frontend AdminPromptsPage:
- Button "📦 Alle exportieren" → downloads all-prompts-{date}.json
- Button "📥 Importieren" → file upload dialog
  - User-Prompt: Überschreiben? Ja/Nein
  - Success-Message mit Statistik (created/updated/skipped)

Frontend api.js:
- exportAllPrompts()
- importPrompts(data, overwrite)

Use Cases:
1. Backup: Prompts als JSON sichern
2. Dev→Prod: Auf dev.mitai entwickeln → exportieren → auf mitai.jinkendo importieren
3. Versionierung: Prompts in Git speichern

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-03-26 09:44:08 +01:00
parent 8b287ca6c9
commit 7f94a41965
3 changed files with 235 additions and 6 deletions

View File

@ -922,3 +922,132 @@ def update_unified_prompt(prompt_id: str, p: UnifiedPromptUpdate, session: dict
)
return {"ok": True}
@router.get("/export-all")
def export_all_prompts(session: dict = Depends(require_admin)):
"""
Export all prompts as JSON array.
Admin only. Used for backup and devprod sync.
"""
from datetime import datetime
with get_db() as conn:
cur = get_cursor(conn)
cur.execute("SELECT * FROM ai_prompts ORDER BY sort_order, slug")
prompts = [r2d(row) for row in cur.fetchall()]
# Convert to export format (clean up DB-specific fields)
export_data = []
for p in prompts:
export_item = {
'slug': p['slug'],
'name': p['name'],
'display_name': p.get('display_name'),
'description': p.get('description'),
'type': p.get('type', 'pipeline'),
'category': p.get('category', 'ganzheitlich'),
'template': p.get('template'),
'stages': p.get('stages'),
'output_format': p.get('output_format', 'text'),
'output_schema': p.get('output_schema'),
'active': p.get('active', True),
'sort_order': p.get('sort_order', 0)
}
export_data.append(export_item)
return {
'export_date': datetime.now().isoformat(),
'count': len(export_data),
'prompts': export_data
}
@router.post("/import")
def import_prompts(
data: dict,
overwrite: bool = False,
session: dict = Depends(require_admin)
):
"""
Import prompts from JSON export.
Args:
data: Export data from /export-all endpoint
overwrite: If true, update existing prompts. If false, skip duplicates.
Returns:
Summary of import results (created, updated, skipped)
"""
if 'prompts' not in data:
raise HTTPException(400, "Invalid import data: missing 'prompts' key")
prompts = data['prompts']
created = 0
updated = 0
skipped = 0
errors = []
with get_db() as conn:
cur = get_cursor(conn)
for p in prompts:
slug = p.get('slug')
if not slug:
errors.append('Prompt without slug skipped')
continue
# Check if exists
cur.execute("SELECT id FROM ai_prompts WHERE slug=%s", (slug,))
existing = cur.fetchone()
if existing and not overwrite:
skipped += 1
continue
# Prepare stages JSON if present
stages_json = None
if p.get('stages'):
stages_json = json.dumps(p['stages']) if isinstance(p['stages'], list) else p['stages']
if existing:
# Update existing
cur.execute("""
UPDATE ai_prompts SET
name=%s, display_name=%s, description=%s, type=%s,
category=%s, template=%s, stages=%s, output_format=%s,
output_schema=%s, active=%s, sort_order=%s,
updated=CURRENT_TIMESTAMP
WHERE slug=%s
""", (
p.get('name'), p.get('display_name'), p.get('description'),
p.get('type', 'pipeline'), p.get('category', 'ganzheitlich'),
p.get('template'), stages_json, p.get('output_format', 'text'),
p.get('output_schema'), p.get('active', True),
p.get('sort_order', 0), slug
))
updated += 1
else:
# Create new
cur.execute("""
INSERT INTO ai_prompts (
slug, name, display_name, description, type, category,
template, stages, output_format, output_schema,
active, sort_order, created, updated
) VALUES (%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,CURRENT_TIMESTAMP,CURRENT_TIMESTAMP)
""", (
slug, p.get('name'), p.get('display_name'), p.get('description'),
p.get('type', 'pipeline'), p.get('category', 'ganzheitlich'),
p.get('template'), stages_json, p.get('output_format', 'text'),
p.get('output_schema'), p.get('active', True), p.get('sort_order', 0)
))
created += 1
conn.commit()
return {
'success': True,
'created': created,
'updated': updated,
'skipped': skipped,
'errors': errors if errors else None
}

View File

@ -17,6 +17,8 @@ export default function AdminPromptsPage() {
const [error, setError] = useState(null)
const [editingPrompt, setEditingPrompt] = useState(null)
const [showNewPrompt, setShowNewPrompt] = useState(false)
const [importing, setImporting] = useState(false)
const [importResult, setImportResult] = useState(null)
const categories = [
{ id: 'all', label: 'Alle Kategorien' },
@ -167,6 +169,53 @@ export default function AdminPromptsPage() {
return 'var(--text3)'
}
const handleExportAll = async () => {
try {
const data = await api.exportAllPrompts()
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' })
const url = URL.createObjectURL(blob)
const a = document.createElement('a')
a.href = url
a.download = `all-prompts-${new Date().toISOString().split('T')[0]}.json`
document.body.appendChild(a)
a.click()
document.body.removeChild(a)
URL.revokeObjectURL(url)
} catch (e) {
setError('Export-Fehler: ' + e.message)
}
}
const handleImport = async (event) => {
const file = event.target.files[0]
if (!file) return
setImporting(true)
setError(null)
setImportResult(null)
try {
const text = await file.text()
const data = JSON.parse(text)
// Ask user about overwrite
const overwrite = confirm(
'Bestehende Prompts überschreiben?\n\n' +
'JA = Existierende Prompts aktualisieren\n' +
'NEIN = Nur neue Prompts erstellen, Duplikate überspringen'
)
const result = await api.importPrompts(data, overwrite)
setImportResult(result)
await loadPrompts()
} catch (e) {
setError('Import-Fehler: ' + e.message)
} finally {
setImporting(false)
event.target.value = '' // Reset file input
}
}
return (
<div style={{
padding: 20,
@ -183,6 +232,24 @@ export default function AdminPromptsPage() {
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 600 }}>
KI-Prompts ({filteredPrompts.length})
</h1>
<div style={{ display: 'flex', gap: 8 }}>
<button
className="btn"
onClick={handleExportAll}
title="Alle Prompts als JSON exportieren (Backup / Dev→Prod Sync)"
>
📦 Alle exportieren
</button>
<label className="btn" style={{ margin: 0, cursor: 'pointer' }}>
📥 Importieren
<input
type="file"
accept=".json"
onChange={handleImport}
disabled={importing}
style={{ display: 'none' }}
/>
</label>
<button
className="btn btn-primary"
onClick={() => setShowNewPrompt(true)}
@ -190,6 +257,7 @@ export default function AdminPromptsPage() {
+ Neuer Prompt
</button>
</div>
</div>
{error && (
<div style={{
@ -203,6 +271,30 @@ export default function AdminPromptsPage() {
</div>
)}
{importResult && (
<div style={{
padding: 16,
background: '#efe',
color: '#060',
borderRadius: 8,
marginBottom: 16
}}>
<div style={{ fontWeight: 600, marginBottom: 4 }}>
Import erfolgreich
</div>
<div style={{ fontSize: 13 }}>
{importResult.created} erstellt · {importResult.updated} aktualisiert · {importResult.skipped} übersprungen
</div>
<button
onClick={() => setImportResult(null)}
style={{ marginTop: 8, fontSize: 12, padding: '4px 8px' }}
className="btn"
>
OK
</button>
</div>
)}
{/* Filters */}
<div style={{
display: 'flex',

View File

@ -318,4 +318,12 @@ export const api = {
},
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))
},
}