"""Чтение журнала завершённых заданий из ``clusters/<имя>/journal/jobs_history.json``. Автор: Сергей Антропов Сайт: https://devops.org.ru """ from __future__ import annotations import asyncio import logging from fastapi import APIRouter, HTTPException, Query from core.cluster_lifecycle import validate_cluster_name from core.job_journal import collect_recent_journal_entries_page_sync, read_cluster_journal_sync from models.schemas import ClusterJournalResponse, JournalEntryModel, JournalRecentResponse logger = logging.getLogger("kind_k8s.api.journal") router = APIRouter(tags=["journal"]) def _journal_total_pages(total: int, limit: int) -> int: if total <= 0 or limit <= 0: return 0 return (total + limit - 1) // limit @router.get( "/journal/recent", response_model=JournalRecentResponse, summary="Страница журнала по всем кластерам (пагинация)", ) async def get_journal_recent( limit: int = Query(default=30, ge=1, le=100, description="Записей на страницу"), offset: int = Query(default=0, ge=0, description="Смещение (0 = первая страница)"), ) -> JournalRecentResponse: """Сборка из ``clusters/*/journal/jobs_history.json``, новые первыми; ``total`` и ``total_pages`` для UI.""" rows, total = await asyncio.to_thread( collect_recent_journal_entries_page_sync, limit=limit, offset=offset, ) entries = [JournalEntryModel.model_validate(r) for r in rows] total_pages = _journal_total_pages(total, limit) if total <= 0: page = 1 else: page = min((offset // limit) + 1, total_pages) page = max(1, page) return JournalRecentResponse( limit=limit, offset=offset, total=total, page=page, total_pages=total_pages, entries=entries, ) @router.get( "/clusters/{name}/journal", response_model=ClusterJournalResponse, summary="Журнал заданий кластера (JSON с диска)", ) async def get_cluster_journal(name: str) -> ClusterJournalResponse: n = name.strip() if not validate_cluster_name(n): raise HTTPException(status_code=400, detail="Некорректное имя кластера") raw = await asyncio.to_thread(read_cluster_journal_sync, n) if raw is None: return ClusterJournalResponse(cluster_name=n, file_version=None, entries=[]) fv = raw.get("file_version") if isinstance(raw.get("file_version"), int) else None ent = raw.get("entries") if not isinstance(ent, list): return ClusterJournalResponse(cluster_name=n, file_version=fv, entries=[]) entries: list[JournalEntryModel] = [] for item in ent: if isinstance(item, dict): try: entries.append(JournalEntryModel.model_validate(item)) except Exception: logger.debug("Пропуск некорректной записи журнала кластера «%s»", n) return ClusterJournalResponse(cluster_name=n, file_version=fv, entries=entries)