feat: batch import/export for prompts (Issue #28 Debug B)
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:
parent
8b287ca6c9
commit
7f94a41965
|
|
@ -922,3 +922,132 @@ def update_unified_prompt(prompt_id: str, p: UnifiedPromptUpdate, session: dict
|
||||||
)
|
)
|
||||||
|
|
||||||
return {"ok": True}
|
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 dev→prod 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
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -17,6 +17,8 @@ export default function AdminPromptsPage() {
|
||||||
const [error, setError] = useState(null)
|
const [error, setError] = useState(null)
|
||||||
const [editingPrompt, setEditingPrompt] = useState(null)
|
const [editingPrompt, setEditingPrompt] = useState(null)
|
||||||
const [showNewPrompt, setShowNewPrompt] = useState(false)
|
const [showNewPrompt, setShowNewPrompt] = useState(false)
|
||||||
|
const [importing, setImporting] = useState(false)
|
||||||
|
const [importResult, setImportResult] = useState(null)
|
||||||
|
|
||||||
const categories = [
|
const categories = [
|
||||||
{ id: 'all', label: 'Alle Kategorien' },
|
{ id: 'all', label: 'Alle Kategorien' },
|
||||||
|
|
@ -167,6 +169,53 @@ export default function AdminPromptsPage() {
|
||||||
return 'var(--text3)'
|
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 (
|
return (
|
||||||
<div style={{
|
<div style={{
|
||||||
padding: 20,
|
padding: 20,
|
||||||
|
|
@ -183,12 +232,31 @@ export default function AdminPromptsPage() {
|
||||||
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 600 }}>
|
<h1 style={{ margin: 0, fontSize: 24, fontWeight: 600 }}>
|
||||||
KI-Prompts ({filteredPrompts.length})
|
KI-Prompts ({filteredPrompts.length})
|
||||||
</h1>
|
</h1>
|
||||||
<button
|
<div style={{ display: 'flex', gap: 8 }}>
|
||||||
className="btn btn-primary"
|
<button
|
||||||
onClick={() => setShowNewPrompt(true)}
|
className="btn"
|
||||||
>
|
onClick={handleExportAll}
|
||||||
+ Neuer Prompt
|
title="Alle Prompts als JSON exportieren (Backup / Dev→Prod Sync)"
|
||||||
</button>
|
>
|
||||||
|
📦 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)}
|
||||||
|
>
|
||||||
|
+ Neuer Prompt
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{error && (
|
{error && (
|
||||||
|
|
@ -203,6 +271,30 @@ export default function AdminPromptsPage() {
|
||||||
</div>
|
</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 */}
|
{/* Filters */}
|
||||||
<div style={{
|
<div style={{
|
||||||
display: 'flex',
|
display: 'flex',
|
||||||
|
|
|
||||||
|
|
@ -318,4 +318,12 @@ export const api = {
|
||||||
},
|
},
|
||||||
createUnifiedPrompt: (d) => req('/prompts/unified', json(d)),
|
createUnifiedPrompt: (d) => req('/prompts/unified', json(d)),
|
||||||
updateUnifiedPrompt: (id,d) => req(`/prompts/unified/${id}`, jput(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))
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user