import html
from urllib.parse import quote
from fastapi import APIRouter, Depends, Form, HTTPException
from fastapi.responses import RedirectResponse
from app.auth import require_user
from app.db.audit import log_audit_event
from app.db.jobs import create_job
router = APIRouter()
# Samostatná stránka "Runtime Management" byla odstraněna – překrývala se s Environment, odkud se
# tyto redeploy akce spouští. Zůstává jen backend: render_action_buttons() + akční endpoint, které
# používá stránka Environment (app/routes/environment.py).
#
# Portál NEMÁ vlastní deploy logiku. Všechny provozní akce deleguje do existujících shell skriptů
# v appfactory-tools přes existující job systém (job_type="run_script").
# POZOR: předáváme HOLÝ název skriptu bez cesty – adresář scripts/ si doplní Worker sám. Cokoli
# s cestou ("scripts/...") Worker odmítne hláškou "Invalid script name" (viz config.py a
# is_allowed_worker_script). Stejné pravidlo hlídá i allowlist ALLOWED_WORKER_SCRIPTS.
DEPLOY_CORE_SCRIPT = "deploy-core-service.sh"
CADDY_SCRIPT = "generate-caddyfile.sh"
ENABLED_APPS_SCRIPT = "redeploy-enabled-apps.sh"
ALL_CORE_KEY = "redeploy-all-core"
ALL_CORE_LABEL = "Redeploy All Core Services"
ALL_CORE_ICON = "fa-server"
# Cílová stránka pro návrat po akci (dříve Runtime Management, nyní Environment).
DEFAULT_ACTION_NEXT = "/portal/admin/environment"
# Core služby nasazované přes deploy-core-service.sh . (key, name, icon, service_arg)
CORE_SERVICES = [
("redeploy-portal", "Portal", "fa-window-maximize", "appfactory-portal"),
("redeploy-worker", "Worker", "fa-gears", "appfactory-worker"),
("redeploy-webhook", "Webhook", "fa-bolt", "appfactory-webhook"),
("redeploy-monitor", "Monitor", "fa-heart-pulse", "appfactory-monitor"),
("redeploy-gateway", "Gateway (Caddy)", "fa-shield-halved", "appfactory-caddy"),
("redeploy-gitea", "Gitea", "fa-code-branch", "appfactory-gitea"),
("redeploy-registry", "Registry", "fa-box-archive", "appfactory-registry"),
]
# Spustitelné akce: key -> (label, icon, script_name, args). ALL_CORE_KEY je speciální (smyčka přes core).
ACTION_MAP: dict[str, tuple[str, str, str, list[str]]] = {}
for _key, _name, _icon, _arg in CORE_SERVICES:
ACTION_MAP[_key] = (f"Redeploy {_name}", _icon, DEPLOY_CORE_SCRIPT, [_arg])
ACTION_MAP["regenerate-caddy"] = ("Regenerate Caddy", "fa-shield-halved", CADDY_SCRIPT, [])
ACTION_MAP["redeploy-enabled-apps"] = ("Redeploy Enabled Apps", "fa-rocket", ENABLED_APPS_SCRIPT, [])
# Pořadí tlačítek po uložení .env (Environment stránka) – dle zadání.
ENV_ACTION_KEYS = [
"regenerate-caddy",
"redeploy-portal",
"redeploy-worker",
"redeploy-webhook",
"redeploy-monitor",
"redeploy-gateway",
"redeploy-gitea",
"redeploy-registry",
ALL_CORE_KEY,
]
def require_admin(user: dict) -> None:
if (user.get("role") or "").lower() != "admin":
raise HTTPException(status_code=403, detail="Runtime akce jsou dostupné pouze administrátorům")
def _is_valid_action(action_key: str) -> bool:
return action_key == ALL_CORE_KEY or action_key in ACTION_MAP
def _button_meta(action_key: str) -> tuple[str, str]:
if action_key == ALL_CORE_KEY:
return ALL_CORE_LABEL, ALL_CORE_ICON
label, icon, _script, _args = ACTION_MAP[action_key]
return label, icon
def enqueue_script_job(script_name: str, args: list[str], user: dict, target_label: str) -> int:
"""Vytvoří run_script job do existující fronty (worker spustí scripts/ s args)
a zaaudituje akci (bez hodnot proměnných). Vrací job_id."""
args = list(args or [])
payload = {"script_name": script_name}
if args:
payload["args"] = args
payload["arguments"] = args
job_id = create_job(
job_type="run_script",
target_type="maintenance_script",
target_id=script_name,
payload=payload,
user=user,
source="portal_runtime",
)
log_audit_event(
user,
action="runtime.action",
target_type="runtime",
target_id=target_label,
metadata={"script_name": script_name, "args": args, "job_id": job_id},
)
return job_id
def run_action(action_key: str, user: dict) -> str:
"""Spustí runtime akci podle klíče. Vrací zprávu pro uživatele."""
if action_key == ALL_CORE_KEY:
job_ids = [enqueue_script_job(DEPLOY_CORE_SCRIPT, [arg], user, arg) for _k, _n, _i, arg in CORE_SERVICES]
return f"Redeploy všech core služeb byl zařazen do fronty ({len(job_ids)} úloh)."
label, _icon, script, args = ACTION_MAP[action_key]
job_id = enqueue_script_job(script, args, user, label)
return f"Akce {label} byla zařazena do fronty (úloha #{job_id})."
def render_action_buttons(keys: list[str], next_url: str) -> str:
next_html = html.escape(next_url, quote=True)
buttons = ""
for key in keys:
label, icon = _button_meta(key)
buttons += f"""
"""
return buttons
@router.post("/admin/runtime/action/{action_key}")
def runtime_action(action_key: str, next: str = Form(DEFAULT_ACTION_NEXT), user=Depends(require_user)):
require_admin(user)
if not _is_valid_action(action_key):
raise HTTPException(status_code=404, detail="Neznámá akce")
message = run_action(action_key, user)
redirect_to = next if next.startswith("/portal/admin/") else DEFAULT_ACTION_NEXT
return RedirectResponse(url=f"{redirect_to}?message=" + quote(message), status_code=303)