Журнал: пагинация 30 записей на страницу, API offset/total_pages

- job_journal: collect_recent_journal_entries_page_sync(limit, offset)
- GET /api/v1/journal/recent: limit по умолчанию 30, offset, total, page, total_pages
- journal.html/js: навигация Первая/Назад/номера/Вперёд/Последняя, стили
- app/docs/api_routes.md: описание query и пример ответа
- Прочие изменения UI/API (аддоны, helm, job_journal в кластерах) в том же коммите
This commit is contained in:
Sergey Antropoff
2026-04-04 11:16:43 +03:00
parent 52538d9816
commit 17f6233fd7
22 changed files with 2811 additions and 4 deletions
+2
View File
@@ -57,6 +57,8 @@
{# Прокрутка только у пилюль: 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="/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">
+175
View File
@@ -0,0 +1,175 @@
{# Установка и удаление Helm-аддонов в выбранном кластере.
Автор: Сергей Антропов — https://devops.org.ru #}
{% extends "base.html" %}
{% block page_title %}Аддоны кластера{% endblock %}
{% block body_extra_class %} cluster-addons-page-loading{% endblock %}
{% block body_attrs %}data-dashboard-mode="addons"{% endblock %}
{% block footer %}
<div class="footer-inner">
<p class="footer-copyright">
© {{ app_title }} ·
<a href="https://devops.org.ru" target="_blank" rel="noopener">devops.org.ru</a>
</p>
</div>
{% endblock %}
{% block content %}
{# Полноэкранный спиннер до загрузки списка кластеров, статуса и версий чартов; скрывается в cluster_addons.js #}
<div
id="addons-page-loading-overlay"
class="page-loading-overlay"
role="status"
aria-live="polite"
aria-busy="true"
aria-label="Загрузка страницы аддонов"
>
<div class="page-loading-backdrop" aria-hidden="true"></div>
<div class="page-loading-center">
<span class="spinner page-loading-spinner" aria-hidden="true"></span>
<span class="page-loading-label">Загрузка списка кластеров и версий чартов…</span>
</div>
</div>
<section class="card hero-panel cluster-create-hero" aria-labelledby="addons-hero-title">
<h1 class="page-title" id="addons-hero-title">Дополнения кластера (Helm)</h1>
<p class="muted page-lead">
Выберите кластер с сохранённым kubeconfig. Установка и удаление выполняются через <code>helm</code> в контейнере веб-приложения;
операции могут занимать несколько минут — дождитесь ответа сервера. Для kind у ingress-nginx задан NodePort <strong>30080</strong>.
</p>
</section>
<section class="card create-cluster-card cluster-addons-toolbar-card">
<h2 class="create-cluster-card__title">Кластер</h2>
<div class="cluster-addons-cluster-row">
<label for="addons-cluster-select" class="cluster-addons-cluster-label">Активный кластер</label>
<select id="addons-cluster-select" class="cluster-addons-cluster-select" aria-describedby="addons-cluster-hint">
<option value="">— загрузка списка —</option>
</select>
<button type="button" class="btn-secondary" id="addons-refresh-status">Обновить статус</button>
</div>
<p id="addons-cluster-hint" class="muted cluster-addons-hint">
Нужен файл <code>clusters/&lt;имя&gt;/kubeconfig</code> и доступ API из контейнера.
</p>
<p id="addons-versions-hint" class="muted cluster-addons-hint">
Список версий чартов подгружается с сервера (<code>GET /api/v1/helm/chart-versions</code>); значение «Последняя» не передаёт <code>--version</code> в Helm.
</p>
<div id="addons-status-bar" class="cluster-addons-status-bar muted" role="status" aria-live="polite"></div>
</section>
<div class="cluster-addons-grid">
<section class="card cluster-addon-card" data-addon="ingress-nginx">
<h2 class="cluster-addon-card__title">ingress-nginx</h2>
<p class="muted cluster-addon-card__desc">Контроллер Ingress; для kind: NodePort HTTP на порту 30080.</p>
<div class="create-form-field">
<label for="addons-ing-version-select">Версия чарта</label>
<select id="addons-ing-version-select" class="cluster-addons-version-select" aria-describedby="addons-versions-hint">
<option value="">Последняя (из репозитория)</option>
</select>
</div>
<div class="cluster-addon-card__actions row">
<button type="button" class="btn-addon-install" data-endpoint="ingress-nginx">Установить</button>
<button type="button" class="btn-secondary btn-addon-remove" data-endpoint="ingress-nginx">Удалить</button>
</div>
</section>
<section class="card cluster-addon-card" data-addon="kube-prometheus-stack">
<h2 class="cluster-addon-card__title">kube-prometheus-stack</h2>
<p class="muted cluster-addon-card__desc">Prometheus Operator, Alertmanager, Grafana.</p>
<div class="create-form-field">
<label for="addons-grafana-user">Логин администратора Grafana</label>
<input id="addons-grafana-user" type="text" value="admin" maxlength="256" autocomplete="username" />
</div>
<div class="create-form-field">
<label for="addons-grafana-pass">Пароль администратора Grafana</label>
<input id="addons-grafana-pass" type="password" minlength="8" maxlength="256" autocomplete="new-password" placeholder="не менее 8 символов" />
</div>
<div class="create-form-field">
<label for="addons-prom-version-select">Версия чарта</label>
<select id="addons-prom-version-select" class="cluster-addons-version-select">
<option value="">Последняя (из репозитория)</option>
</select>
</div>
<div class="cluster-addon-card__actions row">
<button type="button" class="btn-addon-install" data-endpoint="kube-prometheus-stack">Установить</button>
<button type="button" class="btn-secondary btn-addon-remove" data-endpoint="kube-prometheus-stack">Удалить</button>
</div>
</section>
<section class="card cluster-addon-card" data-addon="metrics-server">
<h2 class="cluster-addon-card__title">metrics-server</h2>
<p class="muted cluster-addon-card__desc">Метрики узлов и подов для <code>kubectl top</code>; для kind включён <code>--kubelet-insecure-tls</code>.</p>
<div class="create-form-field">
<label for="addons-ms-version-select">Версия чарта</label>
<select id="addons-ms-version-select" class="cluster-addons-version-select">
<option value="">Последняя (из репозитория)</option>
</select>
</div>
<div class="cluster-addon-card__actions row">
<button type="button" class="btn-addon-install" data-endpoint="metrics-server">Установить</button>
<button type="button" class="btn-secondary btn-addon-remove" data-endpoint="metrics-server">Удалить</button>
</div>
</section>
<section class="card cluster-addon-card cluster-addon-card--wide" data-addon="istio-kiali">
<h2 class="cluster-addon-card__title">Istio + Kiali</h2>
<p class="muted cluster-addon-card__desc">
Чарты <code>istio/base</code>, <code>istio/istiod</code> и <code>kiali-server</code> в <code>istio-system</code>. Вход в Kiali — стратегия
<strong>login</strong> (секрет с логином и паролем).
</p>
<div class="create-form-grid cluster-addon-istio-grid">
<div class="create-form-field">
<label for="addons-kiali-user">Имя пользователя Kiali</label>
<input id="addons-kiali-user" type="text" value="kiali-admin" maxlength="256" autocomplete="username" />
</div>
<div class="create-form-field">
<label for="addons-kiali-pass">Пароль Kiali</label>
<input id="addons-kiali-pass" type="password" minlength="8" maxlength="256" autocomplete="new-password" placeholder="не менее 8 символов" />
</div>
<div class="create-form-field">
<label for="addons-istio-version-select">Версия чартов Istio (base + istiod)</label>
<select id="addons-istio-version-select" class="cluster-addons-version-select">
<option value="">Последняя (из репозитория)</option>
</select>
</div>
<div class="create-form-field">
<label for="addons-kiali-version-select">Версия чарта Kiali</label>
<select id="addons-kiali-version-select" class="cluster-addons-version-select">
<option value="">Последняя (из репозитория)</option>
</select>
</div>
</div>
<div class="cluster-addon-card__actions row">
<button type="button" class="btn-addon-install" data-endpoint="istio-kiali">Установить</button>
<button type="button" class="btn-secondary btn-addon-remove" data-endpoint="istio-kiali">Удалить</button>
</div>
</section>
</div>
<section class="card cluster-addons-log-card">
<h2 class="create-cluster-card__title">Журнал последней операции</h2>
<pre id="addons-log-pre" class="mono cluster-addons-log-pre" role="log" aria-live="polite"></pre>
</section>
<div
id="addons-busy-overlay"
class="page-loading-overlay cluster-addons-busy-overlay hidden"
role="status"
aria-live="polite"
aria-busy="false"
aria-hidden="true"
>
<div class="page-loading-backdrop" aria-hidden="true"></div>
<div class="page-loading-center">
<span class="spinner page-loading-spinner" aria-hidden="true"></span>
<span id="addons-busy-label" class="page-loading-label">Выполняется операция Helm…</span>
</div>
</div>
{% endblock %}
{% block scripts %}
<script src="/static/js/cluster_addons.js" defer></script>
{% endblock %}
+84
View File
@@ -0,0 +1,84 @@
{# Агрегированный журнал завершённых заданий (JSON в каталогах кластеров).
Автор: Сергей Антропов — https://devops.org.ru #}
{% extends "base.html" %}
{% block page_title %}Журнал{% endblock %}
{% block body_extra_class %} journal-page-loading{% endblock %}
{% block body_attrs %}data-dashboard-mode="journal"{% endblock %}
{% block footer %}
<div class="footer-inner">
<p class="footer-copyright">
© {{ app_title }} ·
<a href="https://devops.org.ru" target="_blank" rel="noopener">devops.org.ru</a>
</p>
</div>
{% endblock %}
{% block content %}
{# Полноэкранный спиннер до первой загрузки журнала; скрывается в journal.js #}
<div
id="journal-page-loading-overlay"
class="page-loading-overlay"
role="status"
aria-live="polite"
aria-busy="true"
aria-label="Загрузка журнала"
>
<div class="page-loading-backdrop" aria-hidden="true"></div>
<div class="page-loading-center">
<span class="spinner page-loading-spinner" aria-hidden="true"></span>
<span class="page-loading-label">Загрузка журнала…</span>
</div>
</div>
<section class="card hero-panel cluster-create-hero" aria-labelledby="journal-hero-title">
<h1 class="page-title" id="journal-hero-title">Журнал заданий</h1>
<p class="muted page-lead">
Записи о завершённых операциях (<strong>create</strong>, <strong>start</strong>, <strong>stop</strong> и др.) хранятся в
<code>clusters/&lt;имя&gt;/journal/jobs_history.json</code> на томе с хоста. Сводка по всем кластерам, по <strong>30</strong> записей на страницу;
детали задания в процессе — по-прежнему в <a href="/cluster-create">создании кластера</a> и <code>GET /api/v1/jobs/{id}</code>.
</p>
</section>
<section class="card create-cluster-card journal-toolbar-card">
<h2 class="create-cluster-card__title">Действия</h2>
<div class="journal-toolbar-row">
<button type="button" class="btn-secondary" id="journal-refresh-btn">Обновить</button>
<span class="muted journal-page-size-hint">На странице по <strong>30</strong> записей.</span>
</div>
<p id="journal-status" class="muted journal-status" role="status" aria-live="polite"></p>
</section>
<section class="card journal-list-card">
<h2 class="create-cluster-card__title">Последние записи</h2>
<div id="journal-empty" class="muted journal-empty hidden">Нет записей в журналах кластеров (или каталоги кластеров ещё не созданы).</div>
<div id="journal-table-wrap" class="journal-table-wrap">
<table class="journal-table" id="journal-table" aria-describedby="journal-hero-title">
<thead>
<tr>
<th scope="col">Завершено (UTC)</th>
<th scope="col">Кластер</th>
<th scope="col">Тип</th>
<th scope="col">Статус</th>
<th scope="col">Сообщение</th>
<th scope="col">Журнал</th>
</tr>
</thead>
<tbody id="journal-tbody"></tbody>
</table>
</div>
<nav
id="journal-pagination"
class="journal-pagination hidden"
aria-label="Страницы журнала"
role="navigation"
></nav>
</section>
{% endblock %}
{% block scripts %}
<script src="/static/js/journal.js" defer></script>
{% endblock %}
@@ -54,6 +54,31 @@
</div>
</div>
{# Журнал фонового задания (ошибка / отмена) — страница «Последние задания». #}
<div
id="job-log-modal-overlay"
class="modal-overlay job-log-modal-overlay hidden"
role="dialog"
aria-modal="true"
aria-labelledby="job-log-modal-title"
>
<div class="modal-box modal-box--job-log">
<div class="modal-head modal-head--cluster">
<h3 id="job-log-modal-title" class="modal-title-text modal-title-text--center">Журнал задания</h3>
<div class="modal-toolbar" role="toolbar" aria-label="Закрыть окно">
<button type="button" class="modal-close btn-secondary" id="job-log-modal-close">Закрыть</button>
</div>
</div>
<p id="job-log-modal-sub" class="muted modal-sub" aria-live="polite"></p>
<pre
id="job-log-modal-body"
class="mono job-log-modal-body"
role="region"
aria-label="Текст журнала задания"
></pre>
</div>
</div>
<div
id="confirm-modal-overlay"
class="modal-overlay confirm-modal-overlay hidden"