filtry v jobs a shrnuti

This commit is contained in:
JiriUhlir
2026-05-29 09:23:36 +02:00
parent f651f7efd6
commit 4b4bf55696
2 changed files with 99 additions and 7 deletions
+43 -3
View File
@@ -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()
+56 -4
View File
@@ -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'<option value="">{html.escape(empty_label)}</option>']
for value in values:
selected_attr = " selected" if selected == value else ""
escaped_value = html.escape(value)
options.append(f'<option value="{escaped_value}"{selected_attr}>{escaped_value}</option>')
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 = "<script>setTimeout(() => window.location.reload(), 5000);</script>" 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 = '<tr><td colspan="9">Zatím nejsou evidované žádné joby.</td></tr>'
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)):
<p class="muted">Fronta portálových a webhook úloh připravená pro centrální worker.</p>
</div>
<div class="stats-grid">
<div class="stat-card stat-warning"><span>Queued</span><strong>{stats.get("queued") or 0}</strong></div>
<div class="stat-card stat-warning"><span>Running</span><strong>{stats.get("running") or 0}</strong></div>
<div class="stat-card stat-danger"><span>Failed</span><strong>{stats.get("failed") or 0}</strong></div>
<div class="stat-card stat-success"><span>Success</span><strong>{stats.get("success") or 0}</strong></div>
</div>
<div class="card">
<h2>Filtry</h2>
<form method="get" action="/portal/jobs" class="filter-form">
<select name="status">{status_options}</select>
<select name="type">{type_options}</select>
<input name="target" value="{target_value}" placeholder="Target">
<button type="submit">Filtrovat</button>
<a class="btn btn-secondary" href="/portal/jobs">Reset</a>
</form>
</div>
<div class="card">
<h2>Fronta</h2>
<table>