Files
cloud-ip-validator/internal/dashboard/templates/layout.html
T
ayurishchevandClaude Sonnet 5.5 b7669c9e41 Add the Analytics section: check runs, analytics API and page
Runs (migration 0011): a run groups the cycles of one launch. It opens when an
address enters an idle queue, takes everything submitted or re-checked while it
is open and is finalized when all its addresses are done; a re-check after that
opens a new run, so results of different runs never mix. check_runs,
run_results (one result per address and run, with the verdict and the expected
and stored check counts), subnets, run_id on ip_queue and checks. Existing data
is split into runs at pauses of more than an hour; ingress checks get the
validator that held the address (also at write time from now on).

Analytics (internal/analytics): figures computed from the stored checks of the
latest cycle of each address in the run, as facts next to the verdict: summary,
reasons of partial, data quality, subnets, targets and the subnet x target
matrix by check type, ingress by site, error classes, validators, and the
address lists behind the indicators and error classes. API: analytics runs,
report, lists (JSON or CSV), subnet list; run and subnet filters for the
registry.

Dashboard: /analytics matching the approved mockup (run selector, indicators
with address lists and CSV, error-class dialogs, drill-down to the registry),
subnet list on /settings. Sidebar: the control-api link state, theme toggle and
logout moved to the top, the three dots next to the logo removed, sections
grouped.

Rebuilt bin/control-api and bin/admin-dashboard to match. Plan, summary and the
updated README, API, USAGE, DASHBOARD and ADMIN_CLEANUP docs are in docs/.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
2026-10-03 18:36:03 +03:00

130 lines
8.5 KiB
HTML
Raw 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.
{{/* html/template requires {{template "name"}} names to be string
literals, not dynamic pipelines — so a single "layout" wrapper that
dispatches to a per-page content block by name isn't possible within
one shared template set. Instead, layout.html provides small named
partials (html_head, sidebar_nav, topbar_mobile, banner_inner,
error_banner) that each page's *_page template (see e.g.
overview.html) assembles around its own *_content block explicitly.
A few lines of boilerplate are duplicated per page as a result — see
any *_page define for the pattern. */}}
{{define "html_head"}}
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Cloud IP Validator — Admin</title>
<link rel="stylesheet" href="/static/vendor/fonts.css">
<link rel="stylesheet" href="/static/dashboard.css">
<script>
// Apply a saved theme choice before first paint, so a page load never
// flashes the OS-default theme before switching to the stored one. Runs
// synchronously, after the stylesheet link above (browsers block a
// same-document script on a preceding <link rel="stylesheet"> until it
// loads), and before body markup starts rendering.
(function () {
try {
var saved = localStorage.getItem('theme');
if (saved === 'light' || saved === 'dark') {
document.documentElement.setAttribute('data-theme', saved);
}
} catch (e) {}
})();
</script>
<script src="/static/vendor/htmx.min.js"></script>
<script src="/static/vendor/alpine.min.js" defer></script>
{{end}}
{{define "sidebar_nav"}}
<aside class="sidebar">
<div class="brand">
<span class="brand-mark">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2v6M12 16v6M2 12h6M16 12h6"/><circle cx="12" cy="12" r="3"/></svg>
</span>
<span class="brand-name">Cloud IP Validator<small>admin</small></span>
</div>
<div class="sidebar-session">
{{$down := and .Banner.Message (not .Banner.Client)}}
<div class="session-row">
<span class="session-api" title="{{if $down}}Нет связи с control-api: {{.Banner.Message}}{{else}}Связь с control-api в порядке{{end}}"><span class="pulse-dot{{if $down}} down{{end}}"></span>control-api{{if $down}} · нет связи{{end}}</span>
<button type="button" class="theme-toggle" id="themeToggle" aria-label="Переключить тему" title="Переключить тему">
<svg class="icon-sun" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="4"/><path d="M12 2v2M12 20v2M4.9 4.9l1.4 1.4M17.7 17.7l1.4 1.4M2 12h2M20 12h2M4.9 19.1l1.4-1.4M17.7 6.3l1.4-1.4"/></svg>
<svg class="icon-moon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M21 12.8A9 9 0 1 1 11.2 3 7 7 0 0 0 21 12.8Z"/></svg>
</button>
</div>
{{if .AuthEnabled}}<form class="session-row" method="post" action="/logout">
<span class="session-user" title="{{.User}}">{{.User}}</span>
<button type="submit" class="btn btn-ghost btn-sm">Выйти</button>
</form>{{end}}
</div>
<nav class="nav-groups" aria-label="Разделы">
<div class="nav-group-title">Мониторинг</div>
<a class="nav-link{{if eq .ActiveNav "overview"}} active{{end}}" {{if eq .ActiveNav "overview"}}aria-current="page"{{end}} href="/overview">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="3" width="7" height="9" rx="1.5"/><rect x="14" y="3" width="7" height="5" rx="1.5"/><rect x="14" y="12" width="7" height="9" rx="1.5"/><rect x="3" y="16" width="7" height="5" rx="1.5"/></svg>
Обзор
</a>
<a class="nav-link{{if eq .ActiveNav "ips"}} active{{end}}" {{if eq .ActiveNav "ips"}}aria-current="page"{{end}} href="/ips">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="3" y="4" width="18" height="16" rx="2"/><path d="M3 10h18M8 4v16"/></svg>
Очередь IP
</a>
<a class="nav-link{{if eq .ActiveNav "registry"}} active{{end}}" {{if eq .ActiveNav "registry"}}aria-current="page"{{end}} href="/registry">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 19.5A2.5 2.5 0 0 1 6.5 17H20"/><path d="M6.5 2H20v20H6.5A2.5 2.5 0 0 1 4 19.5v-15A2.5 2.5 0 0 1 6.5 2Z"/></svg>
Реестр
</a>
<a class="nav-link{{if eq .ActiveNav "analytics"}} active{{end}}" {{if eq .ActiveNav "analytics"}}aria-current="page"{{end}} href="/analytics">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M4 20V10M10 20V4M16 20v-7M22 20H2"/></svg>
Аналитика
</a>
<div class="nav-group-title">Настройка</div>
<a class="nav-link{{if eq .ActiveNav "validators"}} active{{end}}" {{if eq .ActiveNav "validators"}}aria-current="page"{{end}} href="/validators">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><rect x="4" y="3" width="16" height="7" rx="1.5"/><rect x="4" y="14" width="16" height="7" rx="1.5"/><circle cx="8" cy="6.5" r="1"/><circle cx="8" cy="17.5" r="1"/></svg>
Валидаторы
</a>
<a class="nav-link{{if eq .ActiveNav "sites"}} active{{end}}" {{if eq .ActiveNav "sites"}}aria-current="page"{{end}} href="/sites">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M12 2 3 7l9 5 9-5-9-5Z"/><path d="m3 12 9 5 9-5M3 17l9 5 9-5"/></svg>
Площадки
</a>
<a class="nav-link{{if eq .ActiveNav "targets"}} active{{end}}" {{if eq .ActiveNav "targets"}}aria-current="page"{{end}} href="/targets">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="9"/><circle cx="12" cy="12" r="4.5"/><circle cx="12" cy="12" r=".6" fill="currentColor"/></svg>
Цели
</a>
<a class="nav-link{{if eq .ActiveNav "check-types"}} active{{end}}" {{if eq .ActiveNav "check-types"}}aria-current="page"{{end}} href="/check-types">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M9 11l3 3L22 4"/><path d="M21 12v7a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h11"/></svg>
Типы проверок
</a>
<a class="nav-link{{if eq .ActiveNav "settings"}} active{{end}}" {{if eq .ActiveNav "settings"}}aria-current="page"{{end}} href="/settings">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><circle cx="12" cy="12" r="3"/><path d="M19.4 15a1.65 1.65 0 0 0 .33 1.82l.06.06a2 2 0 1 1-2.83 2.83l-.06-.06a1.65 1.65 0 0 0-1.82-.33 1.65 1.65 0 0 0-1 1.51V21a2 2 0 0 1-4 0v-.09A1.65 1.65 0 0 0 9 19.4a1.65 1.65 0 0 0-1.82.33l-.06.06a2 2 0 1 1-2.83-2.83l.06-.06a1.65 1.65 0 0 0 .33-1.82 1.65 1.65 0 0 0-1.51-1H3a2 2 0 0 1 0-4h.09A1.65 1.65 0 0 0 4.6 9a1.65 1.65 0 0 0-.33-1.82l-.06-.06a2 2 0 1 1 2.83-2.83l.06.06a1.65 1.65 0 0 0 1.82.33H9a1.65 1.65 0 0 0 1-1.51V3a2 2 0 0 1 4 0v.09a1.65 1.65 0 0 0 1 1.51 1.65 1.65 0 0 0 1.82-.33l.06-.06a2 2 0 1 1 2.83 2.83l-.06.06a1.65 1.65 0 0 0-.33 1.82V9a1.65 1.65 0 0 0 1.51 1H21a2 2 0 0 1 0 4h-.09a1.65 1.65 0 0 0-1.51 1Z"/></svg>
Настройки
</a>
</nav>
</aside>
<script>
(function () {
var btn = document.getElementById('themeToggle');
if (!btn) return;
function effectiveTheme() {
return getComputedStyle(document.documentElement).colorScheme.indexOf('dark') !== -1 ? 'dark' : 'light';
}
btn.addEventListener('click', function () {
var next = effectiveTheme() === 'dark' ? 'light' : 'dark';
document.documentElement.setAttribute('data-theme', next);
try { localStorage.setItem('theme', next); } catch (e) {}
});
})();
</script>
{{end}}
{{define "topbar_mobile"}}
<div class="topbar-mobile">
<label class="burger" for="nav-toggle">
<svg width="17" height="17" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"><path d="M4 7h16M4 12h16M4 17h16"/></svg>
</label>
<strong>Cloud IP Validator</strong>
</div>
{{end}}
{{define "banner_inner"}}{{if .Message}}<div class="alert {{if .Client}}alert-warning{{else}}alert-danger{{end}}" role="alert">{{.Message}}</div>{{end}}{{end}}
{{define "error_banner"}}<div id="error-banner" hx-swap-oob="true">{{template "banner_inner" .}}</div>{{end}}