fix: workflow blueprints freely editable after creation (preserve_user_graph)

canonicalize_workflow_config was silently rebuilding any workflow with
ui.blueprint or ui.preset == still_graph from the canonical template on
every save, preflight, and read — making blueprint-based workflows
effectively read-only.

Add preserve_user_graph=True flag that skips the rebuild blocks. Only
create_workflow still uses preserve_user_graph=False so blueprints are
correctly expanded at creation time.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-07-22 11:17:32 +02:00
co-authored by Claude Sonnet 4.6
parent 4ea6c4ee9c
commit 53b19f2ba1
3 changed files with 33 additions and 28 deletions
+3
View File
@@ -527,3 +527,6 @@ Es gibt keinen `BLENDER_CINEMATIC`-Node im Workflow-Graph-System. Cinematic-Rend
### 2026-07-22 | Frontend | Editierbares Branding — public settings endpoint vor Auth nötig ### 2026-07-22 | Frontend | Editierbares Branding — public settings endpoint vor Auth nötig
Der Admin-Settings-Endpunkt (`GET /api/admin/settings`) erfordert `global_admin`-Auth. Die Login-Seite benötigt den App-Namen aber vor dem Login. Lösung: eigener öffentlicher `GET /api/admin/branding`-Endpunkt ohne Auth-Dependency, der nur `app_name` + `app_subtitle` aus `system_settings` liest. Frontend: `useBranding()`-Hook via React Query mit 5-min-Stale-Time — Query-Key `['branding']` wird nach dem Speichern aus dem Admin-Panel invalidiert, damit Sidebar und Login-Seite sofort aktualisieren. Der Admin-Settings-Endpunkt (`GET /api/admin/settings`) erfordert `global_admin`-Auth. Die Login-Seite benötigt den App-Namen aber vor dem Login. Lösung: eigener öffentlicher `GET /api/admin/branding`-Endpunkt ohne Auth-Dependency, der nur `app_name` + `app_subtitle` aus `system_settings` liest. Frontend: `useBranding()`-Hook via React Query mit 5-min-Stale-Time — Query-Key `['branding']` wird nach dem Speichern aus dem Admin-Panel invalidiert, damit Sidebar und Login-Seite sofort aktualisieren.
### 2026-07-22 | Workflow-Editor | Blueprint-Workflows waren nach jedem Speichern read-only
`canonicalize_workflow_config` in `workflow_config_utils.py` hat bei jedem Aufruf Configs mit `ui.blueprint` (z.B. `still_graph_reference`, `order_rendering`) und `ui.preset == "still_graph"` auf das kanonische Template zurückgebaut — alle Node-Parameter-Änderungen und Strukturänderungen des Users wurden lautlos verworfen. Das betraf Update, Preflight und Execution-Dispatch gleichermaßen. **Lösung:** Parameter `preserve_user_graph: bool = False` — bei `True` werden die Rebuild-Blöcke übersprungen; nur Basic-Normalisierung läuft. `update_workflow`, `_workflow_to_out`, Preflight und Execution-Dispatch rufen jetzt mit `preserve_user_graph=True` auf. `create_workflow` bleibt bei `False` (Blueprints werden beim Erstellen korrekt expandiert).
@@ -512,7 +512,7 @@ def _canonicalize_legacy_custom_config(raw: dict[str, Any]) -> dict[str, Any]:
return canonical return canonical
def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]: def canonicalize_workflow_config(raw: dict[str, Any], *, preserve_user_graph: bool = False) -> dict[str, Any]:
if not isinstance(raw, dict): if not isinstance(raw, dict):
raise ValueError("Workflow config must be a JSON object") raise ValueError("Workflow config must be a JSON object")
@@ -524,32 +524,34 @@ def canonicalize_workflow_config(raw: dict[str, Any]) -> dict[str, Any]:
ui = {} ui = {}
normalized["ui"] = dict(ui) normalized["ui"] = dict(ui)
normalized["ui"].setdefault("execution_mode", "legacy") normalized["ui"].setdefault("execution_mode", "legacy")
preset = normalized["ui"].get("preset")
blueprint = normalized["ui"].get("blueprint")
if preset == "still_graph": if not preserve_user_graph:
merged_ui = dict(normalized["ui"]) preset = normalized["ui"].get("preset")
canonical = build_preset_workflow_config( blueprint = normalized["ui"].get("blueprint")
"still_graph",
_extract_render_params_from_nodes(normalized.get("nodes") or [], StepName.BLENDER_STILL),
)
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
canonical["ui"].update(merged_ui)
return canonical
if blueprint in _WORKFLOW_BLUEPRINTS: if preset == "still_graph":
merged_ui = dict(normalized["ui"]) merged_ui = dict(normalized["ui"])
canonical = build_workflow_blueprint_config(blueprint) canonical = build_preset_workflow_config(
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"]) "still_graph",
canonical["ui"].update(merged_ui) _extract_render_params_from_nodes(normalized.get("nodes") or [], StepName.BLENDER_STILL),
return canonical )
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
canonical["ui"].update(merged_ui)
return canonical
if blueprint in _WORKFLOW_STARTER_BLUEPRINTS: if blueprint in _WORKFLOW_BLUEPRINTS:
merged_ui = dict(normalized["ui"]) merged_ui = dict(normalized["ui"])
canonical = build_starter_workflow_config(_WORKFLOW_STARTER_BLUEPRINTS[blueprint]) canonical = build_workflow_blueprint_config(blueprint)
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"]) merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
canonical["ui"].update(merged_ui) canonical["ui"].update(merged_ui)
return canonical return canonical
if blueprint in _WORKFLOW_STARTER_BLUEPRINTS:
merged_ui = dict(normalized["ui"])
canonical = build_starter_workflow_config(_WORKFLOW_STARTER_BLUEPRINTS[blueprint])
merged_ui.setdefault("execution_mode", canonical["ui"]["execution_mode"])
canonical["ui"].update(merged_ui)
return canonical
return normalized return normalized
@@ -264,7 +264,7 @@ async def _build_rollout_summary(
async def _workflow_to_out(db: AsyncSession, wf: WorkflowDefinition) -> WorkflowDefinitionOut: async def _workflow_to_out(db: AsyncSession, wf: WorkflowDefinition) -> WorkflowDefinitionOut:
canonical_config = canonicalize_workflow_config(wf.config) canonical_config = canonicalize_workflow_config(wf.config, preserve_user_graph=True)
workflow_family = infer_workflow_family_from_config(canonical_config) workflow_family = infer_workflow_family_from_config(canonical_config)
supported_artifact_kinds = tuple( supported_artifact_kinds = tuple(
derive_supported_artifact_kinds_from_workflow_config(canonical_config) derive_supported_artifact_kinds_from_workflow_config(canonical_config)
@@ -408,7 +408,7 @@ def _build_workflow_preflight_for_config(
submit_prepared_workflow_tasks, submit_prepared_workflow_tasks,
) )
normalized_config = canonicalize_workflow_config(workflow_config) normalized_config = canonicalize_workflow_config(workflow_config, preserve_user_graph=True)
try: try:
workflow_context = prepare_workflow_context( workflow_context = prepare_workflow_context(
normalized_config, normalized_config,
@@ -942,7 +942,7 @@ async def update_workflow(
wf.name = body.name wf.name = body.name
if body.config is not None: if body.config is not None:
try: try:
normalized_config = canonicalize_workflow_config(body.config) normalized_config = canonicalize_workflow_config(body.config, preserve_user_graph=True)
WorkflowConfig.model_validate(normalized_config) WorkflowConfig.model_validate(normalized_config)
except (ValidationError, ValueError) as exc: except (ValidationError, ValueError) as exc:
detail = exc.errors() if isinstance(exc, ValidationError) else str(exc) detail = exc.errors() if isinstance(exc, ValidationError) else str(exc)
@@ -1033,7 +1033,7 @@ async def _dispatch_workflow_for_config(
) )
try: try:
normalized_config = canonicalize_workflow_config(workflow_config) normalized_config = canonicalize_workflow_config(workflow_config, preserve_user_graph=True)
workflow_context = prepare_workflow_context( workflow_context = prepare_workflow_context(
normalized_config, normalized_config,
context_id=context_id, context_id=context_id,