import html from urllib.parse import quote from fastapi import APIRouter, Depends, HTTPException, 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 create_job, get_jobs, get_jobs_by_types from app.routes.jobs import render_job_status from app.templates.layout import page router = APIRouter() # Klíčové AppFactory komponenty, které lze z Portálu znovu nasadit. Pořadí určuje zobrazení v UI. # key – identifikátor komponenty (target_id jobu i suffix job typu / shell skriptu) # name – zobrazený název # desc – krátký popis # icon – FontAwesome ikona RUNTIME_COMPONENTS = [ ("portal", "Portal", "Webové administrační rozhraní AppFactory (tento Portál).", "fa-window-maximize"), ("tools", "Tools", "Sdílené provozní skripty a nástroje (appfactory-tools).", "fa-screwdriver-wrench"), ("webhook", "Webhook", "Příjem a zpracování Gitea webhooků (spouští deploy).", "fa-bolt"), ("worker", "Worker", "Centrální worker zpracovávající úlohy z fronty.", "fa-gears"), ("caddy", "Caddy", "Reverzní proxy a TLS pro Portál i služby.", "fa-shield-halved"), ("gitea", "Gitea", "Git server a registr repozitářů.", "fa-code-branch"), ("registry", "Registry", "Docker registr s image jednotlivých služeb.", "fa-box-archive"), ] # Mapování komponenty -> typ jobu. Job typy budou později mapovány na maintenance/redeploy-.sh. COMPONENT_JOB_TYPE = {key: f"redeploy-{key}" for key, _name, _desc, _icon in RUNTIME_COMPONENTS} RUNTIME_JOB_TYPES = tuple(COMPONENT_JOB_TYPE.values()) COMPONENT_NAMES = {key: name for key, name, _desc, _icon in RUNTIME_COMPONENTS} DASHBOARD_LIMIT = 20 def require_admin(user: dict) -> None: if (user.get("role") or "").lower() != "admin": raise HTTPException(status_code=403, detail="Runtime Management je dostupný pouze administrátorům") def _last_action_for(job_type: str) -> str: jobs = get_jobs(job_type=job_type, limit=1) if not jobs: return '' job = jobs[0] job_id = html.escape(str(job.get("id", ""))) created_at = html.escape(job.get("created_at", "") or "") status = render_job_status(job.get("status")) return f'{status}
{created_at} · #{job_id}' @router.get("/admin/runtime", response_class=HTMLResponse) def runtime_page(request: Request, message: str = "", error: str = "", user=Depends(require_user)): require_admin(user) notice = "" if message: notice = f'

{html.escape(message)}

' if error: notice = f'

{html.escape(error)}

' component_rows = "" for key, name, desc, icon in RUNTIME_COMPONENTS: last_action = _last_action_for(COMPONENT_JOB_TYPE[key]) component_rows += f""" {html.escape(name)} {html.escape(desc)} {last_action}
""" job_rows = "" for job in get_jobs_by_types(RUNTIME_JOB_TYPES, limit=DASHBOARD_LIMIT): job_id = html.escape(str(job.get("id", ""))) created_at = html.escape(job.get("created_at", "") or "") job_type = html.escape(job.get("type", "") or "") status = render_job_status(job.get("status")) result_text = job.get("error_text") or job.get("result_json") or "" result_preview = result_text if len(result_text) <= 160 else f"{result_text[:157]}..." result_cell = f"{html.escape(result_preview)}" if result_preview else '' job_rows += f""" {created_at} {job_type} {status} {result_cell} """ if not job_rows: job_rows = 'Zatím nebyly spuštěné žádné runtime operace.' return page( "Runtime Management", f"""

Runtime Management

Centrální správa klíčových AppFactory komponent. Redeploy nespouští akci přímo — vytvoří úlohu do fronty, kterou zpracuje worker. Dostupné pouze administrátorům.

{notice}

Komponenty

{component_rows}
Název Popis Poslední spuštění akce Akce

Posledních {DASHBOARD_LIMIT} runtime operací

{job_rows}
Čas Typ Stav Výsledek
""", user=user, ) @router.post("/admin/runtime/redeploy/{component}") def runtime_redeploy_action(component: str, user=Depends(require_user)): require_admin(user) if component not in COMPONENT_JOB_TYPE: raise HTTPException(status_code=404, detail="Neznámá komponenta") job_type = COMPONENT_JOB_TYPE[component] name = COMPONENT_NAMES[component] job_id = create_job( job_type=job_type, target_type="runtime", target_id=component, payload={"component": component}, user=user, source="portal_runtime", ) log_audit_event( user, action="runtime.redeploy", target_type="runtime", target_id=component, metadata={"component": component, "job_type": job_type, "job_id": job_id}, ) return RedirectResponse( url="/portal/admin/runtime?message=" + quote(f"Redeploy komponenty {name} byl zařazen do fronty (úloha #{job_id})."), status_code=303, )