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}
|
||||
|
||||
|
||||
@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 [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',
|
||||
|
|
|
|||
|
|
@ -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))
|
||||
},
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user