173 lines
5.6 KiB
Python
173 lines
5.6 KiB
Python
import html
|
|
import json
|
|
from urllib.parse import quote
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
|
from fastapi.responses import HTMLResponse
|
|
|
|
from app.auth import require_user
|
|
from app.db.audit import log_audit_event
|
|
from app.db.workers import get_worker, get_workers
|
|
from app.templates.layout import page
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
def render_worker_badge(online: bool) -> str:
|
|
if online:
|
|
return '<span class="pill pill-success">ONLINE</span>'
|
|
return '<span class="pill pill-danger">OFFLINE</span>'
|
|
|
|
|
|
def pretty_json(value: str | None) -> str:
|
|
if not value:
|
|
return "{}"
|
|
|
|
try:
|
|
return json.dumps(json.loads(value), ensure_ascii=False, indent=2, sort_keys=True)
|
|
except (TypeError, ValueError):
|
|
return value
|
|
|
|
|
|
@router.get("/workers", response_class=HTMLResponse)
|
|
def workers_page(request: Request, user=Depends(require_user)):
|
|
workers = get_workers()
|
|
online_count = sum(1 for worker in workers if worker.get("online"))
|
|
total_count = len(workers)
|
|
offline_count = total_count - online_count
|
|
|
|
log_audit_event(
|
|
user,
|
|
action="workers.view",
|
|
target_type="workers",
|
|
metadata={
|
|
"online": online_count,
|
|
"offline": offline_count,
|
|
"total": total_count,
|
|
},
|
|
)
|
|
|
|
rows = ""
|
|
for worker in workers:
|
|
worker_id_raw = worker.get("id", "") or ""
|
|
worker_id = html.escape(worker_id_raw)
|
|
worker_url_id = quote(worker_id_raw, safe="")
|
|
status = html.escape(worker.get("status", "") or "")
|
|
last_seen_at = html.escape(worker.get("last_seen_at", "") or "")
|
|
started_at = html.escape(worker.get("started_at", "") or "")
|
|
badge = render_worker_badge(bool(worker.get("online")))
|
|
current_job_id = worker.get("current_job_id")
|
|
current_job = ""
|
|
if current_job_id is not None:
|
|
current_job_label = html.escape(str(current_job_id))
|
|
current_job = f'<a href="/portal/jobs/{current_job_label}">#{current_job_label}</a>'
|
|
|
|
rows += f"""
|
|
<tr>
|
|
<td>
|
|
<strong><a href="/portal/workers/{worker_url_id}">{worker_id}</a></strong>
|
|
</td>
|
|
<td>{badge}<br><span class="muted">{status}</span></td>
|
|
<td>{last_seen_at}</td>
|
|
<td>{current_job}</td>
|
|
<td>{started_at}</td>
|
|
<td class="actions-cell"><a class="btn" href="/portal/workers/{worker_url_id}">Detail</a></td>
|
|
</tr>
|
|
"""
|
|
|
|
if not rows:
|
|
rows = '<tr><td colspan="6">Zatím nejsou evidovaní žádní workeři.</td></tr>'
|
|
|
|
return page(
|
|
"Workers",
|
|
f"""
|
|
<div class="card">
|
|
<h2>Workers</h2>
|
|
<p class="muted">Přehled worker procesů obsluhujících AppFactory joby.</p>
|
|
</div>
|
|
|
|
<div class="stats-grid">
|
|
<div class="stat-card stat-success"><span>Online Workers</span><strong>{online_count}</strong></div>
|
|
<div class="stat-card stat-danger"><span>Offline Workers</span><strong>{offline_count}</strong></div>
|
|
<div class="stat-card stat-total"><span>Total Workers</span><strong>{total_count}</strong></div>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Workers</h2>
|
|
<table>
|
|
<tr>
|
|
<th>Worker ID</th>
|
|
<th>Status</th>
|
|
<th>Last Seen</th>
|
|
<th>Current Job</th>
|
|
<th>Started At</th>
|
|
<th>Akce</th>
|
|
</tr>
|
|
{rows}
|
|
</table>
|
|
</div>
|
|
""",
|
|
user=user,
|
|
)
|
|
|
|
|
|
@router.get("/workers/{worker_id}", response_class=HTMLResponse)
|
|
def worker_detail_page(worker_id: str, request: Request, user=Depends(require_user)):
|
|
worker = get_worker(worker_id)
|
|
if not worker:
|
|
raise HTTPException(status_code=404, detail="Worker not found")
|
|
|
|
log_audit_event(
|
|
user,
|
|
action="workers.view",
|
|
target_type="worker",
|
|
target_id=worker_id,
|
|
)
|
|
|
|
worker_id_html = html.escape(worker.get("id", "") or "")
|
|
status = html.escape(worker.get("status", "") or "")
|
|
last_seen_at = html.escape(worker.get("last_seen_at", "") or "")
|
|
started_at = html.escape(worker.get("started_at", "") or "")
|
|
badge = render_worker_badge(bool(worker.get("online")))
|
|
current_job_id = worker.get("current_job_id")
|
|
current_job = ""
|
|
if current_job_id is not None:
|
|
current_job_label = html.escape(str(current_job_id))
|
|
current_job = f'<a href="/portal/jobs/{current_job_label}">#{current_job_label}</a>'
|
|
|
|
metadata = html.escape(pretty_json(worker.get("metadata_json")))
|
|
metadata_block = ""
|
|
if worker.get("metadata_json"):
|
|
metadata_block = f"""
|
|
<div class="card">
|
|
<h2>Metadata</h2>
|
|
<pre class="log-viewer log-stdout">{metadata}</pre>
|
|
</div>
|
|
"""
|
|
|
|
return page(
|
|
f"Worker {worker_id_html}",
|
|
f"""
|
|
<div class="card">
|
|
<h2>Worker {worker_id_html}</h2>
|
|
<p>
|
|
<a class="btn" href="/portal/workers">← Zpět na workers</a>
|
|
</p>
|
|
</div>
|
|
|
|
<div class="card">
|
|
<h2>Souhrn</h2>
|
|
<table>
|
|
<tr><th>Worker ID</th><td>{worker_id_html}</td></tr>
|
|
<tr><th>Status</th><td>{badge}<br><span class="muted">{status}</span></td></tr>
|
|
<tr><th>Last Seen</th><td>{last_seen_at}</td></tr>
|
|
<tr><th>Current Job</th><td>{current_job}</td></tr>
|
|
<tr><th>Started At</th><td>{started_at}</td></tr>
|
|
</table>
|
|
</div>
|
|
|
|
{metadata_block}
|
|
""",
|
|
user=user,
|
|
)
|