Веб-UI: темы, навигация API, статистика и доработки API

- Шапка: логотип Kubernetes, ссылка на главную, выпадающее меню API (Swagger/ReDoc/Health), переключатель светлой/тёмной темы (localStorage).
- Светлая тема в синей гамме; выравнивание кнопки темы в ряду с пилюлями.
- Дашборд: единая карточка ошибки health/stats, подсказка Docker/Podman, поле container_cli в GET /stats, total_workers_from_meta всегда число (0 без meta).
- Правки кластеров, job_store, compose, документация и частичные шаблоны.
This commit is contained in:
Sergey Antropoff
2026-04-04 07:58:19 +03:00
parent aa8003061e
commit 6f3daa33ec
20 changed files with 1885 additions and 451 deletions
+17 -4
View File
@@ -17,6 +17,7 @@ from fastapi.responses import FileResponse, JSONResponse
from core.cluster_lifecycle import (
KindClusterError,
cluster_summary_for_api,
configured_container_cli,
create_cluster_non_interactive,
delete_kind_cluster_and_data,
kubectl_nodes_wide,
@@ -27,7 +28,11 @@ from core.cluster_lifecycle import (
stop_kind_cluster_containers,
validate_cluster_name,
)
from core.container_resource_stats import collect_kind_clusters_resource_stats
from core.container_resource_stats import (
aggregate_kind_cluster_resources,
collect_kind_clusters_resource_stats,
running_kind_clusters_mask,
)
from core.job_store import (
JobRecord,
end_job_tracking,
@@ -39,6 +44,7 @@ from core.job_store import (
from core.kind_guard import kind_cluster_lock
from kind_k8s_paths import clusters_dir
from models.schemas import (
AggregateResourcesSummary,
ClusterCreateAccepted,
ClusterCreateRequest,
ClusterSummary,
@@ -100,7 +106,6 @@ def _stats_sync() -> StatsResponse:
subdirs = sorted(p.name for p in cdir.iterdir() if p.is_dir() and not p.name.startswith("."))
total_workers = 0
counted = False
for name in subdirs:
meta = read_meta_json(name)
if not meta:
@@ -110,7 +115,6 @@ def _stats_sync() -> StatsResponse:
continue
try:
total_workers += int(w)
counted = True
except (TypeError, ValueError):
continue
@@ -118,15 +122,22 @@ def _stats_sync() -> StatsResponse:
failed = sum(1 for j in jobs if j.status == "failed")
res_blocks, res_err = collect_kind_clusters_resource_stats()
agg: AggregateResourcesSummary
if res_err:
agg = AggregateResourcesSummary()
else:
agg = aggregate_kind_cluster_resources(res_blocks)
return StatsResponse(
container_cli=configured_container_cli(),
kind_clusters_count=len(kind_names),
local_cluster_dirs_count=len(subdirs),
total_workers_from_meta=total_workers if counted else None,
total_workers_from_meta=total_workers,
jobs_total=len(jobs),
jobs_recent_failed=failed,
cluster_resources=res_blocks,
cluster_resources_error=res_err,
aggregate_cluster_resources=agg,
)
@@ -163,6 +174,7 @@ async def list_clusters() -> list[ClusterSummary]:
if cdir.is_dir():
dir_names = {p.name for p in cdir.iterdir() if p.is_dir() and not p.name.startswith(".")}
all_names = sorted(kind_names | dir_names)
running_map = running_kind_clusters_mask(all_names)
out: list[ClusterSummary] = []
for name in all_names:
summary = cluster_summary_for_api(name)
@@ -170,6 +182,7 @@ async def list_clusters() -> list[ClusterSummary]:
ClusterSummary(
name=str(summary["name"]),
registered_in_kind=bool(summary["registered_in_kind"]),
kind_nodes_running=bool(running_map.get(name, False)),
has_local_kubeconfig=bool(summary["has_local_kubeconfig"]),
meta=dict(summary["meta"]) if isinstance(summary.get("meta"), dict) else {},
)