Files
Sergey Antropoff f0b24c8901 UI: адаптив, журнал, спиннеры; docs: README и api_routes
- Навигация: выезжающее меню при узком экране (nav-mobile.js)
- Журнал: карточки <620px, компактная пагинация, время в две строки <920px
- Создание кластера: оверлей загрузки, инкрементальное обновление таблицы заданий
- Документация: полноэкранный спиннер при загрузке и навигации
- Главная: масштабирование CTA, статистика 2 колонки <520px, донаты перенос <710px
- README: env.example, новые фичи UI, автор в конце файла
- api_routes: маршрут /cluster-create, спиннеры, шаблоны; автор в конце
- env.example: автор перенесён в конец файла
2026-04-05 00:18:19 +03:00

280 lines
11 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

{# Общий каркас страниц веб-интерфейса Kind Clusters Dashboard.
Автор: Сергей Антропов — https://devops.org.ru #}
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<link rel="icon" href="/static/favicon.svg" type="image/svg+xml" />
<link rel="alternate icon" href="/favicon.ico" />
<title>{% block page_title %}{{ app_title }}{% endblock %} — kind</title>
{# Тема до первой отрисовки: localStorage kind_k8s_theme или prefers-color-scheme #}
<script>
(function () {
try {
var k = "kind_k8s_theme";
var v = localStorage.getItem(k);
var t =
v === "light" || v === "dark"
? v
: window.matchMedia("(prefers-color-scheme: light)").matches
? "light"
: "dark";
document.documentElement.setAttribute("data-theme", t);
} catch (e) {
document.documentElement.setAttribute("data-theme", "dark");
}
})();
</script>
<link rel="stylesheet" href="/static/style.css" />
<script src="/static/js/theme.js" defer></script>
<script src="/static/js/nav-mobile.js" defer></script>
{% block head_extra %}{% endblock %}
</head>
<body
class="app-body{% block body_extra_class %}{% endblock %}"
data-api-base="/api/v1"
data-app-title="{{ app_title | e }}"
{% block body_attrs %}{% endblock %}
>
<a class="skip-link" href="#main-content">К основному содержимому</a>
<header class="top-nav" role="banner">
<div class="top-nav-inner">
{# Логотип Kubernetes (SVG из набора Simple Icons, CC0) + название — одна ссылка на главную. #}
<a href="/" class="nav-brand nav-brand-link">
<img
src="/static/icons/kubernetes.svg"
alt=""
class="nav-logo-img"
width="28"
height="28"
decoding="async"
/>
<span class="nav-title">{{ app_title }}</span>
</a>
{# При ширине < 920px «гамбургер»; панель #top-nav-drawer справа (nav-mobile.js). #}
<button
type="button"
class="nav-menu-toggle"
id="nav-menu-toggle"
aria-expanded="false"
aria-controls="top-nav-drawer"
aria-label="Открыть меню разделов"
>
<span class="nav-menu-toggle__bars" aria-hidden="true">
<span class="nav-menu-toggle__bar"></span>
<span class="nav-menu-toggle__bar"></span>
<span class="nav-menu-toggle__bar"></span>
</span>
</button>
<div
class="nav-menu-backdrop"
id="nav-menu-backdrop"
hidden
tabindex="-1"
aria-hidden="true"
></div>
{# Один ряд flex (десктоп): пилюли, API, тема; на мобильных — содержимое в выезжающей панели #}
<nav class="nav-links top-nav-actions" id="top-nav-drawer" aria-label="Разделы и оформление">
{# Шапка панели только на узких экранах (CSS); кнопка дублирует закрытие с «гамбургера». #}
<div class="nav-drawer-header">
<span class="nav-drawer-title">Меню</span>
<button
type="button"
class="nav-drawer-close"
id="nav-drawer-close"
aria-label="Закрыть меню"
>
<svg viewBox="0 0 24 24" width="22" height="22" aria-hidden="true" focusable="false">
<path
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
d="M6 6l12 12M18 6L6 18"
/>
</svg>
</button>
</div>
{# Прокрутка только у пилюль: overflow-x на nav обрезал бы выпадающее меню API (position: absolute). #}
<div class="nav-links-scroll">
<a href="/" class="nav-link nav-pill{% if nav_active|default('') == 'panel' %} nav-pill--active{% endif %}">Панель</a>
<a href="/clusters" class="nav-link nav-pill{% if nav_active|default('') == 'clusters' %} nav-pill--active{% endif %}">Кластеры</a>
<a href="/cluster-addons" class="nav-link nav-pill{% if nav_active|default('') == 'cluster_addons' %} nav-pill--active{% endif %}">Аддоны</a>
<a href="/journal" class="nav-link nav-pill{% if nav_active|default('') == 'journal' %} nav-pill--active{% endif %}">Журнал</a>
<a href="/documentation" class="nav-link nav-pill{% if nav_active|default('') == 'documentation' %} nav-pill--active{% endif %}">Документация</a>
</div>
<div class="nav-dropdown">
<button
type="button"
class="nav-link nav-pill nav-dropdown__trigger"
id="nav-api-trigger"
aria-expanded="false"
aria-haspopup="true"
aria-controls="nav-api-menu"
>
API
</button>
<ul class="nav-dropdown__menu" id="nav-api-menu" role="menu" hidden>
<li role="none">
<a
role="menuitem"
href="/docs"
class="nav-dropdown__link nav-pill--ext"
data-open-window="kind_swagger"
>Swagger</a>
</li>
<li role="none">
<a
role="menuitem"
href="/redoc"
class="nav-dropdown__link nav-pill--ext"
data-open-window="kind_redoc"
>ReDoc</a>
</li>
<li role="none">
<a
role="menuitem"
href="/api/v1/health"
class="nav-dropdown__link nav-pill--ext"
data-open-window="kind_health"
>Health</a>
</li>
<li role="none">
<a
role="menuitem"
href="https://hub.docker.com/r/kindest/node/tags"
class="nav-dropdown__link nav-pill--ext"
target="_blank"
rel="noopener noreferrer"
title="Список тегов kindest/node на Docker Hub (новое окно)"
>Теги образов</a>
</li>
</ul>
</div>
<button type="button" id="theme-toggle" class="theme-toggle theme-toggle--nav-end">
<svg
class="theme-toggle__icon theme-toggle__icon--sun"
viewBox="0 0 24 24"
width="18"
height="18"
aria-hidden="true"
focusable="false"
>
<circle cx="12" cy="12" r="4" fill="none" stroke="currentColor" stroke-width="2" />
<path
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
d="M12 2v2M12 20v2M2 12h2M20 12h2M4.93 4.93l1.41 1.41M17.66 17.66l1.41 1.41M19.07 4.93l-1.41 1.41M6.34 17.66l-1.41 1.41"
/>
</svg>
<svg
class="theme-toggle__icon theme-toggle__icon--moon"
viewBox="0 0 24 24"
width="18"
height="18"
aria-hidden="true"
focusable="false"
>
<path
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linejoin="round"
d="M21 12.79A9 9 0 1 1 11.21 3 7 7 0 0 0 21 12.79z"
/>
</svg>
</button>
</nav>
</div>
</header>
<div id="toast" class="toast hidden" role="status" aria-live="polite"></div>
{# Плавающие подсказки для иконок (position:fixed; не режутся overflow таблицы). #}
<div id="action-tooltip" class="action-tooltip-floating hidden" role="tooltip"></div>
<main id="main-content" class="app-main" tabindex="-1">
{% block content %}{% endblock %}
</main>
<footer class="footer app-footer">
{% block footer %}
<div class="footer-inner">
<p class="muted footer-line">Данные: том <code>clusters/</code> на хосте</p>
<p class="footer-copyright">
© {{ app_title }} ·
<a href="https://devops.org.ru" target="_blank" rel="noopener">devops.org.ru</a>
</p>
</div>
{% endblock %}
</footer>
{# Отдельное окно для Swagger/ReDoc/Health; выпадающее меню API. #}
<script>
(function () {
var opts = "noopener,noreferrer,width=1240,height=840,scrollbars=yes,resizable=yes";
function bindOpenWindow(scope) {
(scope || document).querySelectorAll("[data-open-window]").forEach(function (a) {
if (a.tagName !== "A") return;
a.addEventListener("click", function (e) {
e.preventDefault();
var name = a.getAttribute("data-open-window") || "kind_ext";
window.open(a.getAttribute("href"), name, opts);
});
});
}
bindOpenWindow();
var dd = document.querySelector(".nav-dropdown");
if (!dd) return;
var btn = document.getElementById("nav-api-trigger");
var menu = document.getElementById("nav-api-menu");
if (!btn || !menu) return;
function closeApiMenu() {
dd.classList.remove("nav-dropdown--open");
btn.setAttribute("aria-expanded", "false");
menu.setAttribute("hidden", "");
}
function openApiMenu() {
dd.classList.add("nav-dropdown--open");
btn.setAttribute("aria-expanded", "true");
menu.removeAttribute("hidden");
}
function toggleApiMenu() {
if (menu.hasAttribute("hidden")) openApiMenu();
else closeApiMenu();
}
btn.addEventListener("click", function (e) {
e.stopPropagation();
toggleApiMenu();
});
menu.querySelectorAll("a[role='menuitem']").forEach(function (a) {
a.addEventListener("click", function () {
closeApiMenu();
});
});
/* Закрытие по клику вне блока API (фаза capture: не конфликтует с открытием по кнопке). */
document.addEventListener(
"click",
function (e) {
if (!dd.contains(e.target)) closeApiMenu();
},
true,
);
document.addEventListener("keydown", function (e) {
if (e.key === "Escape") closeApiMenu();
});
})();
</script>
{% block scripts %}{% endblock %}
</body>
</html>