operations

This commit is contained in:
JiriUhlir
2026-05-29 10:44:25 +02:00
parent 3804bb32c4
commit 6e5d9b3120
4 changed files with 323 additions and 1 deletions
+176
View File
@@ -0,0 +1,176 @@
import html
from urllib.parse import quote
from fastapi import APIRouter, Depends, Request
from fastapi.responses import HTMLResponse
from app.auth import require_user
from app.db.audit import log_audit_event
from app.db.operations import (
get_operations_summary,
get_recent_audit_events,
get_recent_deployments,
get_recent_jobs,
)
from app.routes.deployments import render_status_pill
from app.routes.jobs import render_job_status
from app.templates.layout import page
router = APIRouter()
def render_recent_jobs(jobs: list[dict]) -> str:
rows = ""
for job in jobs:
job_id = html.escape(str(job.get("id", "")))
job_type = html.escape(job.get("type", "") or "")
target_type = html.escape(job.get("target_type", "") or "")
target_id = html.escape(job.get("target_id", "") or "")
source = html.escape(job.get("source", "") or "")
created_at = html.escape(job.get("created_at", "") or "")
rows += f"""
<tr>
<td><strong><a href="/portal/jobs/{job_id}">#{job_id}</a></strong></td>
<td>{render_job_status(job.get("status"))}</td>
<td>{job_type}</td>
<td>{target_type}: <strong>{target_id}</strong></td>
<td>{source}</td>
<td>{created_at}</td>
</tr>
"""
if not rows:
rows = '<tr><td colspan="6">Zatím nejsou evidované žádné joby.</td></tr>'
return rows
def render_recent_deployments(deployments: list[dict]) -> str:
rows = ""
for deployment in deployments:
deployment_id = html.escape(str(deployment.get("id", "")))
raw_app_id = deployment.get("app_id", "") or ""
app_id = html.escape(raw_app_id)
app_url_id = quote(raw_app_id, safe="")
kind = html.escape(deployment.get("kind", "") or "")
started_at = html.escape(deployment.get("started_at", "") or "")
finished_at = html.escape(deployment.get("finished_at", "") or "")
source = html.escape(deployment.get("trigger_source", "") or "")
rows += f"""
<tr>
<td><strong><a href="/portal/deployments/{deployment_id}">#{deployment_id}</a></strong></td>
<td><a href="/portal/apps/{app_url_id}">{app_id}</a></td>
<td>{kind}</td>
<td>{render_status_pill(deployment.get("status"))}</td>
<td>{source}</td>
<td>{started_at}</td>
<td>{finished_at}</td>
</tr>
"""
if not rows:
rows = '<tr><td colspan="7">Zatím nejsou evidovaná žádná nasazení.</td></tr>'
return rows
def render_recent_audit_events(events: list[dict]) -> str:
rows = ""
for event in events:
rows += f"""
<tr>
<td>{html.escape(event.get("created_at", "") or "")}</td>
<td>{html.escape(event.get("username", "") or "")}</td>
<td><span class="pill pill-muted">{html.escape(event.get("action", "") or "")}</span></td>
<td>{html.escape(event.get("target_type", "") or "")}</td>
<td>{html.escape(str(event.get("target_id") or ""))}</td>
<td>{html.escape(event.get("source", "") or "")}</td>
</tr>
"""
if not rows:
rows = '<tr><td colspan="6">Zatím nejsou evidované žádné auditní události.</td></tr>'
return rows
@router.get("/operations", response_class=HTMLResponse)
def operations_dashboard(request: Request, user=Depends(require_user)):
summary = get_operations_summary()
log_audit_event(
user,
action="operations.dashboard.view",
target_type="operations",
metadata=summary,
)
recent_jobs = get_recent_jobs()
recent_deployments = get_recent_deployments()
recent_audit_events = get_recent_audit_events()
return page(
"Operations",
f"""
<div class="card">
<h2>Operations Dashboard</h2>
<p class="muted">Provozní přehled workerů, jobů, deploymentů a auditních událostí.</p>
</div>
<div class="stats-grid">
<div class="stat-card stat-success"><span>Workers Online</span><strong>{summary.get("workers_online") or 0}</strong></div>
<div class="stat-card stat-warning"><span>Running Jobs</span><strong>{summary.get("running_jobs") or 0}</strong></div>
<div class="stat-card stat-warning"><span>Queued Jobs</span><strong>{summary.get("queued_jobs") or 0}</strong></div>
<div class="stat-card stat-danger"><span>Failed Jobs (24h)</span><strong>{summary.get("failed_jobs_24h") or 0}</strong></div>
<div class="stat-card stat-total"><span>Deployments Today</span><strong>{summary.get("deployments_today") or 0}</strong></div>
<div class="stat-card stat-total"><span>Active Applications</span><strong>{summary.get("active_applications") or 0}</strong></div>
</div>
<div class="card">
<h2>Recent Jobs</h2>
<table>
<tr>
<th>ID</th>
<th>Status</th>
<th>Type</th>
<th>Target</th>
<th>Source</th>
<th>Created At</th>
</tr>
{render_recent_jobs(recent_jobs)}
</table>
</div>
<div class="card">
<h2>Recent Deployments</h2>
<table>
<tr>
<th>ID</th>
<th>Aplikace</th>
<th>Typ</th>
<th>Status</th>
<th>Zdroj</th>
<th>Spuštěno</th>
<th>Dokončeno</th>
</tr>
{render_recent_deployments(recent_deployments)}
</table>
</div>
<div class="card">
<h2>Recent Audit Events</h2>
<table>
<tr>
<th>Čas</th>
<th>Uživatel</th>
<th>Akce</th>
<th>Typ cíle</th>
<th>Cíl</th>
<th>Zdroj</th>
</tr>
{render_recent_audit_events(recent_audit_events)}
</table>
</div>
""",
user=user,
)