Files
appfactory-portal/app/routes/runtime.py
T
JiriUhlir 39d9ed394d depl
2026-06-15 12:04:59 +02:00

173 lines
6.6 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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-<key>.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 '<span class="muted">—</span>'
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}<br><a href="/portal/jobs/{job_id}"><small>{created_at} · #{job_id}</small></a>'
@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'<p class="alert">{html.escape(message)}</p>'
if error:
notice = f'<p class="alert alert-danger">{html.escape(error)}</p>'
component_rows = ""
for key, name, desc, icon in RUNTIME_COMPONENTS:
last_action = _last_action_for(COMPONENT_JOB_TYPE[key])
component_rows += f"""
<tr>
<td><strong><i class="fa-solid {icon}" aria-hidden="true"></i> {html.escape(name)}</strong></td>
<td>{html.escape(desc)}</td>
<td>{last_action}</td>
<td class="actions-cell">
<form method="post" action="/portal/admin/runtime/redeploy/{key}" class="inline-form"
onsubmit="return confirm('Opravdu chcete redeploy komponenty {html.escape(name)}?');">
<button type="submit"><i class="fa-solid fa-rotate" aria-hidden="true"></i> Redeploy</button>
</form>
</td>
</tr>
"""
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"<code>{html.escape(result_preview)}</code>" if result_preview else '<span class="muted">—</span>'
job_rows += f"""
<tr>
<td>{created_at}</td>
<td><a href="/portal/jobs/{job_id}">{job_type}</a></td>
<td>{status}</td>
<td>{result_cell}</td>
</tr>
"""
if not job_rows:
job_rows = '<tr><td colspan="4">Zatím nebyly spuštěné žádné runtime operace.</td></tr>'
return page(
"Runtime Management",
f"""
<div class="card">
<h2><i class="fa-solid fa-server" aria-hidden="true"></i> Runtime Management</h2>
<p class="muted">
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.
</p>
{notice}
</div>
<div class="card">
<h2>Komponenty</h2>
<table>
<tr>
<th>Název</th>
<th>Popis</th>
<th>Poslední spuštění akce</th>
<th>Akce</th>
</tr>
{component_rows}
</table>
</div>
<div class="card">
<h2>Posledních {DASHBOARD_LIMIT} runtime operací</h2>
<table>
<tr>
<th>Čas</th>
<th>Typ</th>
<th>Stav</th>
<th>Výsledek</th>
</tr>
{job_rows}
</table>
</div>
""",
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,
)