From f4c0c930b80ef7ae4efe0526e9ae8cb48a9a78f1 Mon Sep 17 00:00:00 2001 From: Claude Code Date: Wed, 6 May 2026 23:40:39 +0000 Subject: [PATCH] fix(orchestrator): aktive Pipeline-Schritte beim Cancel mitschliessen Beim User-Cancel wurde nur refresh_log auf cancelled gesetzt, der zuletzt aktive refresh_pipeline_steps-Eintrag blieb verwaist. Der /api/incidents//pipeline-Endpoint liefert daraus dauerhaft "Schritt X laeuft" an die UI, auch lange nach dem Cancel. - pipeline_tracker.cancel_active_steps(): neuer Bulk-Helper, setzt alle noch active-Schritte eines refresh_log_id auf cancelled mit completed_at - _mark_refresh_cancelled holt die refresh_log_id, macht das refresh_log- Update wie bisher und ruft danach cancel_active_steps auf Reproduziert bei Lage 80 (Bjoern Hoecke), refresh_log 1273. Frontend- CSS kennt status-cancelled nicht, faellt auf den neutralen Default-Style zurueck (kein Spinner mehr, kein Haken, korrekt ent-hangen). --- src/agents/orchestrator.py | 20 ++++++++++++++++++-- src/services/pipeline_tracker.py | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 2 deletions(-) diff --git a/src/agents/orchestrator.py b/src/agents/orchestrator.py index ed85947..225a666 100644 --- a/src/agents/orchestrator.py +++ b/src/agents/orchestrator.py @@ -627,16 +627,32 @@ class AgentOrchestrator: self._queue.task_done() async def _mark_refresh_cancelled(self, incident_id: int): - """Markiert den laufenden Refresh-Log-Eintrag als cancelled.""" + """Markiert den laufenden Refresh-Log-Eintrag als cancelled und schliesst + alle noch aktiven Pipeline-Schritte. Ohne den zweiten Schritt blieb der + zuletzt aktive Step-Eintrag verwaist und das Frontend zeigte dauerhaft + 'Schritt X laeuft', weil /api/incidents//pipeline aus + refresh_pipeline_steps liest.""" from database import get_db + from services.pipeline_tracker import cancel_active_steps db = await get_db() try: + now_str = datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S') + cur = await db.execute( + "SELECT id FROM refresh_log WHERE incident_id = ? AND status = 'running'", + (incident_id,), + ) + row = await cur.fetchone() + refresh_log_id = row["id"] if row else None + await db.execute( """UPDATE refresh_log SET status = 'cancelled', error_message = 'Vom Nutzer abgebrochen', completed_at = ? WHERE incident_id = ? AND status = 'running'""", - (datetime.now(TIMEZONE).strftime('%Y-%m-%d %H:%M:%S'), incident_id), + (now_str, incident_id), ) await db.commit() + + if refresh_log_id is not None: + await cancel_active_steps(db, refresh_log_id=refresh_log_id) except Exception as e: logger.warning(f"Konnte Refresh-Log nicht als abgebrochen markieren: {e}") finally: diff --git a/src/services/pipeline_tracker.py b/src/services/pipeline_tracker.py index 5ec0e82..d192964 100644 --- a/src/services/pipeline_tracker.py +++ b/src/services/pipeline_tracker.py @@ -228,3 +228,25 @@ async def error_step(db, ws_manager, *, step_id: Optional[int], refresh_log_id: "status": "error", "pass_number": pass_number, }, visibility, created_by, tenant_id) + + +async def cancel_active_steps(db, *, refresh_log_id: int) -> int: + """Schliesst alle noch aktiven Pipeline-Schritte eines Refreshs als 'cancelled' ab. + + Wird vom Orchestrator nach einem User-Cancel aufgerufen. Ohne diesen Schritt + bleibt der zuletzt aktive Step-Eintrag verwaist und der Pipeline-Endpoint + liefert dauerhaft 'Schritt X laeuft' an die UI. + """ + try: + cur = await db.execute( + """UPDATE refresh_pipeline_steps + SET status = 'cancelled', completed_at = ? + WHERE refresh_log_id = ? AND status = 'active'""", + (_now_db(), refresh_log_id), + ) + await db.commit() + return cur.rowcount or 0 + except Exception as e: + logger.warning(f"Pipeline cancel_active_steps DB-Fehler: {e}") + return 0 +