2026-08-23 20:39:22 +03:00
{{/* 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
2026-08-23 21:58:19 +03:00
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. */}}
2026-08-23 20:39:22 +03:00
{{define "html_head"}}
< meta charset = "utf-8" >
< meta name = "viewport" content = "width=device-width, initial-scale=1" >
< title > Cloud IP Validator — Admin</ title >
2026-08-23 21:58:19 +03:00
< link rel = "stylesheet" href = "/static/vendor/fonts.css" >
2026-08-23 20:39:22 +03:00
< link rel = "stylesheet" href = "/static/dashboard.css" >
2026-08-26 21:18:58 +03:00
< 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 >
2026-08-23 20:39:22 +03:00
< script src = "/static/vendor/htmx.min.js" ></ script >
< script src = "/static/vendor/alpine.min.js" defer ></ script >
{{end}}
2026-08-23 21:58:19 +03:00
{{define "sidebar_nav"}}
< aside class = "sidebar" >
< div class = "brand" >
< span class = "brand-mark" >
2026-08-26 21:08:18 +03:00
< 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 >
2026-08-23 21:58:19 +03:00
</ span >
< span class = "brand-name" > Cloud IP Validator< small > admin</ small ></ span >
2026-08-23 21:15:31 +03:00
</ div >
2026-08-23 21:58:19 +03:00
2026-10-03 18:36:03 +03:00
< 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 >
2026-08-23 21:58:19 +03:00
< 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 >
2026-09-23 09:52:01 +03:00
< 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 >
2026-10-03 18:36:03 +03:00
< 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 >
2026-08-23 21:58:19 +03:00
< 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 >
2026-08-24 10:29:08 +03:00
< 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 >
2026-08-23 20:39:22 +03:00
</ nav >
2026-08-23 21:58:19 +03:00
</ aside >
2026-08-26 21:18:58 +03:00
< 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 >
2026-08-23 20:39:22 +03:00
{{end}}
2026-08-23 21:58:19 +03:00
{{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}}
2026-08-23 20:39:22 +03:00
{{define "error_banner"}}< div id = "error-banner" hx-swap-oob = "true" > {{template "banner_inner" .}}</ div > {{end}}