diff --git a/app/db/jobs.py b/app/db/jobs.py index 62c8010..be50f7b 100644 --- a/app/db/jobs.py +++ b/app/db/jobs.py @@ -246,14 +246,34 @@ def get_next_queued_job(): return dict(row) if row else None -def get_jobs(limit: int = 100): +def get_jobs( + limit: int = 100, + status: str | None = None, + job_type: str | None = None, + target: str | None = None, +): run_migrations() con = get_connection() + filters = [] + params: list[Any] = [] + if status: + filters.append("status = ?") + params.append(status) + if job_type: + filters.append("type = ?") + params.append(job_type) + if target: + filters.append("(target_type LIKE ? OR target_id LIKE ?)") + target_pattern = f"%{target}%" + params.extend([target_pattern, target_pattern]) + + where_sql = f"WHERE {' AND '.join(filters)}" if filters else "" rows = con.execute( - """ + f""" SELECT * FROM jobs + {where_sql} ORDER BY CASE WHEN status = 'running' THEN 0 @@ -263,13 +283,33 @@ def get_jobs(limit: int = 100): id DESC LIMIT ? """, - (limit,), + (*params, limit), ).fetchall() con.close() return [dict(row) for row in rows] +def get_job_stats(): + run_migrations() + con = get_connection() + + rows = con.execute( + """ + SELECT status, COUNT(*) AS count + FROM jobs + WHERE status IN ('queued', 'running', 'failed', 'success') + GROUP BY status + """ + ).fetchall() + + con.close() + stats = {"queued": 0, "running": 0, "failed": 0, "success": 0} + for row in rows: + stats[row["status"]] = row["count"] + return stats + + def get_job(job_id: int): run_migrations() con = get_connection() diff --git a/app/routes/jobs.py b/app/routes/jobs.py index 6de048e..23f3421 100644 --- a/app/routes/jobs.py +++ b/app/routes/jobs.py @@ -2,12 +2,12 @@ import html import json from urllib.parse import quote -from fastapi import APIRouter, Depends, HTTPException, Request +from fastapi import APIRouter, Depends, HTTPException, Query, Request from fastapi.responses import HTMLResponse, RedirectResponse from app.auth import require_user 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.db.jobs import cancel_job, get_job, get_job_logs, get_job_stats, get_jobs, retry_failed_job from app.routes.deployments import calculate_duration, render_status_pill from app.templates.layout import page @@ -15,6 +15,8 @@ router = APIRouter() LIVE_STATUSES = {"queued", "running", "cancelled_requested"} JOB_STATUSES = {"queued", "running", "cancelled_requested", "cancelled", "success", "failed"} +JOB_FILTER_STATUSES = ("queued", "running", "success", "failed", "cancelled") +JOB_FILTER_TYPES = ("deploy_app", "deploy_core_service") def render_job_status(status: str | None) -> str: @@ -45,9 +47,37 @@ def pretty_json(value: str | None) -> str: return value +def render_options(values: tuple[str, ...], selected: str, empty_label: str) -> str: + options = [f''] + for value in values: + selected_attr = " selected" if selected == value else "" + escaped_value = html.escape(value) + options.append(f'') + return "".join(options) + + @router.get("/jobs", response_class=HTMLResponse) -def jobs_page(request: Request, user=Depends(require_user)): - jobs = get_jobs() +def jobs_page( + request: Request, + status: str = Query(""), + job_type: str = Query("", alias="type"), + target: str = Query(""), + user=Depends(require_user), +): + selected_status = status.strip().lower() + selected_type = job_type.strip() + selected_target = target.strip() + if selected_status not in JOB_FILTER_STATUSES: + selected_status = "" + if selected_type not in JOB_FILTER_TYPES: + selected_type = "" + + jobs = get_jobs( + status=selected_status or None, + job_type=selected_type or None, + target=selected_target or None, + ) + stats = get_job_stats() refresh = "" if any( (job.get("status") or "").lower() in LIVE_STATUSES for job in jobs ) else "" @@ -86,6 +116,10 @@ def jobs_page(request: Request, user=Depends(require_user)): if not rows: rows = 'Zatím nejsou evidované žádné joby.' + status_options = render_options(JOB_FILTER_STATUSES, selected_status, "Všechny statusy") + type_options = render_options(JOB_FILTER_TYPES, selected_type, "Všechny typy") + target_value = html.escape(selected_target) + return page( "Joby", f""" @@ -95,6 +129,24 @@ def jobs_page(request: Request, user=Depends(require_user)):

Fronta portálových a webhook úloh připravená pro centrální worker.

+
+
Queued{stats.get("queued") or 0}
+
Running{stats.get("running") or 0}
+
Failed{stats.get("failed") or 0}
+
Success{stats.get("success") or 0}
+
+ +
+

Filtry

+
+ + + + + Reset +
+
+

Fronta