From 39d9ed394d9b93b5b3629620b9d32d76d6628dd6 Mon Sep 17 00:00:00 2001 From: JiriUhlir <149317995+JiriUhlir@users.noreply.github.com> Date: Mon, 15 Jun 2026 12:04:59 +0200 Subject: [PATCH] depl --- app/db/jobs.py | 23 ++++++ app/main.py | 3 +- app/routes/developers.py | 2 + app/routes/runtime.py | 172 +++++++++++++++++++++++++++++++++++++++ app/templates/layout.py | 1 + 5 files changed, 200 insertions(+), 1 deletion(-) create mode 100644 app/routes/runtime.py diff --git a/app/db/jobs.py b/app/db/jobs.py index 05d4a46..5d101e5 100644 --- a/app/db/jobs.py +++ b/app/db/jobs.py @@ -292,6 +292,29 @@ def get_jobs( return [dict(row) for row in rows] +def get_jobs_by_types(job_types, limit: int = 20): + run_migrations() + job_types = list(job_types) + if not job_types: + return [] + + con = get_connection() + placeholders = ",".join("?" for _ in job_types) + rows = con.execute( + f""" + SELECT * + FROM jobs + WHERE type IN ({placeholders}) + ORDER BY id DESC + LIMIT ? + """, + (*job_types, limit), + ).fetchall() + + con.close() + return [dict(row) for row in rows] + + def count_jobs( status: str | None = None, job_type: str | None = None, diff --git a/app/main.py b/app/main.py index 73a2ccf..05661f5 100644 --- a/app/main.py +++ b/app/main.py @@ -5,7 +5,7 @@ from fastapi.staticfiles import StaticFiles from starlette.middleware.sessions import SessionMiddleware from .config import read_env_bool, read_env_value -from .routes import alerting, apps, audit, auth, backups, catalog, deployments, developers, health, incidents, jobs, migration_readiness, operations, scheduled_scripts, users, workers +from .routes import alerting, apps, audit, auth, backups, catalog, deployments, developers, health, incidents, jobs, migration_readiness, operations, runtime, scheduled_scripts, users, workers def create_app() -> FastAPI: @@ -38,6 +38,7 @@ def create_app() -> FastAPI: app.include_router(workers.router) app.include_router(users.router) app.include_router(audit.router) + app.include_router(runtime.router) return app diff --git a/app/routes/developers.py b/app/routes/developers.py index 19512c9..74cf053 100644 --- a/app/routes/developers.py +++ b/app/routes/developers.py @@ -23,6 +23,8 @@ PORTAL_SECTIONS = [ "Pravidla alertů a jejich skripty; úpravy jen pro administrátory.", "developer / admin"), ("fa-gauge-high", "Přehled", "/portal/operations", "Operační přehled systému, běžící nasazení a stav služeb.", "admin"), + ("fa-server", "Runtime Management", "/portal/admin/runtime", + "Redeploy klíčových komponent (Portal, Tools, Webhook, Worker, Caddy, Gitea, Registry) přes job frontu.", "admin"), ("fa-diagram-project", "Migration Readiness", "/portal/migration-readiness", "Připravenost a deploy core služeb AppFactory.", "admin"), ("fa-rocket", "Nasazení", "/portal/deployments", diff --git a/app/routes/runtime.py b/app/routes/runtime.py new file mode 100644 index 0000000..d97e073 --- /dev/null +++ b/app/routes/runtime.py @@ -0,0 +1,172 @@ +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ázevPopisPoslední spuštění akceAkce
+
+ +
+

Posledních {DASHBOARD_LIMIT} runtime operací

+ + + + + + + + {job_rows} +
ČasTypStavVý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, + ) diff --git a/app/templates/layout.py b/app/templates/layout.py index 56576ed..323963b 100644 --- a/app/templates/layout.py +++ b/app/templates/layout.py @@ -27,6 +27,7 @@ def page(title: str, body: str, user=None) -> str: Nasazení Incidenty Workery + Runtime Management Users Audit Zálohy