import html import json from urllib.parse import quote 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_job_stats, 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", "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") IGNORED_RETRY_REPOSITORIES = {"appfactory-tools", "appfactory-infrastructure"} def render_job_status(status: str | None) -> str: 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: if not value: return "{}" try: return json.dumps(json.loads(value), ensure_ascii=False, indent=2, sort_keys=True) except (TypeError, ValueError): 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) def can_retry_job(job: dict) -> bool: job_type = job.get("type") or "" target_id = job.get("target_id") or "" if job_type == "deploy_core_service": return True if job_type == "deploy_app" and target_id not in IGNORED_RETRY_REPOSITORIES: return True return False @router.get("/jobs", response_class=HTMLResponse) 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 "" rows = "" for job in jobs: job_id = html.escape(str(job.get("id", ""))) status = render_job_status(job.get("status")) source = html.escape(job.get("source", "") or "") target_type = html.escape(job.get("target_type", "") or "") target_id = html.escape(job.get("target_id", "") or "") job_type = html.escape(job.get("type", "") or "") created_by = html.escape(job.get("created_by_display_name") or job.get("created_by_username") or "") created_at = html.escape(job.get("created_at", "") or "") started_at = html.escape(job.get("started_at", "") or "") finished_at = html.escape(job.get("finished_at", "") or "") row_class = ' class="running-row"' if (job.get("status") or "").lower() == "running" else "" rows += f"""
Fronta portálových a webhook úloh připravená pro centrální worker.
| ID | Status | Source | Target | Created by | Created at | Started at | Finished at | Akce |
|---|
Tento repozitář není deployovatelná služba.
' if status_value in {"queued", "running"}: actions += f""" """ if actions: actions = f"""{message}
Zatím nejsou uložené žádné logy.
' return page( title, f""" {refresh}| Typ | {html.escape(job.get("type", "") or "")} |
|---|---|
| Status | {status} |
| Source | {html.escape(job.get("source", "") or "")} |
| Target | {html.escape(target_type)}: {target_id_html} |
| Created by | {html.escape(job.get("created_by_display_name") or job.get("created_by_username") or "")} |
| Created at | {html.escape(job.get("created_at", "") or "")} |
| Started at | {html.escape(job.get("started_at", "") or "")} |
| Finished at | {html.escape(job.get("finished_at", "") or "")} |
| Duration | {duration} |
| Worker | {html.escape(job.get("worker_id", "") or "")} |
{payload}
{result}
{error_text}