UI: навигация, документация, favicon; журнал развёртывания; валидация формы

- Меню API: ссылка «Теги образов», обёртка прокрутки пилюль, z-index и padding против обрезки hover
- Документация: ширина колонки как у дашборда (72rem)
- Favicon SVG + GET /favicon.ico, link в base.html
- provision_log.json, GET .../provision-log, кнопка в таблице кластеров
- Валидация create: имя, workers, тег kindest; модалка alert
- Прочие правки из сессии (clusters, job_store, стили, шаблоны)
This commit is contained in:
Sergey Antropoff
2026-04-04 08:15:15 +03:00
parent 6f3daa33ec
commit 4546f50aef
19 changed files with 723 additions and 103 deletions
+14
View File
@@ -0,0 +1,14 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
Favicon 2 AB8;5 ;>3>B8?0 Kubernetes (:0: 2 H0?:5: static/icons/kubernetes.svg).
2B>@: !5@359 =B@>?>2  https://devops.org.ru
-->
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-hidden="true">
<!-- Прозрачный фон: читается и на светлой вкладке, и на тёмной теме браузера. -->
<g transform="translate(4 4)">
<path
fill="#326ce5"
d="M10.204 14.35l.007.01-.999 2.413a5.171 5.171 0 0 1-2.075-2.597l2.578-.437.004.005a.44.44 0 0 1 .484.606zm-.833-2.129a.44.44 0 0 0 .173-.756l.002-.011L7.585 9.7a5.143 5.143 0 0 0-.73 3.255l2.514-.725.002-.009zm1.145-1.98a.44.44 0 0 0 .699-.337l.01-.005.15-2.62a5.144 5.144 0 0 0-3.01 1.442l2.147 1.523.004-.002zm.76 2.75l.723.349.722-.347.18-.78-.5-.623h-.804l-.5.623.179.779zm1.5-3.095a.44.44 0 0 0 .7.336l.008.003 2.134-1.513a5.188 5.188 0 0 0-2.992-1.442l.148 2.615.002.001zm10.876 5.97l-5.773 7.181a1.6 1.6 0 0 1-1.248.594l-9.261.003a1.6 1.6 0 0 1-1.247-.596l-5.776-7.18a1.583 1.583 0 0 1-.307-1.34L2.1 5.573c.108-.47.425-.864.863-1.073L11.305.513a1.606 1.606 0 0 1 1.385 0l8.345 3.985c.438.209.755.604.863 1.073l2.062 8.955c.108.47-.005.963-.308 1.34zm-3.289-2.057c-.042-.01-.103-.026-.145-.034-.174-.033-.315-.025-.479-.038-.35-.037-.638-.067-.895-.148-.105-.04-.18-.165-.216-.216l-.201-.059a6.45 6.45 0 0 0-.105-2.332 6.465 6.465 0 0 0-.936-2.163c.052-.047.15-.133.177-.159.008-.09.001-.183.094-.282.197-.185.444-.338.743-.522.142-.084.273-.137.415-.242.032-.024.076-.062.11-.089.24-.191.295-.52.123-.736-.172-.216-.506-.236-.745-.045-.034.027-.08.062-.111.088-.134.116-.217.23-.33.35-.246.25-.45.458-.673.609-.097.056-.239.037-.303.033l-.19.135a6.545 6.545 0 0 0-4.146-2.003l-.012-.223c-.065-.062-.143-.115-.163-.25-.022-.268.015-.557.057-.905.023-.163.061-.298.068-.475.001-.04-.001-.099-.001-.142 0-.306-.224-.555-.5-.555-.275 0-.499.249-.499.555l.001.014c0 .041-.002.092 0 .128.006.177.044.312.067.475.042.348.078.637.056.906a.545.545 0 0 1-.162.258l-.012.211a6.424 6.424 0 0 0-4.166 2.003 8.373 8.373 0 0 1-.18-.128c-.09.012-.18.04-.297-.029-.223-.15-.427-.358-.673-.608-.113-.12-.195-.234-.329-.349-.03-.026-.077-.062-.111-.088a.594.594 0 0 0-.348-.132.481.481 0 0 0-.398.176c-.172.216-.117.546.123.737l.007.005.104.083c.142.105.272.159.414.242.299.185.546.338.743.522.076.082.09.226.1.288l.16.143a6.462 6.462 0 0 0-1.02 4.506l-.208.06c-.055.072-.133.184-.215.217-.257.081-.546.11-.895.147-.164.014-.305.006-.48.039-.037.007-.09.02-.133.03l-.004.002-.007.002c-.295.071-.484.342-.423.608.061.267.349.429.645.365l.007-.001.01-.003.129-.029c.17-.046.294-.113.448-.172.33-.118.604-.217.87-.256.112-.009.23.069.288.101l.217-.037a6.5 6.5 0 0 0 2.88 3.596l-.09.218c.033.084.069.199.044.282-.097.252-.263.517-.452.813-.091.136-.185.242-.268.399-.02.037-.045.095-.064.134-.128.275-.034.591.213.71.248.12.556-.007.69-.282v-.002c.02-.039.046-.09.062-.127.07-.162.094-.301.144-.458.132-.332.205-.68.387-.897.05-.06.13-.082.215-.105l.113-.205a6.453 6.453 0 0 0 4.609.012l.106.192c.086.028.18.042.256.155.136.232.229.507.342.84.05.156.074.295.145.457.016.037.043.09.062.129.133.276.442.402.69.282.247-.118.341-.435.213-.71-.02-.039-.045-.096-.065-.134-.083-.156-.177-.261-.268-.398-.19-.296-.346-.541-.443-.793-.04-.13.007-.21.038-.294-.018-.022-.059-.144-.083-.202a6.499 6.499 0 0 0 2.88-3.622c.064.01.176.03.213.038.075-.05.144-.114.28-.104.266.039.54.138.87.256.154.06.277.128.448.173.036.01.088.019.13.028l.009.003.007.001c.297.064.584-.098.645-.365.06-.266-.128-.537-.423-.608zM16.4 9.701l-1.95 1.746v.005a.44.44 0 0 0 .173.757l.003.01 2.526.728a5.199 5.199 0 0 0-.108-1.674A5.208 5.208 0 0 0 16.4 9.7zm-4.013 5.325a.437.437 0 0 0-.404-.232.44.44 0 0 0-.372.233h-.002l-1.268 2.292a5.164 5.164 0 0 0 3.326.003l-1.27-2.296h-.01zm1.888-1.293a.44.44 0 0 0-.27.036.44.44 0 0 0-.214.572l-.003.004 1.01 2.438a5.15 5.15 0 0 0 2.081-2.615l-2.6-.44-.004.005z"
/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 3.9 KiB

+250 -12
View File
@@ -35,6 +35,18 @@
/** local_cluster_dirs_count из GET /stats; показывается справа от Kubectl. */
var cachedLocalClusterDirsCount = null;
/**
* Кэш GET /versions для проверки тега перед созданием кластера.
* null — загрузка ещё не завершена; [] — каталог пуст или отключён; иначе список тегов.
*/
var cachedKindestTags = null;
/** true, если запрос /versions упал (сеть, 5xx и т.д.). */
var cachedKindestVersionsLoadError = false;
/** true, если сервер вернул skipped (например KIND_K8S_SKIP_VERSION_LIST). */
var cachedKindestTagsSkipped = false;
/** Режим модалки подтверждения: только кнопка «Понятно» (Escape закрывает как OK). */
var confirmModalIsAlert = false;
function isHomePage() {
return (document.body.getAttribute("data-dashboard-mode") || "home") === "home";
}
@@ -115,6 +127,37 @@
* Скачать kubeconfig кластера (GET /clusters/{name}/kubeconfig).
* @param {string} clusterName
*/
function isProvisionLogModalOpen() {
const el = document.getElementById("provision-log-modal-overlay");
return !!(el && !el.classList.contains("hidden"));
}
function closeProvisionLogModal() {
const el = document.getElementById("provision-log-modal-overlay");
if (el) el.classList.add("hidden");
}
/**
* Загрузить ``GET /clusters/{name}/provision-log`` и показать JSON в модалке.
* @param {string} clusterName
*/
function openProvisionLogModal(clusterName) {
const overlay = document.getElementById("provision-log-modal-overlay");
const sub = document.getElementById("provision-log-modal-sub");
const pre = document.getElementById("provision-log-body");
if (!overlay || !pre) return;
pre.textContent = "Загрузка…";
if (sub) sub.textContent = "Кластер: " + clusterName;
overlay.classList.remove("hidden");
api("/clusters/" + encodeURIComponent(clusterName) + "/provision-log")
.then(function (data) {
pre.textContent = JSON.stringify(data, null, 2);
})
.catch(function (e) {
pre.textContent = "Ошибка: " + e.message;
});
}
function downloadKubeconfig(clusterName) {
const url = API + "/clusters/" + encodeURIComponent(clusterName) + "/kubeconfig";
const a = document.createElement("a");
@@ -137,6 +180,8 @@
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/><polyline points="7 10 12 15 17 10"/><line x1="12" y1="15" x2="12" y2="3"/></svg>',
trash:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><polyline points="3 6 5 6 21 6"/><path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"/><line x1="10" y1="11" x2="10" y2="17"/><line x1="14" y1="11" x2="14" y2="17"/></svg>',
logFile:
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"/><polyline points="14 2 14 8 20 8"/><line x1="16" y1="13" x2="8" y2="13"/><line x1="16" y1="17" x2="8" y2="17"/><line x1="10" y1="9" x2="8" y2="9"/></svg>',
};
/** @type {ReturnType<typeof setTimeout> | null} */
@@ -618,32 +663,139 @@
const sel = document.getElementById("version-select");
const verInput = document.getElementById("kubernetes_version");
if (!sel || !verInput) return;
cachedKindestTags = null;
cachedKindestVersionsLoadError = false;
cachedKindestTagsSkipped = false;
sel.innerHTML = "<option value=\"\">— загрузка —</option>";
try {
const data = await api("/versions");
cachedKindestTagsSkipped = !!data.skipped;
sel.innerHTML = "";
if (!data.tags || !data.tags.length) {
cachedKindestTags = [];
sel.innerHTML = "<option value=\"\">(список пуст — введите версию вручную)</option>";
return;
}
cachedKindestTags = data.tags.slice();
const opt0 = document.createElement("option");
opt0.value = "";
opt0.textContent = "— выберите тег —";
sel.appendChild(opt0);
data.tags.slice(0, 100).forEach(function (t) {
/* Весь список с бэкенда: первым идёт latest, далее семверы от новых к старым (раньше slice(0,100) отрезал 1.19 и др.) */
data.tags.forEach(function (t) {
const o = document.createElement("option");
o.value = t;
o.textContent = t;
o.textContent =
t === "latest" ? "latest (самый новый образ kindest/node)" : t;
sel.appendChild(o);
});
sel.onchange = function () {
if (sel.value) verInput.value = sel.value.replace(/^v/, "");
if (!sel.value) return;
verInput.value = sel.value === "latest" ? "latest" : sel.value.replace(/^v/, "");
};
} catch (e) {
cachedKindestVersionsLoadError = true;
cachedKindestTags = null;
sel.innerHTML = "<option value=\"\">(ошибка загрузки тегов)</option>";
}
}
/**
* Имя кластера: DNS-подмножество, как на бэкенде (a-z0-9-, не длиннее 63).
* @param {string} name
*/
function isValidClusterName(name) {
const s = String(name || "").trim();
if (!s || s.length > 63) return false;
return /^[a-z0-9]([-a-z0-9]*[a-z0-9])?$/.test(s);
}
/**
* Число worker-нод 0…20; иначе null.
* @param {unknown} raw
* @returns {number | null}
*/
function parseWorkersBounded(raw) {
const n = parseInt(String(raw === undefined || raw === null ? "" : raw), 10);
if (Number.isNaN(n) || n < 0 || n > 20) return null;
return n;
}
/**
* Нормализация тега kindest/node для сравнения с каталогом (latest или vX.Y.Z).
* @param {string} raw
*/
function normalizeKindestTagForLookup(raw) {
let s = String(raw || "").trim().toLowerCase();
if (!s) return "";
if (s === "latest" || s === "vlatest") return "latest";
s = s.replace(/^v/, "");
return "v" + s;
}
/**
* Проверка поля версии перед POST /clusters: каталог доступен и тег в списке.
* @param {string} kubernetesVersionRaw
* @returns {{ ok: true } | { ok: false, modalTitle: string, modalMessage: string }}
*/
function validateCreateFormKubernetesVersion(kubernetesVersionRaw) {
const normalized = normalizeKindestTagForLookup(kubernetesVersionRaw);
if (!normalized) {
return {
ok: false,
modalTitle: "Нет версии Kubernetes",
modalMessage:
"Заполните поле «Версия Kubernetes / тег node» (например 1.29.4, v1.29.4 или latest) или выберите значение в списке «Тег образа».",
};
}
if (cachedKindestVersionsLoadError) {
return {
ok: false,
modalTitle: "Каталог тегов недоступен",
modalMessage:
"Не удалось загрузить список тегов kindest/node с сервера. Проверьте соединение и обновите страницу. Пока каталог не загружен, нельзя убедиться, что образ существует.",
};
}
if (cachedKindestTags === null) {
return {
ok: false,
modalTitle: "Список тегов загружается",
modalMessage:
"Подождите, пока в поле «Тег образа» завершится загрузка (не должно оставаться «— загрузка —»), и повторите отправку формы.",
};
}
if (!cachedKindestTags.length) {
const skipHint = cachedKindestTagsSkipped
? " На сервере отключена выдача списка (переменная KIND_K8S_SKIP_VERSION_LIST)."
: "";
return {
ok: false,
modalTitle: "Каталог тегов недоступен",
modalMessage:
"Приложение не получило ни одного тега kindest/node для проверки." +
skipHint +
" Создание кластера с непроверенным тегом отключено. Включите загрузку каталога или обратитесь к администратору.",
};
}
const canonSet = new Set(cachedKindestTags.map(normalizeKindestTagForLookup));
if (canonSet.has(normalized)) {
return { ok: true };
}
return {
ok: false,
modalTitle: "Тег не найден в каталоге",
modalMessage:
"Значение «" +
String(kubernetesVersionRaw).trim() +
"» отсутствует в списке доступных тегов kindest/node в приложении. Выберите тег из списка «Тег образа» или введите поддерживаемый семвер (см. подпись поля и Docker Hub).",
};
}
function jobBadgeClass(status) {
if (status === "success") return "badge badge-ok";
if (status === "failed") return "badge badge-err";
@@ -749,6 +901,19 @@
!c.has_local_kubeconfig,
),
);
if (c.has_provision_log) {
td.appendChild(
iconActionButton(
ICONS.logFile,
"Журнал развёртывания: полный JSON последнего создания или старта кластера",
"icon-btn--secondary",
function () {
openProvisionLogModal(c.name);
},
false,
),
);
}
td.appendChild(
iconActionButton(
ICONS.trash,
@@ -923,18 +1088,26 @@
*/
function closeConfirmModal(confirmed) {
const ov = document.getElementById("confirm-modal-overlay");
const cancelBtn = document.getElementById("confirm-modal-cancel");
if (ov) ov.classList.add("hidden");
document.body.classList.remove("modal-open");
if (cancelBtn) cancelBtn.classList.remove("hidden");
if (confirmModalResolver) {
var fn = confirmModalResolver;
confirmModalResolver = null;
fn(!!confirmed);
var out = confirmModalIsAlert ? true : !!confirmed;
confirmModalIsAlert = false;
fn(out);
} else {
confirmModalIsAlert = false;
}
}
/**
* Показать модальное подтверждение (вместо window.confirm).
* @param {{ title?: string, message: string, confirmLabel?: string, danger?: boolean }} opts
* При opts.alert — одна кнопка «Понятно», клик по фону и Escape закрывают как подтверждение.
*
* @param {{ title?: string, message: string, confirmLabel?: string, danger?: boolean, alert?: boolean }} opts
* @returns {Promise<boolean>}
*/
function openConfirmModal(opts) {
@@ -943,18 +1116,30 @@
const titleEl = document.getElementById("confirm-modal-title");
const msgEl = document.getElementById("confirm-modal-message");
const okBtn = document.getElementById("confirm-modal-ok");
const cancelBtn = document.getElementById("confirm-modal-cancel");
if (!ov || !titleEl || !msgEl || !okBtn) {
resolve(false);
return;
}
/* Снять предыдущее ожидание без логики «alert = всегда true» (иначе Promise подтверждения ломается). */
if (confirmModalResolver) {
closeConfirmModal(false);
const prevFn = confirmModalResolver;
confirmModalResolver = null;
confirmModalIsAlert = false;
prevFn(false);
ov.classList.add("hidden");
document.body.classList.remove("modal-open");
if (cancelBtn) cancelBtn.classList.remove("hidden");
}
confirmModalResolver = resolve;
confirmModalIsAlert = !!opts.alert;
titleEl.textContent = opts.title || "Подтвердите действие";
msgEl.textContent = opts.message || "";
okBtn.textContent = opts.confirmLabel || "Подтвердить";
okBtn.textContent = opts.confirmLabel || (confirmModalIsAlert ? "Понятно" : "Подтвердить");
okBtn.className = opts.danger ? "btn-danger" : "";
if (cancelBtn) {
cancelBtn.classList.toggle("hidden", confirmModalIsAlert);
}
ov.classList.remove("hidden");
document.body.classList.add("modal-open");
okBtn.focus();
@@ -1256,10 +1441,47 @@
if (details) details.classList.add("hidden");
setProgressHint(null, "create");
const fd = new FormData(form);
const nameRaw = String(fd.get("name") || "").trim();
const k8sVer = String(fd.get("kubernetes_version") || "").trim();
const workersParsed = parseWorkersBounded(fd.get("workers"));
if (!isValidClusterName(nameRaw)) {
await openConfirmModal({
alert: true,
title: "Некорректное имя кластера",
message:
"Имя должно состоять из строчных латинских букв, цифр и дефиса (a-z, 0-9, -), начинаться и заканчиваться буквой или цифрой, длина не более 63 символов. Пример: dev, my-cluster-1.",
confirmLabel: "Понятно",
});
return;
}
if (workersParsed === null) {
await openConfirmModal({
alert: true,
title: "Некорректное число worker-нод",
message:
"Укажите целое число worker-нод от 0 до 20 включительно.",
confirmLabel: "Понятно",
});
return;
}
const verCheck = validateCreateFormKubernetesVersion(k8sVer);
if (!verCheck.ok) {
await openConfirmModal({
alert: true,
title: verCheck.modalTitle,
message: verCheck.modalMessage,
confirmLabel: "Понятно",
});
return;
}
const body = {
name: String(fd.get("name") || "").trim(),
kubernetes_version: String(fd.get("kubernetes_version") || "").trim(),
workers: parseInt(String(fd.get("workers") || "0"), 10),
name: nameRaw,
kubernetes_version: k8sVer,
workers: workersParsed,
};
try {
const res = await api("/clusters", {
@@ -1310,7 +1532,11 @@
document.addEventListener("keydown", function (ev) {
if (ev.key !== "Escape") return;
if (isConfirmModalOpen()) {
closeConfirmModal(false);
closeConfirmModal(confirmModalIsAlert ? true : false);
return;
}
if (isProvisionLogModalOpen()) {
closeProvisionLogModal();
return;
}
hideActionTooltip();
@@ -1332,7 +1558,18 @@
}
if (confirmOv) {
confirmOv.addEventListener("click", function (ev) {
if (ev.target === confirmOv) closeConfirmModal(false);
if (ev.target === confirmOv) closeConfirmModal(confirmModalIsAlert ? true : false);
});
}
const provLogOv = document.getElementById("provision-log-modal-overlay");
const provLogClose = document.getElementById("provision-log-modal-close");
if (provLogClose) {
provLogClose.addEventListener("click", closeProvisionLogModal);
}
if (provLogOv) {
provLogOv.addEventListener("click", function (ev) {
if (ev.target === provLogOv) closeProvisionLogModal();
});
}
@@ -1365,6 +1602,7 @@
loadVersions();
}
bindActionTooltipHosts(document.getElementById("modal-overlay"));
bindActionTooltipHosts(document.getElementById("provision-log-modal-overlay"));
}
if (document.readyState === "loading") {
+124 -42
View File
@@ -112,14 +112,34 @@ body.modal-open {
justify-content: flex-end;
flex: 1 1 auto;
min-width: 0;
/* Не overflow-x: здесь — иначе обрезается .nav-dropdown__menu (absolute под кнопкой API). */
overflow: visible;
}
/* Горизонтальная прокрутка только подписей «Панель» / «Документация», без выпадающего меню. */
.nav-links-scroll {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 0.45rem;
min-width: 0;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
scrollbar-width: thin;
/*
* При overflow-x: auto вертикаль становится auto — обрезает translateY(-1px) и тень у .nav-pill:hover.
* Внутренний padding даёт место сверху/снизу; API вне .nav-links-scroll, там обрезки не было.
* z-index как у .nav-dropdown — общий stacking context при overflow.
*/
padding-top: 0.45rem;
padding-bottom: 0.45rem;
position: relative;
z-index: 160;
}
/* Выпадающее меню «API»: Swagger, ReDoc, Health в отдельном окне */
.nav-dropdown {
position: relative;
z-index: 160;
display: inline-flex;
align-items: center;
flex-shrink: 0;
@@ -339,14 +359,6 @@ html[data-theme="light"] .nav-link.nav-pill.nav-pill--active:not(.nav-pill--ext)
outline: none;
}
/* Страница /documentation: основной блок и подвал на всю ширину окна */
.doc-layout-full .app-main,
.doc-layout-full .app-footer {
max-width: none;
width: 100%;
box-sizing: border-box;
}
.app-footer {
max-width: 72rem;
margin: 0 auto;
@@ -690,54 +702,112 @@ html[data-theme="light"] .cluster-resources-expand-cell {
}
.cluster-create-hero {
margin-bottom: 1.35rem;
}
/* Карточка формы: чуть плотнее внутри, заголовок отступает от полей */
.card.create-cluster-card {
padding: 0.85rem 1rem 1rem;
margin-bottom: 1rem;
}
.create-cluster-card {
margin-bottom: 1rem;
.create-cluster-card__title {
margin: 0 0 1rem;
padding: 0;
font-size: 1.02rem;
}
/* Форма создания: две колонки, кнопка на всю ширину */
/* Сетка 2×2: имя | тег, затем подсказка на всю ширину, workers | версия; выравнивание по нижнему краю ячеек */
.create-form-grid {
display: grid;
gap: 0.75rem 1.5rem;
margin-top: 0.25rem;
gap: 0.4rem 1.15rem;
margin-top: 0;
}
.create-form-field {
display: flex;
flex-direction: column;
gap: 0.2rem;
min-width: 0;
}
.create-form-field > label {
margin: 0;
font-size: 0.8125rem;
line-height: 1.3;
font-weight: 600;
}
.create-form-grid .create-form-field input,
.create-form-grid .create-form-field select {
width: 100%;
max-width: none;
margin: 0;
padding: 0.35rem 0.5rem;
min-height: 2.3rem;
box-sizing: border-box;
line-height: 1.25;
}
@media (min-width: 640px) {
.create-form-grid {
grid-template-columns: 1fr 1fr;
align-items: start;
align-items: end;
}
}
.create-form-col > label:first-of-type {
margin-top: 0;
}
.create-form-span {
grid-column: 1 / -1;
}
/* Ссылка на актуальные теги kindest/node (Docker Hub), новая вкладка */
.create-form-doc-link {
color: var(--accent);
font-weight: 650;
text-decoration: none;
}
.create-form-doc-link:hover {
text-decoration: underline;
text-underline-offset: 2px;
}
.create-form-doc-link:focus-visible {
outline: 2px solid var(--accent);
outline-offset: 2px;
border-radius: 3px;
}
.create-form-doc-link__ext {
margin-left: 0.12rem;
font-size: 0.82em;
font-weight: 700;
color: var(--accent);
opacity: 0.85;
}
.create-form-label-rest {
font-weight: 600;
}
.create-ver-hint {
margin: 0.25rem 0 0;
font-size: 0.85rem;
margin: 0;
font-size: 0.8rem;
line-height: 1.35;
}
.create-ver-hint--full {
grid-column: 1 / -1;
margin: 0.1rem 0 0.25rem;
}
.create-form-grid .create-actions {
margin-top: 0.25rem;
margin-top: 0.35rem;
padding-top: 0.15rem;
width: 100%;
justify-content: center;
}
.create-form-grid .create-actions button {
margin-top: 0;
}
.create-form-col input,
.create-form-col select {
max-width: none;
width: 100%;
}
/* Одна карточка: заголовок + описание + строка состояния среды */
.hero-panel {
margin-bottom: 1rem;
@@ -1342,6 +1412,26 @@ html[data-theme="light"] .icon-btn--danger {
.confirm-modal-overlay {
z-index: 150;
}
/* Журнал развёртывания: между окном «Состояние» (100) и подтверждением (150) */
.provision-log-modal-overlay {
z-index: 120;
}
.modal-box--provision-log .provision-log-body {
max-height: min(70vh, 32rem);
overflow: auto;
margin: 0.35rem 0 0;
padding: 0.65rem 0.75rem;
font-size: 0.78rem;
line-height: 1.45;
white-space: pre-wrap;
word-break: break-word;
border-radius: 8px;
border: 1px solid var(--border);
background: rgba(0, 0, 0, 0.2);
}
html[data-theme="light"] .modal-box--provision-log .provision-log-body {
background: rgba(0, 0, 0, 0.04);
}
.modal-box--confirm {
max-width: 24rem;
text-align: center;
@@ -1407,17 +1497,13 @@ button.btn-danger:hover {
filter: brightness(1.12);
}
/* Хлебные крошки на странице документации */
/* Хлебные крошки на странице документации — ширина как у .app-main (с дашбордом). */
.doc-breadcrumbs {
max-width: 52rem;
margin: 0 auto 0.65rem;
padding: 0 0.15rem;
}
.doc-layout-full .doc-breadcrumbs {
max-width: none;
margin-left: 0;
margin-right: 0;
width: 100%;
margin: 0 0 0.65rem;
padding: 0;
box-sizing: border-box;
}
.doc-breadcrumbs__list {
display: flex;
@@ -1455,16 +1541,12 @@ button.btn-danger:hover {
word-break: break-word;
}
/* Страница «Документация»: Markdown → карточки секций */
/* Страница «Документация»: Markdown → карточки; ширина блока = ширина main (как card на дашборде). */
.readme-doc-shell {
max-width: 52rem;
margin: 0 auto;
}
.doc-layout-full .readme-doc-shell {
max-width: none;
margin-left: 0;
margin-right: 0;
width: 100%;
margin: 0;
box-sizing: border-box;
}
.readme-doc-loading {
margin: 0 0 0.75rem;