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/<id>/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).
Dieser Commit ist enthalten in:
Claude Code
2026-05-06 23:40:39 +00:00
Ursprung f73c21235e
Commit f4c0c930b8
2 geänderte Dateien mit 40 neuen und 2 gelöschten Zeilen

Datei anzeigen

@@ -627,16 +627,32 @@ class AgentOrchestrator:
self._queue.task_done() self._queue.task_done()
async def _mark_refresh_cancelled(self, incident_id: int): 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/<id>/pipeline aus
refresh_pipeline_steps liest."""
from database import get_db from database import get_db
from services.pipeline_tracker import cancel_active_steps
db = await get_db() db = await get_db()
try: 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( await db.execute(
"""UPDATE refresh_log SET status = 'cancelled', error_message = 'Vom Nutzer abgebrochen', """UPDATE refresh_log SET status = 'cancelled', error_message = 'Vom Nutzer abgebrochen',
completed_at = ? WHERE incident_id = ? AND status = 'running'""", 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() 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: except Exception as e:
logger.warning(f"Konnte Refresh-Log nicht als abgebrochen markieren: {e}") logger.warning(f"Konnte Refresh-Log nicht als abgebrochen markieren: {e}")
finally: finally:

Datei anzeigen

@@ -228,3 +228,25 @@ async def error_step(db, ws_manager, *, step_id: Optional[int], refresh_log_id:
"status": "error", "status": "error",
"pass_number": pass_number, "pass_number": pass_number,
}, visibility, created_by, tenant_id) }, 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