docs: скриншоты UI (светлая/тёмная тема) в app/docs и README
- app/docs/screenshots.md и каталог app/docs/images/*.png - раздача /static/docs-images/* из FastAPI; documentation.js переписывает src картинок - стили .markdown-body img; строка в api_routes.md; превью в README
@@ -1,7 +1,5 @@
|
|||||||
# Kind Clusters Dashboard — локальные кластеры Kubernetes (kind)
|
# Kind Clusters Dashboard — локальные кластеры Kubernetes (kind)
|
||||||
|
|
||||||
**Репозиторий Git:** `ssh://git@git.antropoff.ru:222/DevOpsTools/KindClustersDashboard.git` — клон: `git clone ssh://git@git.antropoff.ru:222/DevOpsTools/KindClustersDashboard.git`. Уже клонировали раньше: `git remote set-url origin ssh://git@git.antropoff.ru:222/DevOpsTools/KindClustersDashboard.git`.
|
|
||||||
|
|
||||||
Образ **kind-k8s-tools:local** и **Makefile** поднимают **веб-интерфейс** (FastAPI) на порту **8080** на хосте по умолчанию (или **`KIND_K8S_WEB_PORT`** в `.env`; внутри контейнера приложение слушает **6000**). Порт **6000 на хосте** не используем по умолчанию: Chrome и другие браузеры на Chromium отдают **ERR_UNSAFE_PORT**. Через браузер создаёте и удаляете кластеры, смотрите статистику и вывод `kubectl`. **kubeconfig** сохраняется в `clusters/<имя>/`. На хосте достаточно **Docker** (или Podman) и **make**; **kind** и **kubectl** — внутри контейнера.
|
Образ **kind-k8s-tools:local** и **Makefile** поднимают **веб-интерфейс** (FastAPI) на порту **8080** на хосте по умолчанию (или **`KIND_K8S_WEB_PORT`** в `.env`; внутри контейнера приложение слушает **6000**). Порт **6000 на хосте** не используем по умолчанию: Chrome и другие браузеры на Chromium отдают **ERR_UNSAFE_PORT**. Через браузер создаёте и удаляете кластеры, смотрите статистику и вывод `kubectl`. **kubeconfig** сохраняется в `clusters/<имя>/`. На хосте достаточно **Docker** (или Podman) и **make**; **kind** и **kubectl** — внутри контейнера.
|
||||||
|
|
||||||
## Документация
|
## Документация
|
||||||
@@ -9,8 +7,17 @@
|
|||||||
| Ресурс | Описание |
|
| Ресурс | Описание |
|
||||||
|--------|----------|
|
|--------|----------|
|
||||||
| **[app/docs/api_routes.md](app/docs/api_routes.md)** | Описание REST API `/api/v1/*` с примерами JSON (для фронтенда и интеграций) |
|
| **[app/docs/api_routes.md](app/docs/api_routes.md)** | Описание REST API `/api/v1/*` с примерами JSON (для фронтенда и интеграций) |
|
||||||
|
| **[app/docs/screenshots.md](app/docs/screenshots.md)** | Скриншоты веб-интерфейса (**светлая** и **тёмная** тема); файлы в **`app/docs/images/`** |
|
||||||
| **`/docs`** (Swagger), **`/redoc`**, **`/api/v1/health`** | На панели открываются в **отдельном окне** браузера (`window.open`); прямой URL — тот же порт, что и UI (по умолчанию **8080**) |
|
| **`/docs`** (Swagger), **`/redoc`**, **`/api/v1/health`** | На панели открываются в **отдельном окне** браузера (`window.open`); прямой URL — тот же порт, что и UI (по умолчанию **8080**) |
|
||||||
|
|
||||||
|
### Скриншоты (превью)
|
||||||
|
|
||||||
|
Полная галерея и подписи — в **[app/docs/screenshots.md](app/docs/screenshots.md)**. Ниже — главная панель в обеих темах.
|
||||||
|
|
||||||
|
| Светлая тема | Тёмная тема |
|
||||||
|
| :------------: | :-----------: |
|
||||||
|
|  |  |
|
||||||
|
|
||||||
В корне репозитория — файл **`env.example`**: перечислены **только имена** переменных (без значений), для ориентира при ручной настройке **`.env`**. Полноценно создать **`.env`** можно интерактивно скриптом **`scripts/setup_env_interactive.py`** (`make setup`; в начале — выбор **docker** или **podman**, путь **`CONTAINER_SOCKET`** подставляется автоматически).
|
В корне репозитория — файл **`env.example`**: перечислены **только имена** переменных (без значений), для ориентира при ручной настройке **`.env`**. Полноценно создать **`.env`** можно интерактивно скриптом **`scripts/setup_env_interactive.py`** (`make setup`; в начале — выбор **docker** или **podman**, путь **`CONTAINER_SOCKET`** подставляется автоматически).
|
||||||
|
|
||||||
## Зачем это нужно
|
## Зачем это нужно
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
| `GET /documentation` | HTML-оболочка; **`documentation.js`**: без `path` — **`GET /api/v1/docs/readme`**, с `?path=app/docs/…` — **`GET /api/v1/docs/file`**; разбор Markdown из **`/static/js/vendor/`** (marked, DOMPurify). Полноэкранный спиннер при первой загрузке и при **каждом** переходе по внутренним ссылкам / **popstate**. Каждая секция по **H2** — **одна карточка** (заголовок h2 и содержимое до следующего h2 вместе). Заголовок вкладки браузера: **«Документация — …»** + текст **первого H1** документа + имя приложения (`KIND_K8S_APP_TITLE` на `body`). В шапке на этой странице активна только **Документация**; **Панель** как обычная пилюля (на дашборде активна **Панель**). Путь к README: `KIND_K8S_README_PATH` или `README.md` рядом с `app/`; в образе — `/opt/kind-k8s/README.md`. |
|
| `GET /documentation` | HTML-оболочка; **`documentation.js`**: без `path` — **`GET /api/v1/docs/readme`**, с `?path=app/docs/…` — **`GET /api/v1/docs/file`**; разбор Markdown из **`/static/js/vendor/`** (marked, DOMPurify). Полноэкранный спиннер при первой загрузке и при **каждом** переходе по внутренним ссылкам / **popstate**. Каждая секция по **H2** — **одна карточка** (заголовок h2 и содержимое до следующего h2 вместе). Заголовок вкладки браузера: **«Документация — …»** + текст **первого H1** документа + имя приложения (`KIND_K8S_APP_TITLE` на `body`). В шапке на этой странице активна только **Документация**; **Панель** как обычная пилюля (на дашборде активна **Панель**). Путь к README: `KIND_K8S_README_PATH` или `README.md` рядом с `app/`; в образе — `/opt/kind-k8s/README.md`. |
|
||||||
| `GET /ui` | Редирект **307** на `/` (удобный ярлык). |
|
| `GET /ui` | Редирект **307** на `/` (удобный ярлык). |
|
||||||
| `GET /static/…` | CSS (`style.css`), скрипты панели (`js/dashboard.js`) и документации (`js/documentation.js`); базовый URL API задаётся атрибутом `data-api-base` на `<body>` (по умолчанию `/api/v1`). |
|
| `GET /static/…` | CSS (`style.css`), скрипты панели (`js/dashboard.js`) и документации (`js/documentation.js`); базовый URL API задаётся атрибутом `data-api-base` на `<body>` (по умолчанию `/api/v1`). |
|
||||||
|
| `GET /static/docs-images/…` | PNG из **`app/docs/images/`** для страницы **`/documentation`** (в Markdown пути вида `images/*.png` в **`documentation.js`** переписываются на этот префикс; см. **`app/docs/screenshots.md`**). |
|
||||||
|
|
||||||
Шаблоны: `app/templates/base.html` (шапка, навигация; **меню «гамбургер»** при узком viewport — `nav-mobile.js`), `app/templates/dashboard.html` (панель), `app/templates/clusters.html` (список кластеров и донаты узлов), `app/templates/cluster_create.html` (создание кластера и последние задания), `app/templates/cluster_detail.html` (страница кластера), `app/templates/cluster_edit.html` (редактирование конфигурации), `app/templates/cluster_addons.html` (Helm-аддоны), `app/templates/journal.html` (журнал заданий), `app/templates/documentation.html` (README и `app/docs/*.md`).
|
Шаблоны: `app/templates/base.html` (шапка, навигация; **меню «гамбургер»** при узком viewport — `nav-mobile.js`), `app/templates/dashboard.html` (панель), `app/templates/clusters.html` (список кластеров и донаты узлов), `app/templates/cluster_create.html` (создание кластера и последние задания), `app/templates/cluster_detail.html` (страница кластера), `app/templates/cluster_edit.html` (редактирование конфигурации), `app/templates/cluster_addons.html` (Helm-аддоны), `app/templates/journal.html` (журнал заданий), `app/templates/documentation.html` (README и `app/docs/*.md`).
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 257 KiB |
|
After Width: | Height: | Size: 259 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 326 KiB |
|
After Width: | Height: | Size: 287 KiB |
|
After Width: | Height: | Size: 226 KiB |
|
After Width: | Height: | Size: 522 KiB |
|
After Width: | Height: | Size: 485 KiB |
|
After Width: | Height: | Size: 302 KiB |
|
After Width: | Height: | Size: 268 KiB |
@@ -0,0 +1,63 @@
|
|||||||
|
# Скриншоты интерфейса
|
||||||
|
|
||||||
|
Снимки экрана в **светлой** и **тёмной** теме (переключатель темы в шапке). Исходные файлы лежат в каталоге **`app/docs/images/`** (в репозитории рядом с этим файлом).
|
||||||
|
|
||||||
|
Открыть в веб-приложении: страница **«Документация»** → путь **`app/docs/screenshots.md`**.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Главная панель
|
||||||
|
|
||||||
|
**Светлая тема**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Тёмная тема**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Страница «Кластеры»
|
||||||
|
|
||||||
|
Снимок в тёмной теме (отдельный скрин светлой темы не добавлялся).
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Создание кластера
|
||||||
|
|
||||||
|
Снимок в светлой теме.
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Аддоны (Helm)
|
||||||
|
|
||||||
|
**Светлая тема**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Тёмная тема**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Встроенная документация (Markdown)
|
||||||
|
|
||||||
|
**Светлая тема**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Тёмная тема**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
## Журнал
|
||||||
|
|
||||||
|
**Светлая тема**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
**Тёмная тема**
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
*Автор: Сергей Антропов — https://devops.org.ru*
|
||||||
@@ -57,6 +57,15 @@ _static_dir = _BASE / "static"
|
|||||||
if _static_dir.is_dir():
|
if _static_dir.is_dir():
|
||||||
app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
|
app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
|
||||||
|
|
||||||
|
# PNG-скриншоты для app/docs/screenshots.md и др.: относительные пути images/* в Markdown → /static/docs-images/*
|
||||||
|
_docs_images_dir = _BASE / "docs" / "images"
|
||||||
|
if _docs_images_dir.is_dir():
|
||||||
|
app.mount(
|
||||||
|
"/static/docs-images",
|
||||||
|
StaticFiles(directory=str(_docs_images_dir)),
|
||||||
|
name="docs_images",
|
||||||
|
)
|
||||||
|
|
||||||
templates = Jinja2Templates(directory=str(_templates_dir))
|
templates = Jinja2Templates(directory=str(_templates_dir))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -139,6 +139,33 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Картинки из app/docs/*.md с src вида images/foo.png → /static/docs-images/foo.png
|
||||||
|
* (каталог app/docs/images смонтирован в FastAPI как StaticFiles).
|
||||||
|
*/
|
||||||
|
function rewriteMdImages(container, currentDocPath) {
|
||||||
|
if (!currentDocPath || currentDocPath.indexOf("app/docs/") !== 0) return;
|
||||||
|
var baseDir = currentDocPath.replace(/[^/]+\.md$/i, "");
|
||||||
|
if (!baseDir) return;
|
||||||
|
container.querySelectorAll("img[src]").forEach(function (img) {
|
||||||
|
var src = (img.getAttribute("src") || "").trim();
|
||||||
|
if (!src || /^https?:\/\//i.test(src) || src.startsWith("data:")) return;
|
||||||
|
if (src.indexOf("..") >= 0) return;
|
||||||
|
if (src.startsWith("/static/docs-images/")) return;
|
||||||
|
try {
|
||||||
|
var u = new URL(src, "http://doc.local/" + baseDir);
|
||||||
|
var pathname = decodeURIComponent(u.pathname.replace(/^\/+/, ""));
|
||||||
|
if (pathname.indexOf("..") >= 0) return;
|
||||||
|
if (!pathname.startsWith("app/docs/images/")) return;
|
||||||
|
var rest = pathname.replace(/^app\/docs\/images\/?/, "");
|
||||||
|
if (!rest) return;
|
||||||
|
img.setAttribute("src", "/static/docs-images/" + rest);
|
||||||
|
} catch (e) {
|
||||||
|
/* ignore */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/** Текст первого H1 в разобранном HTML (для заголовка вкладки). */
|
/** Текст первого H1 в разобранном HTML (для заголовка вкладки). */
|
||||||
function extractFirstH1Text(root) {
|
function extractFirstH1Text(root) {
|
||||||
var h = root.querySelector("h1");
|
var h = root.querySelector("h1");
|
||||||
@@ -261,6 +288,7 @@
|
|||||||
var temp = document.createElement("div");
|
var temp = document.createElement("div");
|
||||||
temp.innerHTML = DOMPurify.sanitize(rawHtml);
|
temp.innerHTML = DOMPurify.sanitize(rawHtml);
|
||||||
rewriteMdLinks(temp, docPath);
|
rewriteMdLinks(temp, docPath);
|
||||||
|
rewriteMdImages(temp, docPath);
|
||||||
|
|
||||||
var h1ForTitle = extractFirstH1Text(temp);
|
var h1ForTitle = extractFirstH1Text(temp);
|
||||||
applyDocumentationTabTitle(h1ForTitle);
|
applyDocumentationTabTitle(h1ForTitle);
|
||||||
|
|||||||
@@ -2935,6 +2935,16 @@ button.btn-danger:hover {
|
|||||||
.readme-section-card.markdown-body li {
|
.readme-section-card.markdown-body li {
|
||||||
margin: 0.2rem 0;
|
margin: 0.2rem 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Скриншоты в app/docs/screenshots.md: не вылезают за карточку */
|
||||||
|
.readme-section-card.markdown-body img {
|
||||||
|
max-width: 100%;
|
||||||
|
height: auto;
|
||||||
|
border-radius: 8px;
|
||||||
|
margin: 0.5rem 0;
|
||||||
|
border: 1px solid var(--color-border-subtle, rgba(128, 128, 128, 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
.readme-section-card.markdown-body a {
|
.readme-section-card.markdown-body a {
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
text-decoration: underline;
|
text-decoration: underline;
|
||||||
|
|||||||