fix: Phase 5 - Workflow save + node persistence bugs
All checks were successful
Deploy Development / deploy (push) Successful in 47s
Build Test / lint-backend (push) Successful in 0s
Build Test / build-frontend (push) Successful in 14s

KRITISCHE FIXES:

1. Backend: Workflow-Type Support
   - models.py: graph_data Feld hinzugefügt
   - models.py: slug Optional (auto-generiert)
   - prompts.py: 'workflow' in erlaubten Typen
   - prompts.py: graph_data in INSERT/UPDATE
   - prompts.py: Auto-Slug-Generierung aus Name
   - FIX: "Field required: slug" Error behoben

2. Frontend: Node-Updates Persistence
   - selectedNode sync mit nodes array (useEffect)
   - FIX: Änderungen gingen verloren (stale state)
   - FIX: Prompt-Auswahl nicht sichtbar nach Edit
   - FIX: Fallback-Strategy nicht gespeichert
   - FIX: Node-Name Änderungen nicht übernommen

BEHOBEN:
-  Save fehlgeschlagen →  Workflows speicherbar
-  Node-Name ignoriert →  Live-Update
-  Prompt verschwindet →  Bleibt sichtbar
-  Fallback nicht saved →  Persistiert

Tested: Backend API akzeptiert jetzt type='workflow'

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Lars 2026-04-04 19:17:41 +02:00
parent e3ef18674a
commit 7d22b052dd
3 changed files with 44 additions and 11 deletions

View File

@ -177,12 +177,12 @@ class StageCreate(BaseModel):
class UnifiedPromptCreate(BaseModel):
"""Create a new unified prompt (base or pipeline type)"""
"""Create a new unified prompt (base, pipeline, or workflow type)"""
name: str
slug: str
slug: Optional[str] = None # Auto-generated from name if not provided
display_name: Optional[str] = None
description: Optional[str] = None
type: str # 'base' or 'pipeline'
type: str # 'base' | 'pipeline' | 'workflow'
category: str = 'ganzheitlich'
active: bool = True
sort_order: int = 0
@ -195,6 +195,9 @@ class UnifiedPromptCreate(BaseModel):
# For pipeline prompts (multi-stage workflow)
stages: Optional[list[StageCreate]] = None # Required if type='pipeline'
# For workflow prompts (visual graph editor)
graph_data: Optional[dict] = None # Required if type='workflow'
class UnifiedPromptUpdate(BaseModel):
"""Update an existing unified prompt"""
@ -209,6 +212,7 @@ class UnifiedPromptUpdate(BaseModel):
output_format: Optional[str] = None
output_schema: Optional[dict] = None
stages: Optional[list[StageCreate]] = None
graph_data: Optional[dict] = None # For workflow type
# ── Pipeline Config Models (Issue #28) ─────────────────────────────────────

View File

@ -1380,20 +1380,26 @@ async def execute_unified_prompt(
@router.post("/unified")
def create_unified_prompt(p: UnifiedPromptCreate, session: dict = Depends(require_admin)):
"""
Create a new unified prompt (base or pipeline type).
Create a new unified prompt (base, pipeline, or workflow type).
Admin only.
"""
with get_db() as conn:
cur = get_cursor(conn)
# Auto-generate slug if not provided (for workflows)
if not p.slug:
import re
base_slug = re.sub(r'[^a-z0-9_]+', '_', p.name.lower()).strip('_')
p.slug = f"{base_slug}_{uuid.uuid4().hex[:6]}"
# Check for duplicate slug
cur.execute("SELECT id FROM ai_prompts WHERE slug=%s", (p.slug,))
if cur.fetchone():
raise HTTPException(status_code=400, detail="Slug already exists")
# Validate type
if p.type not in ['base', 'pipeline']:
raise HTTPException(status_code=400, detail="Type must be 'base' or 'pipeline'")
if p.type not in ['base', 'pipeline', 'workflow']:
raise HTTPException(status_code=400, detail="Type must be 'base', 'pipeline', or 'workflow'")
# Validate base type has template
if p.type == 'base' and not p.template:
@ -1403,6 +1409,10 @@ def create_unified_prompt(p: UnifiedPromptCreate, session: dict = Depends(requir
if p.type == 'pipeline' and not p.stages:
raise HTTPException(status_code=400, detail="Pipeline prompts require stages")
# Validate workflow type has graph_data
if p.type == 'workflow' and not p.graph_data:
raise HTTPException(status_code=400, detail="Workflow prompts require graph_data")
# Convert stages to JSONB
stages_json = None
if p.stages:
@ -1426,16 +1436,22 @@ def create_unified_prompt(p: UnifiedPromptCreate, session: dict = Depends(requir
prompt_id = str(uuid.uuid4())
# Convert graph_data to JSONB
graph_data_json = None
if p.graph_data:
graph_data_json = json.dumps(p.graph_data)
cur.execute(
"""INSERT INTO ai_prompts
(id, slug, name, display_name, description, template, category, active, sort_order,
type, stages, output_format, output_schema)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
type, stages, output_format, output_schema, graph_data)
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)""",
(
prompt_id, p.slug, p.name, p.display_name, p.description,
p.template, p.category, p.active, p.sort_order,
p.type, stages_json, p.output_format,
json.dumps(p.output_schema) if p.output_schema else None
json.dumps(p.output_schema) if p.output_schema else None,
graph_data_json
)
)
@ -1470,8 +1486,8 @@ def update_unified_prompt(prompt_id: str, p: UnifiedPromptUpdate, session: dict
updates.append('description=%s')
values.append(p.description)
if p.type is not None:
if p.type not in ['base', 'pipeline']:
raise HTTPException(status_code=400, detail="Type must be 'base' or 'pipeline'")
if p.type not in ['base', 'pipeline', 'workflow']:
raise HTTPException(status_code=400, detail="Type must be 'base', 'pipeline', or 'workflow'")
updates.append('type=%s')
values.append(p.type)
if p.category is not None:
@ -1512,6 +1528,9 @@ def update_unified_prompt(prompt_id: str, p: UnifiedPromptUpdate, session: dict
])
updates.append('stages=%s')
values.append(stages_json)
if p.graph_data is not None:
updates.append('graph_data=%s')
values.append(json.dumps(p.graph_data))
if not updates:
return {"ok": True}

View File

@ -73,6 +73,16 @@ export default function WorkflowEditorPage() {
setValidationWarnings(warnings)
}, [nodes, edges])
// Keep selectedNode in sync with nodes array (wichtig für Config Panel!)
useEffect(() => {
if (selectedNode) {
const updatedNode = nodes.find(n => n.id === selectedNode.id)
if (updatedNode && updatedNode !== selectedNode) {
setSelectedNode(updatedNode)
}
}
}, [nodes])
// Handlers
const onConnect = useCallback(