""" Pipeline control — retry / force-node / pause / resume / state. Wraps an in-memory engine (set by main.py) and emits events on the bus so the UI live stream picks them up. """ from fastapi import APIRouter from pydantic import BaseModel from typing import Optional, Any from backend.api.api import bus from backend.events.schema import EventType router = APIRouter(prefix="/api/pipeline", tags=["pipeline"]) # In-memory engine reference (set by main.py) _ENGINE: Optional[Any] = None _PIPELINE_STATE: dict = { "state": "idle", # idle | running | paused | complete | failed "active_actions": [], "completed": 0, "failed": 0, "queued": 0, "build_id": None, } class ActionBody(BaseModel): action: str node: Optional[str] = None class ForceBody(BaseModel): action: str node: str def set_engine(engine): global _ENGINE _ENGINE = engine def get_state_snapshot(): return dict(_PIPELINE_STATE) # ----------------------------- # GET STATE # ----------------------------- @router.get("/state") def get_state(): return get_state_snapshot() # ----------------------------- # RETRY ACTION # ----------------------------- @router.post("/retry") def retry_action(body: ActionBody): bus.emit( EventType.PIPELINE_UPDATE, action=body.action, node=body.node, state="retrying", reason="manual_retry", ) # In a real engine this would re-queue the action. # For now we just record the intent. return {"status": "retry_queued", "action": body.action, "node": body.node} # ----------------------------- # FORCE NODE # ----------------------------- @router.post("/force-node") def force_node(body: ForceBody): bus.emit( EventType.PIPELINE_UPDATE, action=body.action, node=body.node, state="forced", reason="manual_force", ) return {"status": "forced", "action": body.action, "node": body.node} # ----------------------------- # PAUSE # ----------------------------- @router.post("/pause") def pause(): _PIPELINE_STATE["state"] = "paused" if _ENGINE and hasattr(_ENGINE, "pause"): _ENGINE.pause() bus.emit(EventType.PIPELINE_UPDATE, state="paused", reason="manual_pause") return {"status": "paused"} # ----------------------------- # RESUME # ----------------------------- @router.post("/resume") def resume(): _PIPELINE_STATE["state"] = "running" if _ENGINE and hasattr(_ENGINE, "resume"): _ENGINE.resume() bus.emit(EventType.PIPELINE_UPDATE, state="running", reason="manual_resume") return {"status": "running"} # ----------------------------- # Legacy wrapper for api.py facade # ----------------------------- def pipeline_control_endpoint(action, payload=None, bus=None): """Legacy dispatcher — `action` is one of: retry, force, pause, resume, state.""" if action == "retry": return retry_action(ActionBody(**(payload or {}))) elif action == "force": return force_node(ForceBody(**(payload or {}))) elif action == "pause": return pause() elif action == "resume": return resume() elif action == "state": return get_state_snapshot() return {"error": f"unknown action: {action}"}