Веб-интерфейс: страница /clusters, навигация и крошки для кластеров

- Выделена страница списка кластеров, панель упрощена; nav_active и крошки
  ведут в раздел Кластеры; theme.js синхронизирует активную пилюлю по URL.
- Доработки дашборда, аддонов, журнала, стилей и API-документации.
- Поддержка Podman: docker-compose.podman.yml, скрипты сокета; Makefile и env.
This commit is contained in:
Sergey Antropoff
2026-04-04 13:42:21 +03:00
parent 17f6233fd7
commit eb063aec20
38 changed files with 3990 additions and 734 deletions
+46 -9
View File
@@ -82,9 +82,18 @@ def append_job_from_record_sync(
return
cdir = clusters_dir() / name
# Страница «Журнал» читает clusters/<имя>/journal/jobs_history.json — нужен каталог кластера.
# После отмены create_cluster каталог мог быть удалён (rmtree) — не создаём пустой каталог заново.
if not cdir.is_dir():
logger.debug("Журнал кластера: каталог %s отсутствует", cdir)
return
if (status or "").lower() == "cancelled":
logger.debug("Журнал кластера: каталог %s удалён при отмене — запись jobs_history пропускаем", cdir)
return
try:
cdir.mkdir(parents=True, exist_ok=True)
logger.info("Журнал: создан каталог кластера для записи jobs_history: %s", cdir)
except OSError as e:
logger.warning("Журнал кластера: не удалось создать %s: %s", cdir, e)
return
jdir = cdir / JOURNAL_SUBDIR
finished = datetime.now(timezone.utc).isoformat()
@@ -185,21 +194,49 @@ def _collect_all_journal_rows_sorted_sync() -> list[tuple[str, dict[str, Any]]]:
return flat
def collect_recent_journal_entries_page_sync(*, limit: int, offset: int) -> tuple[list[dict[str, Any]], int]:
def collect_recent_journal_entries_page_sync(
*,
limit: int,
offset: int,
cluster_name: str | None = None,
) -> tuple[list[dict[str, Any]], int]:
"""
Страница записей из всех ``clusters/*/journal/jobs_history.json``, новые первыми.
Страница записей из ``journal/jobs_history.json``, новые первыми.
В каждую запись добавляется поле ``source_cluster`` (имя каталога).
Возвращает ``(страница записей, общее число записей)``.
Если ``cluster_name`` задан — только этот кластер (файл на диске), иначе объединение по всем кластерам.
В каждую запись добавляется поле ``source_cluster``.
"""
lim = max(1, min(limit, 100))
off = max(0, offset)
if cluster_name and str(cluster_name).strip():
n = str(cluster_name).strip()
if not validate_cluster_name(n):
return [], 0
raw = read_cluster_journal_sync(n)
if not raw or not isinstance(raw.get("entries"), list):
return [], 0
entries = [e for e in raw["entries"] if isinstance(e, dict)]
def _entry_time_key(e: dict[str, Any]) -> str:
return str(e.get("finished_at_utc") or e.get("created_at_utc") or "")
entries.sort(key=_entry_time_key, reverse=True)
total = len(entries)
chunk = entries[off : off + lim]
out: list[dict[str, Any]] = []
for e in chunk:
row = dict(e)
row["source_cluster"] = n
out.append(row)
return out, total
flat = _collect_all_journal_rows_sorted_sync()
total = len(flat)
chunk = flat[off : off + lim]
out: list[dict[str, Any]] = []
out2: list[dict[str, Any]] = []
for cluster_dir_name, e in chunk:
row = dict(e)
row["source_cluster"] = cluster_dir_name
out.append(row)
return out, total
out2.append(row)
return out2, total