Веб-интерфейс: страница /clusters, навигация и крошки для кластеров
- Выделена страница списка кластеров, панель упрощена; nav_active и крошки ведут в раздел Кластеры; theme.js синхронизирует активную пилюлю по URL. - Доработки дашборда, аддонов, журнала, стилей и API-документации. - Поддержка Podman: docker-compose.podman.yml, скрипты сокета; Makefile и env.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
"""Чтение журнала завершённых заданий из ``clusters/<имя>/journal/jobs_history.json``.
|
||||
"""Чтение журналов: задания (jobs_history), развёртывание (provision_log), Helm-аддоны (helm_addon_log).
|
||||
|
||||
Автор: Сергей Антропов
|
||||
Сайт: https://devops.org.ru
|
||||
@@ -13,7 +13,18 @@ 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
|
||||
from core.provision_log import (
|
||||
HELM_ADDON_LOG_FILENAME,
|
||||
PROVISION_LOG_FILENAME,
|
||||
collect_cluster_dir_logs_page_sync,
|
||||
)
|
||||
from models.schemas import (
|
||||
ClusterDirLogEntryModel,
|
||||
ClusterJournalResponse,
|
||||
JournalEntryModel,
|
||||
JournalPagedDirLogsResponse,
|
||||
JournalRecentResponse,
|
||||
)
|
||||
|
||||
logger = logging.getLogger("kind_k8s.api.journal")
|
||||
|
||||
@@ -26,20 +37,52 @@ def _journal_total_pages(total: int, limit: int) -> int:
|
||||
return (total + limit - 1) // limit
|
||||
|
||||
|
||||
def _build_paged_dir_response(
|
||||
rows: list[dict],
|
||||
*,
|
||||
limit: int,
|
||||
offset: int,
|
||||
total: int,
|
||||
) -> JournalPagedDirLogsResponse:
|
||||
total_pages = _journal_total_pages(total, limit)
|
||||
if total <= 0:
|
||||
page = 1
|
||||
else:
|
||||
page = min((offset // limit) + 1, total_pages)
|
||||
page = max(1, page)
|
||||
entries = [ClusterDirLogEntryModel.model_validate(r) for r in rows]
|
||||
return JournalPagedDirLogsResponse(
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
total=total,
|
||||
page=page,
|
||||
total_pages=total_pages,
|
||||
entries=entries,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/journal/recent",
|
||||
response_model=JournalRecentResponse,
|
||||
summary="Страница журнала по всем кластерам (пагинация)",
|
||||
summary="Страница журнала заданий (jobs_history), опционально один кластер",
|
||||
)
|
||||
async def get_journal_recent(
|
||||
limit: int = Query(default=30, ge=1, le=100, description="Записей на страницу"),
|
||||
offset: int = Query(default=0, ge=0, description="Смещение (0 = первая страница)"),
|
||||
cluster: str | None = Query(
|
||||
default=None,
|
||||
description="Имя кластера: только записи из clusters/<имя>/journal/jobs_history.json",
|
||||
),
|
||||
) -> JournalRecentResponse:
|
||||
"""Сборка из ``clusters/*/journal/jobs_history.json``, новые первыми; ``total`` и ``total_pages`` для UI."""
|
||||
"""Сборка из ``journal/jobs_history.json``; при ``cluster`` — только выбранный каталог."""
|
||||
c = cluster.strip() if cluster else None
|
||||
if c and not validate_cluster_name(c):
|
||||
raise HTTPException(status_code=400, detail="Некорректное имя кластера")
|
||||
rows, total = await asyncio.to_thread(
|
||||
collect_recent_journal_entries_page_sync,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
cluster_name=c,
|
||||
)
|
||||
entries = [JournalEntryModel.model_validate(r) for r in rows]
|
||||
total_pages = _journal_total_pages(total, limit)
|
||||
@@ -58,6 +101,44 @@ async def get_journal_recent(
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/journal/provision",
|
||||
response_model=JournalPagedDirLogsResponse,
|
||||
summary="Журналы развёртывания по кластерам (provision_log.json)",
|
||||
)
|
||||
async def get_journal_provision_logs(
|
||||
limit: int = Query(default=30, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> JournalPagedDirLogsResponse:
|
||||
"""По одному последнему ``provision_log.json`` на кластер, новые первыми."""
|
||||
rows, total = await asyncio.to_thread(
|
||||
collect_cluster_dir_logs_page_sync,
|
||||
filename=PROVISION_LOG_FILENAME,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
return _build_paged_dir_response(rows, limit=limit, offset=offset, total=total)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/journal/helm-addons",
|
||||
response_model=JournalPagedDirLogsResponse,
|
||||
summary="Журналы установки Helm-аддонов (helm_addon_log.json)",
|
||||
)
|
||||
async def get_journal_helm_addon_logs(
|
||||
limit: int = Query(default=30, ge=1, le=100),
|
||||
offset: int = Query(default=0, ge=0),
|
||||
) -> JournalPagedDirLogsResponse:
|
||||
"""Все записи из ``helm_addon_log.json`` по кластерам (история операций), новые первыми."""
|
||||
rows, total = await asyncio.to_thread(
|
||||
collect_cluster_dir_logs_page_sync,
|
||||
filename=HELM_ADDON_LOG_FILENAME,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
return _build_paged_dir_response(rows, limit=limit, offset=offset, total=total)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/clusters/{name}/journal",
|
||||
response_model=ClusterJournalResponse,
|
||||
|
||||
Reference in New Issue
Block a user