From f651f7efd639745b2dfb8ff042a1d7b64336c729 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Fri, 29 May 2026 09:16:37 +0200 Subject: [PATCH] added jobs handling. --- app/db/jobs.py | 115 +++++++++++++++++++++++++++++++++++++++++++++ app/routes/jobs.py | 92 ++++++++++++++++++++++++++++++++++-- 2 files changed, 203 insertions(+), 4 deletions(-) diff --git a/app/db/jobs.py b/app/db/jobs.py index 3d18c18..62c8010 100644 --- a/app/db/jobs.py +++ b/app/db/jobs.py @@ -56,6 +56,121 @@ def create_job( return job_id +def retry_failed_job(job_id: int, user: dict[str, Any]): + run_migrations() + created_by_user_id = user.get("id") if user else None + created_by_username = user.get("username") if user else None + created_by_display_name = user.get("display_name") if user else None + + con = get_connection() + source_job = con.execute( + """ + SELECT id + FROM jobs + WHERE id = ? + AND status = 'failed' + """, + (job_id,), + ).fetchone() + if not source_job: + con.close() + return None + + cur = con.execute( + """ + INSERT INTO jobs ( + type, + target_type, + target_id, + payload_json, + status, + created_by_user_id, + created_by_username, + created_by_display_name, + source + ) + SELECT + type, + target_type, + target_id, + payload_json, + 'queued', + ?, + ?, + ?, + 'portal_retry' + FROM jobs + WHERE id = ? + AND status = 'failed' + """, + (created_by_user_id, created_by_username, created_by_display_name, job_id), + ) + if cur.rowcount != 1: + con.close() + return None + + new_job_id = cur.lastrowid + con.commit() + con.close() + + append_job_log(new_job_id, "system", f"Job queued as retry of #{job_id}.") + return new_job_id + + +def cancel_job(job_id: int): + run_migrations() + con = get_connection() + + row = con.execute( + """ + SELECT status + FROM jobs + WHERE id = ? + """, + (job_id,), + ).fetchone() + if not row: + con.close() + return None + + status = (row["status"] or "").lower() + if status == "queued": + new_status = "cancelled" + cur = con.execute( + """ + UPDATE jobs + SET status = 'cancelled', + finished_at = CURRENT_TIMESTAMP + WHERE id = ? + AND status = 'queued' + """, + (job_id,), + ) + elif status == "running": + new_status = "cancelled_requested" + cur = con.execute( + """ + UPDATE jobs + SET status = 'cancelled_requested' + WHERE id = ? + AND status = 'running' + """, + (job_id,), + ) + else: + con.close() + return None + + if cur.rowcount != 1: + con.close() + return None + + con.commit() + con.close() + append_job_log(job_id, "system", f"Job status changed from {status} to {new_status}.") + return {"previous_status": status, "status": new_status} + + def update_job_status( job_id: int, status: str, diff --git a/app/routes/jobs.py b/app/routes/jobs.py index 154250a..6de048e 100644 --- a/app/routes/jobs.py +++ b/app/routes/jobs.py @@ -3,20 +3,36 @@ import json from urllib.parse import quote from fastapi import APIRouter, Depends, HTTPException, Request -from fastapi.responses import HTMLResponse +from fastapi.responses import HTMLResponse, RedirectResponse from app.auth import require_user -from app.db.jobs import get_job, get_job_logs, get_jobs +from app.db.audit import log_audit_event +from app.db.jobs import cancel_job, get_job, get_job_logs, get_jobs, retry_failed_job from app.routes.deployments import calculate_duration, render_status_pill from app.templates.layout import page router = APIRouter() -LIVE_STATUSES = {"queued", "running"} +LIVE_STATUSES = {"queued", "running", "cancelled_requested"} +JOB_STATUSES = {"queued", "running", "cancelled_requested", "cancelled", "success", "failed"} def render_job_status(status: str | None) -> str: - return render_status_pill(status) + status_value = status or "" + normalized = status_value.lower() + if normalized not in JOB_STATUSES: + return render_status_pill(status) + + if normalized == "success": + class_name = "pill pill-success" + elif normalized in {"queued", "running", "cancelled_requested"}: + class_name = "pill pill-warning" + elif normalized in {"failed", "cancelled"}: + class_name = "pill pill-danger" + else: + class_name = "pill pill-muted" + + return f'{html.escape(status_value)}' def pretty_json(value: str | None) -> str: @@ -119,6 +135,25 @@ def job_detail_page(job_id: int, request: Request, user=Depends(require_user)): payload = html.escape(pretty_json(job.get("payload_json"))) result = html.escape(pretty_json(job.get("result_json"))) error_text = html.escape(job.get("error_text", "") or "") + actions = "" + if status_value == "failed": + actions += f""" +
+ """ + if status_value in {"queued", "running"}: + actions += f""" + + """ + if actions: + actions = f""" +