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 'stderr' if value == "stdout": return 'stdout' return 'system' def render_stream_options(selected: str) -> str: options = [''] for value in STREAM_FILTERS: selected_attr = " selected" if selected == value else "" options.append(f'') 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"""
{message_html}Souhrnné čtení logů napříč všemi úlohami. Vyfiltrujte si chyby (stream stderr) nebo hledejte text a proklikněte se přímo na problematický řádek konkrétní úlohy.
| Čas | Úloha | Stream | Stav / cíl | Zpráva | Akce |
|---|