From db1729730f3bf20664e834ad1187bdcd26809e65 Mon Sep 17 00:00:00 2001 From: johnfrum1234 Date: Sun, 15 Mar 2026 15:29:05 +0200 Subject: [PATCH] Full pipeline flow through web interface with live updates MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit API: POST /api/tasks/{id}/run — sets task to in_progress immediately, launches subprocess with error handling and logging. GET /api/tasks/{id}/running — checks pipelines table for active run. Fixed --db flag position in subprocess command. TaskDetail (live pipeline): - Run button starts pipeline, auto-starts 3s polling - Pipeline cards update in real-time as agent_logs appear - Pulsing blue dot on header while in_progress - Spinner on run button during execution - Auto-stops polling when status changes from in_progress - Cleanup on component unmount (no leaked timers) ProjectView (run from list): - [>] button on each pending task row - Confirm dialog before starting - Pulsing blue dot for in_progress tasks - Click task row → /task/:id with live view Dashboard (live statuses): - Pulsing blue dot next to active task count - Auto-poll every 5s when any project has active tasks - Stops polling when no active tasks 5 new API tests (running endpoint, run sets status, not found). 141 tests total, all passing. Frontend builds clean. Co-Authored-By: Claude Opus 4.6 (1M context) --- tests/test_api.py | 38 ++++++++++++++++ web/api.py | 37 ++++++++++++--- web/frontend/src/views/Dashboard.vue | 23 +++++++++- web/frontend/src/views/ProjectView.vue | 18 ++++++++ web/frontend/src/views/TaskDetail.vue | 62 +++++++++++++++----------- 5 files changed, 145 insertions(+), 33 deletions(-) diff --git a/tests/test_api.py b/tests/test_api.py index 5527797..8d7ea42 100644 --- a/tests/test_api.py +++ b/tests/test_api.py @@ -135,6 +135,44 @@ def test_task_pipeline_not_found(client): assert r.status_code == 404 +def test_running_endpoint_no_pipeline(client): + r = client.get("/api/tasks/P1-001/running") + assert r.status_code == 200 + assert r.json()["running"] is False + + +def test_running_endpoint_with_pipeline(client): + from core.db import init_db + from core import models + conn = init_db(api_module.DB_PATH) + models.create_pipeline(conn, "P1-001", "p1", "debug", + [{"role": "debugger"}]) + conn.close() + + r = client.get("/api/tasks/P1-001/running") + assert r.status_code == 200 + assert r.json()["running"] is True + + +def test_running_endpoint_not_found(client): + r = client.get("/api/tasks/NOPE/running") + assert r.status_code == 404 + + +def test_run_sets_in_progress(client): + """POST /run should set task to in_progress immediately.""" + r = client.post("/api/tasks/P1-001/run") + assert r.status_code == 202 + + r = client.get("/api/tasks/P1-001") + assert r.json()["status"] == "in_progress" + + +def test_run_not_found(client): + r = client.post("/api/tasks/NOPE/run") + assert r.status_code == 404 + + def test_project_summary_includes_review(client): from core.db import init_db from core import models diff --git a/web/api.py b/web/api.py index 52c7fc5..6536a77 100644 --- a/web/api.py +++ b/web/api.py @@ -257,6 +257,24 @@ def reject_task(task_id: str, body: TaskReject): return {"status": "pending", "reason": body.reason} +@app.get("/api/tasks/{task_id}/running") +def is_task_running(task_id: str): + """Check if task has an active (running) pipeline.""" + conn = get_conn() + t = models.get_task(conn, task_id) + if not t: + conn.close() + raise HTTPException(404, f"Task '{task_id}' not found") + row = conn.execute( + "SELECT id, status FROM pipelines WHERE task_id = ? ORDER BY created_at DESC LIMIT 1", + (task_id,), + ).fetchone() + conn.close() + if row and row["status"] == "running": + return {"running": True, "pipeline_id": row["id"]} + return {"running": False} + + @app.post("/api/tasks/{task_id}/run") def run_task(task_id: str): """Launch pipeline for a task in background. Returns 202.""" @@ -265,15 +283,22 @@ def run_task(task_id: str): if not t: conn.close() raise HTTPException(404, f"Task '{task_id}' not found") + # Set task to in_progress immediately so UI updates + models.update_task(conn, task_id, status="in_progress") conn.close() # Launch kin run in background subprocess kin_root = Path(__file__).parent.parent - subprocess.Popen( - [sys.executable, "-m", "cli.main", "run", task_id, "--db", - str(DB_PATH)], - cwd=str(kin_root), - stdout=subprocess.DEVNULL, - ) + try: + proc = subprocess.Popen( + [sys.executable, "-m", "cli.main", "--db", str(DB_PATH), + "run", task_id], + cwd=str(kin_root), + stdout=subprocess.DEVNULL, + ) + import logging + logging.getLogger("kin").info(f"Pipeline started for {task_id}, pid={proc.pid}") + except Exception as e: + raise HTTPException(500, f"Failed to start pipeline: {e}") return JSONResponse({"status": "started", "task_id": task_id}, status_code=202) diff --git a/web/frontend/src/views/Dashboard.vue b/web/frontend/src/views/Dashboard.vue index 7c7edc9..e8fc54a 100644 --- a/web/frontend/src/views/Dashboard.vue +++ b/web/frontend/src/views/Dashboard.vue @@ -31,7 +31,23 @@ async function load() { } } -onMounted(load) +let dashPollTimer: ReturnType | null = null + +onMounted(async () => { + await load() + // Poll if there are running tasks + checkAndPoll() +}) + +function checkAndPoll() { + const hasRunning = projects.value.some(p => p.active_tasks > 0) + if (hasRunning && !dashPollTimer) { + dashPollTimer = setInterval(load, 5000) + } else if (!hasRunning && dashPollTimer) { + clearInterval(dashPollTimer) + dashPollTimer = null + } +} const costMap = computed(() => { const m: Record = {} @@ -115,7 +131,10 @@ async function runBootstrap() {
{{ p.total_tasks }} tasks - {{ p.active_tasks }} active + + + {{ p.active_tasks }} active + {{ p.review_tasks }} awaiting review {{ p.blocked_tasks }} blocked {{ p.done_tasks }} done diff --git a/web/frontend/src/views/ProjectView.vue b/web/frontend/src/views/ProjectView.vue index c20a99b..06e608f 100644 --- a/web/frontend/src/views/ProjectView.vue +++ b/web/frontend/src/views/ProjectView.vue @@ -109,6 +109,18 @@ async function addTask() { } } +async function runTask(taskId: string, event: Event) { + event.preventDefault() + event.stopPropagation() + if (!confirm(`Run pipeline for ${taskId}?`)) return + try { + await api.runTask(taskId) + await load() + } catch (e: any) { + error.value = e.message + } +} + async function addDecision() { decFormError.value = '' try { @@ -202,6 +214,12 @@ async function addDecision() {
{{ t.assigned_role }} pri {{ t.priority }} + +
diff --git a/web/frontend/src/views/TaskDetail.vue b/web/frontend/src/views/TaskDetail.vue index 9b779ec..70b6503 100644 --- a/web/frontend/src/views/TaskDetail.vue +++ b/web/frontend/src/views/TaskDetail.vue @@ -1,5 +1,5 @@