gitea user and pswd, some pages removed, logs
This commit is contained in:
@@ -0,0 +1,177 @@
|
||||
import html
|
||||
import math
|
||||
from urllib.parse import urlencode
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, Request
|
||||
from fastapi.responses import HTMLResponse
|
||||
|
||||
from app.auth import require_user
|
||||
from app.db.jobs import count_job_logs, search_job_logs
|
||||
from app.routes.jobs import render_job_status
|
||||
from app.templates.layout import page
|
||||
|
||||
router = APIRouter()
|
||||
|
||||
STREAM_FILTERS = ("stdout", "stderr", "system")
|
||||
PAGE_SIZE = 100
|
||||
|
||||
|
||||
def _can_view_logs(user: dict) -> bool:
|
||||
return (user.get("role") or "").lower() in ("admin", "developer")
|
||||
|
||||
|
||||
def render_stream_pill(stream: str | None) -> str:
|
||||
value = (stream or "system").lower()
|
||||
if value == "stderr":
|
||||
return '<span class="pill pill-danger">stderr</span>'
|
||||
if value == "stdout":
|
||||
return '<span class="pill pill-success">stdout</span>'
|
||||
return '<span class="pill pill-muted">system</span>'
|
||||
|
||||
|
||||
def render_stream_options(selected: str) -> str:
|
||||
options = ['<option value="">Všechny streamy</option>']
|
||||
for value in STREAM_FILTERS:
|
||||
selected_attr = " selected" if selected == value else ""
|
||||
options.append(f'<option value="{value}"{selected_attr}>{value}</option>')
|
||||
return "".join(options)
|
||||
|
||||
|
||||
@router.get("/logs", response_class=HTMLResponse)
|
||||
def logs_page(
|
||||
request: Request,
|
||||
q: str = Query(""),
|
||||
stream: str = Query(""),
|
||||
job: str = Query(""),
|
||||
page_number: int = Query(1, alias="page", ge=1),
|
||||
user=Depends(require_user),
|
||||
):
|
||||
if not _can_view_logs(user):
|
||||
raise HTTPException(status_code=403, detail="Logy mohou číst jen developer nebo admin")
|
||||
|
||||
query = q.strip()
|
||||
selected_stream = stream.strip().lower()
|
||||
if selected_stream not in STREAM_FILTERS:
|
||||
selected_stream = ""
|
||||
|
||||
job_id: int | None = None
|
||||
job_value = job.strip()
|
||||
if job_value.isdigit():
|
||||
job_id = int(job_value)
|
||||
|
||||
total = count_job_logs(query=query or None, stream=selected_stream or None, job_id=job_id)
|
||||
total_pages = max(1, math.ceil(total / PAGE_SIZE))
|
||||
if page_number > total_pages:
|
||||
page_number = total_pages
|
||||
offset = (page_number - 1) * PAGE_SIZE
|
||||
|
||||
logs = search_job_logs(
|
||||
query=query or None,
|
||||
stream=selected_stream or None,
|
||||
job_id=job_id,
|
||||
limit=PAGE_SIZE,
|
||||
offset=offset,
|
||||
)
|
||||
|
||||
rows = ""
|
||||
for log in logs:
|
||||
log_id = html.escape(str(log.get("id", "")))
|
||||
job_id_html = html.escape(str(log.get("job_id", "")))
|
||||
created_at = html.escape(log.get("created_at", "") or "")
|
||||
job_type = html.escape(log.get("job_type", "") or "")
|
||||
target_type = html.escape(log.get("target_type", "") or "")
|
||||
target_id = html.escape(log.get("target_id", "") or "")
|
||||
message = log.get("message", "") or ""
|
||||
# Náhled zprávy ať tabulka nepřeteče; plný text je po prokliku na úlohu.
|
||||
preview = message if len(message) <= 400 else f"{message[:397]}..."
|
||||
message_html = html.escape(preview)
|
||||
# Proklik míří přímo na konkrétní řádek logu v detailu úlohy (kotva #log-{id}).
|
||||
deep_link = f"/portal/jobs/{job_id_html}#log-{log_id}"
|
||||
rows += f"""
|
||||
<tr>
|
||||
<td>{created_at}</td>
|
||||
<td><strong><a href="/portal/jobs/{job_id_html}">#{job_id_html}</a></strong><br><span class="muted">{job_type}</span></td>
|
||||
<td>{render_stream_pill(log.get("stream"))}</td>
|
||||
<td>{render_job_status(log.get("job_status"))}<br><span class="muted">{target_type}: {target_id}</span></td>
|
||||
<td><pre class="log-viewer log-inline">{message_html}</pre></td>
|
||||
<td class="actions-cell"><a class="icon-action" href="{deep_link}" title="Zobrazit v úloze" aria-label="Zobrazit v úloze"><i class="fa-solid fa-up-right-from-square" aria-hidden="true"></i></a></td>
|
||||
</tr>
|
||||
"""
|
||||
|
||||
if not rows:
|
||||
rows = '<tr><td colspan="6">Žádné logy neodpovídají filtru.</td></tr>'
|
||||
|
||||
def page_url(target_page: int) -> str:
|
||||
params = {"page": target_page}
|
||||
if query:
|
||||
params["q"] = query
|
||||
if selected_stream:
|
||||
params["stream"] = selected_stream
|
||||
if job_value:
|
||||
params["job"] = job_value
|
||||
return f"/portal/logs?{urlencode(params)}"
|
||||
|
||||
first_item = offset + 1 if total else 0
|
||||
last_item = min(offset + len(logs), total)
|
||||
previous_link = (
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number - 1)}"><i class="fa-solid fa-chevron-left" aria-hidden="true"></i> Předchozí</a>'
|
||||
if page_number > 1
|
||||
else ""
|
||||
)
|
||||
next_link = (
|
||||
f'<a class="btn btn-secondary" href="{page_url(page_number + 1)}">Další <i class="fa-solid fa-chevron-right" aria-hidden="true"></i></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}</span>
|
||||
<div class="pagination-actions">
|
||||
{previous_link}
|
||||
<span class="page-indicator">Strana {page_number} / {total_pages}</span>
|
||||
{next_link}
|
||||
</div>
|
||||
</div>
|
||||
"""
|
||||
|
||||
return page(
|
||||
"Logy",
|
||||
f"""
|
||||
<div class="card">
|
||||
<h2><i class="fa-solid fa-file-lines" aria-hidden="true"></i> Logy</h2>
|
||||
<p class="muted">
|
||||
Souhrnné čtení logů napříč všemi úlohami. Vyfiltrujte si chyby (stream <strong>stderr</strong>)
|
||||
nebo hledejte text a proklikněte se přímo na problematický řádek konkrétní úlohy.
|
||||
</p>
|
||||
<form method="get" action="/portal/logs" class="filter-form">
|
||||
<input name="q" value="{html.escape(query)}" placeholder="Hledat v textu logu">
|
||||
<select name="stream">{render_stream_options(selected_stream)}</select>
|
||||
<input name="job" value="{html.escape(job_value)}" placeholder="ID úlohy" inputmode="numeric">
|
||||
<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/logs?stream=stderr"><i class="fa-solid fa-triangle-exclamation" aria-hidden="true"></i> Jen chyby</a>
|
||||
<a class="btn btn-secondary" href="/portal/logs"><i class="fa-solid fa-arrow-rotate-left" aria-hidden="true"></i> Reset</a>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="card">
|
||||
<h2>Záznamy</h2>
|
||||
{pagination}
|
||||
<table>
|
||||
<tr>
|
||||
<th>Čas</th>
|
||||
<th>Úloha</th>
|
||||
<th>Stream</th>
|
||||
<th>Stav / cíl</th>
|
||||
<th>Zpráva</th>
|
||||
<th>Akce</th>
|
||||
</tr>
|
||||
{rows}
|
||||
</table>
|
||||
{pagination}
|
||||
</div>
|
||||
""",
|
||||
user=user,
|
||||
)
|
||||
Reference in New Issue
Block a user