UI/документация: крошки, метрики узлов в stats, правки навигации и подвала
- Документация: хлебные крошки; секции H2 в одной карточке; заголовок вкладки от H1 - Навигация: активна только текущая пилюля (Панель без постоянного home-стиля) - GET /api/v1/stats: cluster_resources (docker stats CPU/RAM/I/O по узлам kind) - Панель: блок ресурсов в карточке статистики; убраны строки подвала про api_routes/clusters - Удалён app/docs/README.md; крошки app/docs → api_routes.md; README корня обновлён
This commit is contained in:
@@ -248,6 +248,77 @@
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Блок «Ресурсы узлов» из cluster_resources (CPU/RAM/I/O из docker stats).
|
||||
*/
|
||||
function renderStatsResources(s) {
|
||||
const wrap = document.getElementById("stats-resources");
|
||||
if (!wrap) return;
|
||||
wrap.replaceChildren();
|
||||
if (s.cluster_resources_error) {
|
||||
const p = document.createElement("p");
|
||||
p.className = "muted stats-resources-msg";
|
||||
p.textContent = "Ресурсы узлов: " + s.cluster_resources_error;
|
||||
wrap.appendChild(p);
|
||||
return;
|
||||
}
|
||||
const list = s.cluster_resources || [];
|
||||
if (!list.length) {
|
||||
const p2 = document.createElement("p");
|
||||
p2.className = "muted stats-resources-msg";
|
||||
p2.textContent = "Нет кластеров в kind — метрики узлов пусты.";
|
||||
wrap.appendChild(p2);
|
||||
return;
|
||||
}
|
||||
const h3 = document.createElement("h3");
|
||||
h3.className = "stats-resources-heading";
|
||||
h3.textContent = "Ресурсы узлов (CPU, RAM, I/O)";
|
||||
wrap.appendChild(h3);
|
||||
list.forEach(function (cr) {
|
||||
const sec = document.createElement("section");
|
||||
sec.className = "stats-cluster-res";
|
||||
const h4 = document.createElement("h4");
|
||||
h4.className = "stats-cluster-res__title";
|
||||
h4.textContent = cr.cluster_name;
|
||||
sec.appendChild(h4);
|
||||
const nodes = cr.nodes || [];
|
||||
if (!nodes.length) {
|
||||
const pn = document.createElement("p");
|
||||
pn.className = "muted stats-cluster-res__note";
|
||||
pn.textContent = cr.note || "Нет запущенных контейнеров узлов.";
|
||||
sec.appendChild(pn);
|
||||
} else {
|
||||
nodes.forEach(function (n) {
|
||||
const row = document.createElement("div");
|
||||
row.className = "stats-node-res";
|
||||
const code = document.createElement("code");
|
||||
code.textContent = n.container_name;
|
||||
row.appendChild(code);
|
||||
const ul = document.createElement("ul");
|
||||
ul.className = "stats-node-res__metrics";
|
||||
function addMetric(label, val) {
|
||||
if (val == null || val === "") return;
|
||||
const li = document.createElement("li");
|
||||
const strong = document.createElement("strong");
|
||||
strong.textContent = label + ": ";
|
||||
li.appendChild(strong);
|
||||
li.appendChild(document.createTextNode(String(val)));
|
||||
ul.appendChild(li);
|
||||
}
|
||||
addMetric("CPU", n.cpu_percent);
|
||||
addMetric("Память", n.memory_usage);
|
||||
addMetric("% RAM", n.memory_percent);
|
||||
addMetric("Сеть", n.net_io);
|
||||
addMetric("Диск", n.block_io);
|
||||
addMetric("PID", n.pids != null ? String(n.pids) : null);
|
||||
row.appendChild(ul);
|
||||
sec.appendChild(row);
|
||||
});
|
||||
}
|
||||
wrap.appendChild(sec);
|
||||
});
|
||||
}
|
||||
|
||||
async function loadStats() {
|
||||
const dl = document.getElementById("stats-dl");
|
||||
const errEl = document.getElementById("stats-err");
|
||||
@@ -272,9 +343,12 @@
|
||||
frag.appendChild(dd);
|
||||
});
|
||||
dl.replaceChildren(frag);
|
||||
renderStatsResources(s);
|
||||
} catch (e) {
|
||||
errEl.textContent = "Статистика: " + e.message;
|
||||
errEl.classList.remove("hidden");
|
||||
const wrap = document.getElementById("stats-resources");
|
||||
if (wrap) wrap.replaceChildren();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,6 +35,50 @@
|
||||
return DOC_BASE + "?path=" + encodeURIComponent(relPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Хлебные крошки: Документация → файл (для app/docs/* кроме api_routes — ссылка «app/docs» на api_routes.md).
|
||||
*/
|
||||
function updateBreadcrumbs(docPath) {
|
||||
var ol = document.getElementById("doc-breadcrumbs-list");
|
||||
if (!ol) return;
|
||||
ol.replaceChildren();
|
||||
|
||||
function addLiLink(href, text) {
|
||||
var li = document.createElement("li");
|
||||
li.className = "doc-breadcrumbs__item";
|
||||
var a = document.createElement("a");
|
||||
a.href = href;
|
||||
a.className = "doc-breadcrumbs__link";
|
||||
a.textContent = text;
|
||||
li.appendChild(a);
|
||||
ol.appendChild(li);
|
||||
}
|
||||
|
||||
function addLiCurrent(text) {
|
||||
var li = document.createElement("li");
|
||||
li.className = "doc-breadcrumbs__item doc-breadcrumbs__item--current";
|
||||
li.setAttribute("aria-current", "page");
|
||||
li.textContent = text;
|
||||
ol.appendChild(li);
|
||||
}
|
||||
|
||||
addLiLink(DOC_BASE, "Документация");
|
||||
if (!docPath) {
|
||||
addLiCurrent("README.md (корень репозитория)");
|
||||
return;
|
||||
}
|
||||
if (docPath.indexOf("app/docs/") === 0) {
|
||||
if (docPath === "app/docs/api_routes.md") {
|
||||
addLiCurrent("api_routes.md");
|
||||
} else {
|
||||
addLiLink(docUrlForPath("app/docs/api_routes.md"), "app/docs");
|
||||
addLiCurrent(docPath.replace(/^.*\//, ""));
|
||||
}
|
||||
} else {
|
||||
addLiCurrent(docPath);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Внутренняя ссылка на .md под app/docs/: полный путь или относительно текущего файла.
|
||||
*/
|
||||
@@ -76,8 +120,27 @@
|
||||
});
|
||||
}
|
||||
|
||||
/** Текст первого H1 в разобранном HTML (для заголовка вкладки). */
|
||||
function extractFirstH1Text(root) {
|
||||
var h = root.querySelector("h1");
|
||||
if (!h) return "";
|
||||
return (h.textContent || "").replace(/\s+/g, " ").trim();
|
||||
}
|
||||
|
||||
/**
|
||||
* До первого H2 — одна карточка; для каждого H2 — карточка заголовка и карточка тела до следующего H2.
|
||||
* Заголовок вкладки: «Документация — <первый H1> — <app_title>» (как в api_routes: заголовок документа).
|
||||
*/
|
||||
function applyDocumentationTabTitle(h1Text) {
|
||||
var appTitle = (body.dataset.appTitle || "").trim() || "kind";
|
||||
if (h1Text) {
|
||||
document.title = "Документация — " + h1Text + " — " + appTitle;
|
||||
} else {
|
||||
document.title = "Документация — " + appTitle;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* До первого H2 — одна карточка; для каждого H2 — одна карточка: заголовок h2 и всё содержимое до следующего H2.
|
||||
*/
|
||||
function sectionizeFromNodes(sourceRoot) {
|
||||
var page = document.createElement("div");
|
||||
@@ -110,25 +173,23 @@
|
||||
appendCard("readme-section-card--intro", chunk);
|
||||
}
|
||||
|
||||
function flushH2Pair() {
|
||||
function flushH2Section() {
|
||||
if (i >= nodes.length) return;
|
||||
var h2 = nodes[i];
|
||||
if (h2.nodeType !== 1 || h2.tagName !== "H2") return;
|
||||
i++;
|
||||
appendCard("readme-section-card--head", [h2]);
|
||||
|
||||
var bodyChunk = [];
|
||||
var chunk = [h2];
|
||||
while (i < nodes.length) {
|
||||
var n = nodes[i];
|
||||
if (n.nodeType === 1 && n.tagName === "H2") break;
|
||||
bodyChunk.push(n);
|
||||
chunk.push(n);
|
||||
i++;
|
||||
}
|
||||
appendCard("readme-section-card--body", bodyChunk);
|
||||
appendCard("readme-section-card--block", chunk);
|
||||
}
|
||||
|
||||
flushIntro();
|
||||
while (i < nodes.length) flushH2Pair();
|
||||
while (i < nodes.length) flushH2Section();
|
||||
return page;
|
||||
}
|
||||
|
||||
@@ -136,6 +197,7 @@
|
||||
if (typeof marked === "undefined" || typeof DOMPurify === "undefined") {
|
||||
showErr(errEl, "Не загружены скрипты marked или DOMPurify из /static/js/vendor/.");
|
||||
if (loadEl) loadEl.classList.add("hidden");
|
||||
applyDocumentationTabTitle("");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -155,12 +217,16 @@
|
||||
temp.innerHTML = DOMPurify.sanitize(rawHtml);
|
||||
rewriteMdLinks(temp, docPath);
|
||||
|
||||
var h1ForTitle = extractFirstH1Text(temp);
|
||||
applyDocumentationTabTitle(h1ForTitle);
|
||||
|
||||
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
|
||||
var pageInner = sectionizeFromNodes(temp);
|
||||
rootEl.appendChild(pageInner);
|
||||
rootEl.removeAttribute("hidden");
|
||||
} catch (e2) {
|
||||
showErr(errEl, "Ошибка разбора: " + (e2.message || String(e2)));
|
||||
applyDocumentationTabTitle("");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -184,6 +250,8 @@
|
||||
while (rootEl.firstChild) rootEl.removeChild(rootEl.firstChild);
|
||||
loadEl.classList.remove("hidden");
|
||||
|
||||
updateBreadcrumbs(docPath);
|
||||
|
||||
var url = fetchUrlForPath(docPath);
|
||||
try {
|
||||
var r = await fetch(url, {
|
||||
@@ -200,6 +268,7 @@
|
||||
}
|
||||
showErr(errEl, "Не удалось загрузить документ: " + (detail || r.statusText));
|
||||
loadEl.classList.add("hidden");
|
||||
applyDocumentationTabTitle("");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -215,6 +284,7 @@
|
||||
} catch (e) {
|
||||
showErr(errEl, "Ошибка: " + (e.message || String(e)));
|
||||
loadEl.classList.add("hidden");
|
||||
applyDocumentationTabTitle("");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+121
-30
@@ -128,36 +128,17 @@ body.modal-open {
|
||||
outline-offset: 3px;
|
||||
}
|
||||
|
||||
/* Текущее приложение — акцентная пилюля */
|
||||
.nav-link.nav-pill--home {
|
||||
background: linear-gradient(145deg, rgba(59, 130, 246, 0.28), rgba(59, 130, 246, 0.1));
|
||||
border-color: rgba(59, 130, 246, 0.55);
|
||||
color: #93c5fd;
|
||||
}
|
||||
|
||||
@media (prefers-color-scheme: light) {
|
||||
.nav-link.nav-pill--home {
|
||||
color: #1d4ed8;
|
||||
}
|
||||
}
|
||||
|
||||
.nav-link.nav-pill--home:hover {
|
||||
color: var(--accent);
|
||||
background: linear-gradient(145deg, rgba(59, 130, 246, 0.35), rgba(59, 130, 246, 0.15));
|
||||
}
|
||||
|
||||
/* Активный раздел (панель или документация) */
|
||||
/* Активный раздел: «Панель» на дашборде или «Документация» на /documentation */
|
||||
.nav-link.nav-pill.nav-pill--active:not(.nav-pill--ext) {
|
||||
border-color: rgba(59, 130, 246, 0.85);
|
||||
box-shadow: 0 0 0 1px rgba(59, 130, 246, 0.35), 0 2px 14px rgba(59, 130, 246, 0.18);
|
||||
background: rgba(59, 130, 246, 0.16);
|
||||
color: #93c5fd;
|
||||
}
|
||||
.nav-link.nav-pill.nav-pill--home.nav-pill--active {
|
||||
background: linear-gradient(145deg, rgba(59, 130, 246, 0.42), rgba(59, 130, 246, 0.18));
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
.nav-link.nav-pill.nav-pill--active:not(.nav-pill--ext) {
|
||||
color: #1e40af;
|
||||
background: rgba(59, 130, 246, 0.12);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -232,18 +213,89 @@ body.modal-open {
|
||||
.stats-side-card {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
max-height: min(85vh, 52rem);
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.stats-side-card h2 {
|
||||
margin-bottom: 0.5rem;
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.stats-dl--compact {
|
||||
flex: 1;
|
||||
flex: 0 0 auto;
|
||||
margin: 0;
|
||||
font-size: 0.88rem;
|
||||
}
|
||||
|
||||
/* Метрики узлов kind (docker stats) под сводкой */
|
||||
.stats-resources {
|
||||
margin-top: 0.75rem;
|
||||
padding-top: 0.65rem;
|
||||
border-top: 1px solid var(--border);
|
||||
flex: 1 1 auto;
|
||||
min-height: 0;
|
||||
overflow-y: auto;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.stats-resources-heading {
|
||||
margin: 0 0 0.45rem;
|
||||
font-size: 0.8125rem;
|
||||
font-weight: 650;
|
||||
color: var(--muted);
|
||||
text-transform: uppercase;
|
||||
letter-spacing: 0.04em;
|
||||
}
|
||||
.stats-resources-msg {
|
||||
margin: 0;
|
||||
font-size: 0.8rem;
|
||||
}
|
||||
.stats-cluster-res {
|
||||
margin-bottom: 0.65rem;
|
||||
}
|
||||
.stats-cluster-res:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
.stats-cluster-res__title {
|
||||
margin: 0 0 0.35rem;
|
||||
font-size: 0.85rem;
|
||||
font-weight: 650;
|
||||
}
|
||||
.stats-cluster-res__note {
|
||||
margin: 0;
|
||||
font-size: 0.78rem;
|
||||
}
|
||||
.stats-node-res {
|
||||
margin-bottom: 0.45rem;
|
||||
padding: 0.35rem 0.45rem;
|
||||
border-radius: 6px;
|
||||
background: rgba(0, 0, 0, 0.12);
|
||||
border: 1px solid var(--border);
|
||||
}
|
||||
.stats-node-res code {
|
||||
display: block;
|
||||
font-size: 0.76rem;
|
||||
margin-bottom: 0.25rem;
|
||||
word-break: break-all;
|
||||
}
|
||||
.stats-node-res__metrics {
|
||||
margin: 0;
|
||||
padding-left: 1rem;
|
||||
list-style: disc;
|
||||
}
|
||||
.stats-node-res__metrics li {
|
||||
margin: 0.1rem 0;
|
||||
}
|
||||
.stats-node-res__metrics strong {
|
||||
font-weight: 600;
|
||||
color: var(--muted);
|
||||
}
|
||||
@media (prefers-color-scheme: light) {
|
||||
.stats-node-res {
|
||||
background: rgba(0, 0, 0, 0.04);
|
||||
}
|
||||
}
|
||||
|
||||
.create-cluster-card {
|
||||
margin-bottom: 1rem;
|
||||
}
|
||||
@@ -902,6 +954,48 @@ button.btn-danger:hover {
|
||||
filter: brightness(1.12);
|
||||
}
|
||||
|
||||
/* Хлебные крошки на странице документации */
|
||||
.doc-breadcrumbs {
|
||||
max-width: 52rem;
|
||||
margin: 0 auto 0.65rem;
|
||||
padding: 0 0.15rem;
|
||||
}
|
||||
.doc-breadcrumbs__list {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
gap: 0.35rem 0.15rem;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
list-style: none;
|
||||
font-size: 0.8125rem;
|
||||
}
|
||||
.doc-breadcrumbs__item {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 0.35rem;
|
||||
}
|
||||
.doc-breadcrumbs__item:not(:last-child)::after {
|
||||
content: "›";
|
||||
opacity: 0.45;
|
||||
font-weight: 500;
|
||||
margin-left: 0.2rem;
|
||||
}
|
||||
.doc-breadcrumbs__link {
|
||||
color: var(--accent);
|
||||
text-decoration: none;
|
||||
}
|
||||
.doc-breadcrumbs__link:hover {
|
||||
text-decoration: underline;
|
||||
text-underline-offset: 2px;
|
||||
}
|
||||
.doc-breadcrumbs__item--current {
|
||||
color: var(--muted);
|
||||
font-weight: 600;
|
||||
max-width: 100%;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
/* Страница «Документация»: Markdown → карточки секций */
|
||||
.readme-doc-shell {
|
||||
max-width: 52rem;
|
||||
@@ -925,10 +1019,10 @@ button.btn-danger:hover {
|
||||
.readme-section-card {
|
||||
margin: 0;
|
||||
}
|
||||
.readme-section-card--head h2 {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border-bottom: none;
|
||||
/* Заголовок секции (h2) и содержимое — в одной карточке: без лишнего зазора сверху у первого заголовка */
|
||||
.readme-section-card.markdown-body > h1:first-child,
|
||||
.readme-section-card.markdown-body > h2:first-child {
|
||||
margin-top: 0;
|
||||
}
|
||||
.readme-section-card.markdown-body {
|
||||
font-size: 0.95rem;
|
||||
@@ -952,9 +1046,6 @@ button.btn-danger:hover {
|
||||
border-bottom: 1px solid var(--border);
|
||||
padding-bottom: 0.25rem;
|
||||
}
|
||||
.readme-section-card--head.markdown-body h2 {
|
||||
margin: 0;
|
||||
}
|
||||
.readme-section-card.markdown-body h3 {
|
||||
font-size: 1.05rem;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user