Files
JiriUhlir 23bbcad835 st
2026-06-16 10:47:14 +02:00

251 lines
9.2 KiB
Python

import html
import json
import math
from urllib.parse import quote, urlencode
from fastapi import APIRouter, Depends, HTTPException, Query, 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()
DEFAULT_PAGE_SIZE = 20
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,
status: str = Query(""),
q: str = Query(""),
page_number: int = Query(1, alias="page", ge=1),
user=Depends(require_user),
):
all_workers = get_workers()
selected_status = status.strip().lower()
query = q.strip()
online_count = sum(1 for worker in all_workers if worker.get("online"))
total_count = len(all_workers)
offline_count = total_count - online_count
workers = all_workers
if selected_status == "online":
workers = [worker for worker in workers if worker.get("online")]
elif selected_status == "offline":
workers = [worker for worker in workers if not worker.get("online")]
else:
selected_status = ""
if query:
workers = [worker for worker in workers if query.lower() in (worker.get("id", "") or "").lower()]
total_filtered = len(workers)
total_pages = max(1, math.ceil(total_filtered / DEFAULT_PAGE_SIZE))
if page_number > total_pages:
page_number = total_pages
offset = (page_number - 1) * DEFAULT_PAGE_SIZE
workers = workers[offset : offset + DEFAULT_PAGE_SIZE]
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 data-worker-id="{worker_id}">
<td>
<strong><a href="/portal/workers/{worker_url_id}">{worker_id}</a></strong>
</td>
<td data-worker-field="status">{badge}<br><span class="muted">{status}</span></td>
<td data-worker-field="last_seen_at">{last_seen_at}</td>
<td data-worker-field="current_job">{current_job}</td>
<td>{started_at}</td>
<td class="actions-cell"><a class="icon-action" href="/portal/workers/{worker_url_id}" title="Detail" aria-label="Detail"><i class="fa-solid fa-eye" aria-hidden="true"></i></a></td>
</tr>
"""
if not rows:
rows = '<tr><td colspan="6">Zatím nejsou evidovaní žádní workeři.</td></tr>'
live_table_attr = ' data-live-table="workers.recent"' if not selected_status and not query and page_number == 1 else ""
first_item = offset + 1 if total_filtered else 0
last_item = min(offset + len(workers), total_filtered)
def page_url(page: int) -> str:
params = {"page": page}
if selected_status:
params["status"] = selected_status
if query:
params["q"] = query
return f"/portal/workers?{urlencode(params)}"
previous_link = (
f'<a class="btn btn-secondary" href="{page_url(page_number - 1)}">Předchozí</a>'
if page_number > 1
else ""
)
next_link = (
f'<a class="btn btn-secondary" href="{page_url(page_number + 1)}">Další</a>'
if page_number < total_pages
else ""
)
pagination = ""
if total_pages > 1:
pagination = f"""
<div class="pagination">
<span>Zobrazeno {first_item}-{last_item} z {total_filtered}</span>
<div class="pagination-actions">
{previous_link}
<span class="page-indicator">Strana {page_number} / {total_pages}</span>
{next_link}
</div>
</div>
"""
status_options = "".join(
f'<option value="{value}"{" selected" if selected_status == value else ""}>{label}</option>'
for value, label in [("", "Všichni workeři"), ("online", "Online"), ("offline", "Offline")]
)
return page(
"Workery",
f"""
<div class="card">
<h2><i class="fa-solid fa-gears" aria-hidden="true"></i> Workery</h2>
<p class="muted">Přehled worker procesů obsluhujících úlohy portálu.</p>
</div>
<div class="stats-grid">
<div class="stat-card stat-success"><span>Online workery</span><strong data-live-count="workers.online">{online_count}</strong></div>
<div class="stat-card stat-danger"><span>Offline workery</span><strong data-live-count="workers.offline">{offline_count}</strong></div>
<div class="stat-card stat-total"><span>Celkem workerů</span><strong data-live-count="workers.total">{total_count}</strong></div>
</div>
<div class="card">
<h2>Filtry</h2>
<form method="get" action="/portal/workers" class="filter-form">
<select name="status">{status_options}</select>
<input name="q" value="{html.escape(query)}" placeholder="ID workeru">
<input type="hidden" name="page" value="1">
<button type="submit"><i class="fa-solid fa-filter" aria-hidden="true"></i> Filtrovat</button>
<a class="btn btn-secondary" href="/portal/workers"><i class="fa-solid fa-arrow-rotate-left" aria-hidden="true"></i> Reset</a>
</form>
</div>
<div class="card">
<h2>Workery</h2>
{pagination}
<table>
<tr>
<th>ID workeru</th>
<th>Status</th>
<th>Naposledy viděn</th>
<th>Aktuální úloha</th>
<th>Spuštěn</th>
<th>Akce</th>
</tr>
<tbody{live_table_attr}>{rows}</tbody>
</table>
{pagination}
</div>
<script src="/portal/static/operations-live.js"></script>
""",
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">&larr; Zpět na workery</a>
</p>
</div>
<div class="card">
<h2>Souhrn</h2>
<table>
<tr><th>ID workeru</th><td>{worker_id_html}</td></tr>
<tr><th>Status</th><td>{badge}<br><span class="muted">{status}</span></td></tr>
<tr><th>Naposledy viděn</th><td>{last_seen_at}</td></tr>
<tr><th>Aktuální úloha</th><td>{current_job}</td></tr>
<tr><th>Spuštěn</th><td>{started_at}</td></tr>
</table>
</div>
{metadata_block}
""",
user=user,
)