2026-08-23 20:39:22 +03:00
{{define "targets_page"}}
<!doctype html>
< html lang = "ru" >
< head > {{template "html_head" .}}</ head >
< body >
2026-08-26 22:19:42 +03:00
< div class = "bg-grid" ></ div >
2026-08-23 21:58:19 +03:00
< input type = "checkbox" id = "nav-toggle" class = "nav-toggle" >
< div class = "shell" >
{{template "sidebar_nav" .}}
< div >
{{template "topbar_mobile" .}}
< main class = "main" >
2026-08-23 20:39:22 +03:00
< div id = "error-banner" > {{template "banner_inner" .Banner}}</ div >
{{template "targets_content" .}}
</ main >
2026-08-23 21:58:19 +03:00
</ div >
</ div >
2026-09-18 10:52:27 +03:00
< script >
( function () {
var maxLines = 20 ;
function resize ( el ) {
var cs = getComputedStyle ( el );
var lineHeight = parseFloat ( cs . lineHeight ) || parseFloat ( cs . fontSize ) * 1.5 ;
var paddingV = parseFloat ( cs . paddingTop ) + parseFloat ( cs . paddingBottom );
var borderV = parseFloat ( cs . borderTopWidth ) + parseFloat ( cs . borderBottomWidth );
var maxHeight = Math . round ( lineHeight * maxLines + paddingV + borderV );
el . style . height = 'auto' ;
var next = Math . min ( el . scrollHeight , maxHeight );
el . style . height = next + 'px' ;
el . style . overflowY = el . scrollHeight > maxHeight ? 'auto' : 'hidden' ;
}
function bind ( el ) {
if ( el . dataset . autosizeBound ) return ;
el . dataset . autosizeBound = '1' ;
el . addEventListener ( 'input' , function () { resize ( el ); });
// An element inside a hidden (display:none) edit row reports
// scrollHeight 0 — sizing it now would wrongly collapse it to ~0px
// and that inline height sticks around after the row is later shown.
// Skip the initial measurement here; toggleEditRow re-measures once
// the row is actually visible.
if ( el . offsetParent !== null ) resize ( el );
}
function bindAll () {
document . querySelectorAll ( 'textarea.autosize' ). forEach ( bind );
}
document . addEventListener ( 'DOMContentLoaded' , bindAll );
document . body . addEventListener ( 'htmx:afterSwap' , bindAll );
// Shared by every row's "Изменить"/"Отмена" buttons (see targets_table).
window . toggleEditRow = function ( name ) {
var row = document . getElementById ( 'edit-' + name );
if ( ! row ) return ;
row . hidden = ! row . hidden ;
if ( ! row . hidden ) {
var ta = row . querySelector ( 'textarea.autosize' );
if ( ta ) { bind ( ta ); resize ( ta ); }
}
};
})();
</ script >
2026-08-23 20:39:22 +03:00
</ body >
</ html >
{{end}}
{{define "targets_content"}}
2026-08-23 21:58:19 +03:00
< div class = "topbar" >< h1 > Цели проверки</ h1 ></ div >
< p class = "muted" style = "margin-bottom:16px" > Группа целей — именованный список URL/адресов, на который затем ссылаются типы проверок.</ p >
2026-08-23 20:39:22 +03:00
2026-09-18 10:52:27 +03:00
< div id = "targets-stats-wrap" > {{template "targets_stats_inner" .}}</ div >
2026-08-23 21:58:19 +03:00
< div class = "panel" >
2026-08-26 22:19:42 +03:00
< div class = "panel-head" >< h2 > Новая группа целей</ h2 ></ div >
2026-08-23 21:58:19 +03:00
< div class = "panel-body" >
< form hx-post = "/targets" hx-target = "#targets-table-wrap" hx-swap = "innerHTML" hx-on::after-request = "this.reset()" >
< div class = "field-row" >
< div class = "field" style = "flex: 0 1 200px;" >
2026-09-18 10:52:27 +03:00
< label for = "new-group-name" > Имя новой группы</ label >
< input type = "text" id = "new-group-name" name = "name" placeholder = "web" required >
2026-08-23 21:15:31 +03:00
</ div >
2026-08-23 21:58:19 +03:00
< div class = "field" style = "flex: 1 1 380px;" >
2026-09-18 10:52:27 +03:00
< label for = "new-group-targets" > Цели (по одной на строку или через запятую)</ label >
< textarea id = "new-group-targets" name = "targets" rows = "2" class = "autosize" placeholder = "https://hub.docker.com https://github.com" required ></ textarea >
2026-08-23 21:15:31 +03:00
</ div >
2026-08-23 21:58:19 +03:00
< button type = "submit" class = "btn btn-primary" > Создать</ button >
2026-08-23 21:15:31 +03:00
</ div >
2026-08-23 20:39:22 +03:00
</ form >
2026-08-23 21:58:19 +03:00
</ div >
</ div >
2026-08-23 20:39:22 +03:00
< div id = "targets-table-wrap" >
{{template "targets_table" .}}
</ div >
{{end}}
2026-09-18 10:52:27 +03:00
{{/* Stat-grid content only, shared by the initial page render (plain,
inside #targets-stats-wrap above) and the OOB update below (targets
mutations only swap #targets-table-wrap, so the stats need their own
out-of-band refresh to stay live — same pattern as layout.html's
banner_inner/error_banner split). */}}
{{define "targets_stats_inner"}}
< div class = "stat-grid cols-4" >
< div class = "stat-card accented" >< span class = "value" > {{.Stats.Groups}}</ span >< span class = "label" > групп целей</ span ></ div >
< div class = "stat-card" >< span class = "value" > {{.Stats.TotalTargets}}</ span >< span class = "label" > адресов всего</ span ></ div >
< div class = "stat-card" >< span class = "value" > {{.Stats.CheckTypesUsing}}</ span >< span class = "label" > типа проверок ссылаются</ span ></ div >
< div class = "stat-card{{if gt .Stats.Unused 0}} warn{{end}}" >< span class = "value" > {{.Stats.Unused}}</ span >< span class = "label" > без типа проверки</ span ></ div >
</ div >
{{end}}
{{define "targets_stats_oob"}}< div id = "targets-stats-wrap" hx-swap-oob = "true" > {{template "targets_stats_inner" .}}</ div > {{end}}
2026-08-23 20:39:22 +03:00
{{define "targets_table"}}
2026-08-23 21:58:19 +03:00
< div class = "panel" >
< div class = "table-scroll" >
< table >
2026-09-18 10:52:27 +03:00
< thead >< tr >< th > Группа</ th >< th > Цели</ th >< th > Используется в</ th >< th ></ th ></ tr ></ thead >
2026-08-23 20:39:22 +03:00
< tbody >
{{range .Items}}
< tr >
2026-09-18 10:52:27 +03:00
< td data-label = "Группа" >
< span class = "group-name" >< span class = "mono" style = "font-weight:600" > {{.Name}}</ span >< span class = "count" > {{len .Targets}} {{pluralAddr (len .Targets)}}</ span ></ span >
</ td >
2026-08-23 21:58:19 +03:00
< td data-label = "Цели" >
2026-09-18 10:52:27 +03:00
< div class = "chip-list" > {{range .Targets}}< span class = "chip" > {{.}}</ span > {{end}}</ div >
</ td >
< td data-label = "Используется в" >
{{if .UsedBy}}< div class = "used-by" > {{range .UsedBy}}< span class = "pill {{if .Enabled}}pill-success{{else}}pill-neutral{{end}}" > {{.Name}}{{if not .Enabled}} (выкл.){{end}}</ span > {{end}}</ div >
{{else}}< span class = "muted" > не используется ни одним типом проверки</ span > {{end}}
2026-08-23 20:39:22 +03:00
</ td >
2026-08-23 21:58:19 +03:00
< td data-label = "" >
< div class = "actions" >
2026-09-18 10:52:27 +03:00
< button type = "button" class = "btn btn-ghost btn-sm" onclick = "toggleEditRow('{{.Name}}')" > Изменить</ button >
2026-08-23 21:58:19 +03:00
< button class = "btn btn-danger-ghost btn-sm" hx-delete = "/targets/{{.Name}}" hx-target = "#targets-table-wrap" hx-swap = "innerHTML" hx-confirm = "Удалить группу {{.Name}}?" > Удалить</ button >
</ div >
2026-08-23 20:39:22 +03:00
</ td >
</ tr >
2026-09-18 10:52:27 +03:00
< tr class = "edit-row" id = "edit-{{.Name}}" hidden >
< td colspan = "4" >
< form class = "inline-form" hx-put = "/targets/{{.Name}}" hx-target = "#targets-table-wrap" hx-swap = "innerHTML" >
< textarea name = "targets" rows = "2" class = "autosize" > {{join .Targets "\n"}}</ textarea >
< button type = "submit" class = "btn btn-primary btn-sm" > Сохранить</ button >
< button type = "button" class = "btn btn-ghost btn-sm" onclick = "toggleEditRow('{{.Name}}')" > Отмена</ button >
</ form >
</ td >
</ tr >
2026-08-23 20:39:22 +03:00
{{end}}
</ tbody >
</ table >
2026-08-23 21:15:31 +03:00
</ div >
2026-09-18 10:52:27 +03:00
{{if .Items}}< div class = "section-note" > Колонка «Используется в» показывает типы проверок (< a href = "/check-types" style = "color:var(--accent)" > /check-types</ a > ), которые ссылаются на группу; пусто — группа ничем не используется. «Изменить» разворачивает поле для редактирования только для этой группы.</ div > {{end}}
2026-08-23 21:58:19 +03:00
</ div >
2026-08-23 20:39:22 +03:00
{{if not .Items}}< p class = "muted" > Групп целей нет.</ p > {{end}}
{{end}}