251 lines
9.1 KiB
Python
251 lines
9.1 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="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>'
|
|
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">← 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,
|
|
)
|