deployment observability, filters, stats, details

This commit is contained in:
JiriUhlir
2026-05-28 13:22:51 +02:00
parent 80d82d36dd
commit e6ffeb8fef
5 changed files with 585 additions and 34 deletions
+125 -4
View File
@@ -1,7 +1,8 @@
import html
from urllib.parse import quote
from fastapi import APIRouter, Depends, Form, Request
from fastapi.responses import HTMLResponse
from fastapi import APIRouter, Depends, Form, HTTPException, Request
from fastapi.responses import HTMLResponse, RedirectResponse
from ..auth import require_user
from ..config import (
@@ -13,9 +14,10 @@ from ..config import (
NEW_APP_SCRIPT,
read_env_value,
)
from ..db.apps import get_apps, update_app_resources
from ..db.apps import get_app, get_app_deployments, get_apps, update_app_resources
from ..db.audit import log_audit_event
from ..shell import run_command
from ..routes.deployments import render_status_pill
from ..shell import run_command, run_command_background
from ..templates.layout import page, render_result
router = APIRouter()
@@ -33,6 +35,7 @@ def index(request: Request, user=Depends(require_user)):
for item in apps:
app_id = html.escape(item.get("id", ""))
app_url_id = quote(item.get("id", ""), safe="")
status = html.escape(item.get("status", ""))
docs = html.escape(item.get("docs", f"/apps/{app_id}/docs"))
memory = item.get("memory", "")
@@ -102,6 +105,11 @@ def index(request: Request, user=Depends(require_user)):
</details>
</td>
<td>
<p><a class="btn" href="/portal/apps/{app_url_id}">Detail</a></p>
<p><a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení</a></p>
<form method="post" action="/portal/apps/{app_url_id}/redeploy" onsubmit="return confirm('Spustit redeploy aplikace {app_id}?');">
<button type="submit" class="btn-secondary">Redeploy</button>
</form>
<form method="post" action="/portal/delete-app" onsubmit="return confirm('Smazat {app_id}? Tím se odstraní kontejner, image, workspace, záznam v katalogu a Gitea repozitář.');">
<input type="hidden" name="app_id" value="{app_id}">
<button type="submit" class="danger">Smazat</button>
@@ -153,6 +161,119 @@ def index(request: Request, user=Depends(require_user)):
)
@router.get("/apps/{app_id}", response_class=HTMLResponse)
def app_detail(app_id: str, request: Request, user=Depends(require_user)):
app = get_app(app_id)
if not app:
raise HTTPException(status_code=404, detail="App not found")
escaped_app_id = html.escape(app.get("id", ""))
app_url_id = quote(app.get("id", ""), safe="")
name = html.escape(app.get("name", "") or "")
language = html.escape(app.get("language", "") or "")
version = html.escape(app.get("version", "") or "")
status = html.escape(app.get("status", "") or "")
memory = html.escape(app.get("memory", "") or "")
cpus = html.escape(app.get("cpus", "") or "")
updated_at = html.escape(app.get("updated_at", "") or "")
rows = ""
for deployment in get_app_deployments(app.get("id", ""), limit=10):
deployment_id = html.escape(str(deployment.get("id", "")))
started_at = html.escape(deployment.get("started_at", "") or "")
triggered_by = html.escape(
deployment.get("triggered_by_display_name")
or deployment.get("triggered_by_username")
or ""
)
rows += f"""
<tr>
<td><a href="/portal/deployments/{deployment_id}">#{deployment_id}</a></td>
<td>{render_status_pill(deployment.get("status"))}</td>
<td>{started_at}</td>
<td>{triggered_by}</td>
</tr>
"""
if not rows:
rows = '<tr><td colspan="4">Zatím nejsou evidovaná žádná nasazení této aplikace.</td></tr>'
return page(
f"Aplikace {escaped_app_id}",
f"""
<div class="card">
<h2>{escaped_app_id}</h2>
<p class="muted">{name}</p>
<p>
<a class="btn" href="/portal">&larr; Zpět na aplikace</a>
<a class="btn btn-secondary" href="/portal/deployments?app_id={app_url_id}">Nasazení aplikace</a>
</p>
<form method="post" action="/portal/apps/{app_url_id}/redeploy" onsubmit="return confirm('Spustit redeploy aplikace {escaped_app_id}?');">
<button type="submit">Redeploy</button>
</form>
</div>
<div class="card">
<h2>Souhrn</h2>
<table>
<tr><th>ID</th><td>{escaped_app_id}</td></tr>
<tr><th>Název</th><td>{name}</td></tr>
<tr><th>Jazyk</th><td>{language}</td></tr>
<tr><th>Verze</th><td>{version}</td></tr>
<tr><th>Status</th><td><span class="pill">{status}</span></td></tr>
<tr><th>Paměť</th><td>{memory}</td></tr>
<tr><th>CPU</th><td>{cpus}</td></tr>
<tr><th>Upraveno</th><td>{updated_at}</td></tr>
</table>
</div>
<div class="card">
<h2>Poslední deploymenty</h2>
<table>
<tr>
<th>ID</th>
<th>Status</th>
<th>Timestamp</th>
<th>Triggered by</th>
</tr>
{rows}
</table>
</div>
""",
user=user,
)
@router.post("/apps/{app_id}/redeploy")
def redeploy_app(app_id: str, user=Depends(require_user)):
app = get_app(app_id)
if not app:
raise HTTPException(status_code=404, detail="App not found")
process = run_command_background(
[DEPLOY_SCRIPT, app_id],
extra_env={
"APPFACTORY_TRIGGER_SOURCE": "portal",
"APPFACTORY_TRIGGERED_BY_USER_ID": user.get("id"),
"APPFACTORY_TRIGGERED_BY_USERNAME": user.get("username"),
"APPFACTORY_TRIGGERED_BY_DISPLAY_NAME": user.get("display_name") or user.get("username"),
},
)
log_audit_event(
user,
action="app.redeploy",
target_type="app",
target_id=app_id,
metadata={
"app_id": app_id,
"pid": process.pid,
"trigger_source": "portal",
},
)
return RedirectResponse(url=f"/portal/apps/{quote(app_id, safe='')}", status_code=303)
@router.get("/new-app", response_class=HTMLResponse)
def new_app_form(request: Request, user=Depends(require_user)):
return page(