Журнал: пагинация 30 записей на страницу, API offset/total_pages
- job_journal: collect_recent_journal_entries_page_sync(limit, offset) - GET /api/v1/journal/recent: limit по умолчанию 30, offset, total, page, total_pages - journal.html/js: навигация Первая/Назад/номера/Вперёд/Последняя, стили - app/docs/api_routes.md: описание query и пример ответа - Прочие изменения UI/API (аддоны, helm, job_journal в кластерах) в том же коммите
This commit is contained in:
@@ -0,0 +1,84 @@
|
||||
"""Чтение журнала завершённых заданий из ``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)
|
||||
Reference in New Issue
Block a user