2026-10-03 18:36:03 +03:00
/* Analytics page: renders the report of one finished run (embedded as JSON in
#analytics-data) and opens the address lists behind the indicators and the
error classes in a dialog. No other run's data is on the page. */
( function () {
'use strict' ;
var D = JSON . parse ( document . getElementById ( 'analytics-data' ). textContent );
var R = D . report , M = D . meta , S = R . summary , Q = R . quality ;
function $ ( id ) { return document . getElementById ( id ); }
function fmt ( n ) { return Math . round ( n ). toLocaleString ( 'ru-RU' ); }
function pct ( a , b ) { return b ? Math . round ( a / b * 100 ) : 0 ; }
function pct1 ( a , b ) { return b ? ( a / b * 100 ). toLocaleString ( 'ru-RU' , { maximumFractionDigits : 1 }) : '0' ; }
function esc ( s ) {
return String ( s ). replace ( /[&<>"']/g , function ( c ) {
return { '&' : '&' , '<' : '<' , '>' : '>' , '"' : '"' , "'" : ''' }[ c ];
});
}
function tipAttr ( t ) { return 'data-tip="' + esc ( t ) + '"' ; }
function vnum ( id ) { var m = /(\d+)$/ . exec ( id ); return m ? + m [ 1 ] : null ; }
function vshort ( id ) { var n = vnum ( id ); return n === null ? id : 'v' + n ; }
var state = { sort : 'worst' , all : false , type : R . targets . types . indexOf ( 'https' ) >= 0 ? 'https' : ( R . targets . types [ 0 ] || '' ) };
/* ---- indicators ---- */
function renderKpis () {
2026-10-04 10:00:39 +03:00
var late = Q . late_failed_checks_at_pass , GO = ' <span class="an-go">список →</span>' ;
2026-10-03 18:36:03 +03:00
var tiles = [
[ 'Адресов' , fmt ( S . addresses ), 'последний цикл каждого адреса' , '' , '' ],
2026-10-04 10:00:39 +03:00
[ 'pass' , fmt ( S . pass ), pct ( S . pass , S . addresses ) + '% адресов' , '<span class="an-tag an-t-ok">успех</span>' + GO , 'verdict_pass' ],
[ 'partial' , fmt ( S . partial ), pct ( S . partial , S . addresses ) + '% адресов' , '<span class="an-tag an-t-warn">частично</span>' + GO , 'verdict_partial' ],
2026-10-03 18:36:03 +03:00
[ 'fail' , fmt ( S . fail ), S . fail ? pct ( S . fail , S . addresses ) + '% адресов' : 'ни одной полностью проваленной' ,
2026-10-04 10:00:39 +03:00
S . fail ? '<span class="an-tag an-t-bad">провал</span>' + GO : '<span class="an-tag an-t-n">нет</span>' , S . fail ? 'verdict_fail' : '' ],
2026-10-03 18:36:03 +03:00
[ 'Egress OK' , pct1 ( S . egress_ok , S . addresses ) + '%' , 'все egress-проверки адреса успешны' , '' , '' ],
[ 'Ingress OK' , pct1 ( S . ingress_ok , S . addresses ) + '%' , 'все ingress-проверки адреса успешны' , '' , '' ],
[ 'Egress https: есть провалы' , fmt ( S . egress_https_any_failed ), pct1 ( S . egress_https_any_failed , S . addresses ) + '% адресов, хотя бы одна цель недоступна' ,
'<span class="an-tag an-t-warn">прикладной</span> <span class="an-go">список →</span>' , 'egress_https_any' ],
[ 'Ingress ssh: есть провалы' , fmt ( S . ingress_ssh_any_failed ), pct1 ( S . ingress_ssh_any_failed , S . addresses ) + '% адресов, хотя бы с одной площадки' ,
'<span class="an-tag an-t-warn">прикладной</span> <span class="an-go">список →</span>' , 'ingress_ssh_any' ],
[ 'Egress https: все провалены' , fmt ( S . egress_https_all_failed ), pct1 ( S . egress_https_all_failed , S . addresses ) + '% адресов, по всем целям: ' + fmt ( S . egress_https_all_targets_failed ),
'<span class="an-tag an-t-bad">прикладной</span> <span class="an-go">список →</span>' , 'egress_https_all' ],
[ 'Ingress ssh: все провалены' , fmt ( S . ingress_ssh_all_failed ), pct1 ( S . ingress_ssh_all_failed , S . addresses ) + '% адресов, ssh провален со всех площадок' ,
'<span class="an-tag an-t-bad">прикладной</span> <span class="an-go">список →</span>' , 'ingress_ssh_all' ],
[ 'Длительность' , esc ( M . duration ), fmt ( S . addresses_per_minute ) + ' адр./мин' , '' , '' ],
[ 'Поздние результаты' , fmt ( late ), 'у ' + fmt ( Q . late_failed_addresses_at_pass ) + ' адресов, после вердикта' ,
late ? '<span class="an-tag an-t-warn">внимание</span>' : '<span class="an-tag an-t-ok">в норме</span>' , '' ]
];
$ ( 'an-kpis' ). innerHTML = tiles . map ( function ( t ) {
var inner = '<div class="an-k">' + t [ 0 ] + '</div><div class="an-v">' + t [ 1 ] + '</div><div class="an-d">' + t [ 2 ] + ' ' + t [ 3 ] + '</div>' ;
return t [ 4 ]
? '<button type="button" class="an-kpi" data-list="' + t [ 4 ] + '" aria-haspopup="dialog">' + inner + '</button>'
: '<div class="an-kpi">' + inner + '</div>' ;
}). join ( '' );
}
function bar ( v , max ) {
return '<div class="an-track"><div class="an-fill" style="width:' + ( max ? Math . max ( 1 , v / max * 100 ) : 0 ) + '%"></div></div>' ;
}
function renderReasons () {
var max = Math . max . apply ( null , R . reasons . map ( function ( x ) { return x . count ; }). concat ([ 1 ]));
$ ( 'an-reasons' ). innerHTML = R . reasons . map ( function ( x ) {
return '<div class="an-bar-row" ' + tipAttr ( x . name + ': ' + fmt ( x . count ) + ' адр. (' + pct ( x . count , S . partial ) + '% от partial)' ) + '><span class="an-n">' + esc ( x . name ) + '</span>' + bar ( x . count , max ) + '<span class="an-num">' + fmt ( x . count ) + '</span></div>' ;
}). join ( '' );
}
function renderQuality () {
var errTotal = Q . ingress_failed_checks ;
var rows = [
[ 'Поздние провалы у адресов с вердиктом pass' , fmt ( Q . late_failed_checks_at_pass ) + ' / ' + fmt ( Q . late_failed_addresses_at_pass ) + ' адр.' ],
[ 'Провалы ingress, пришедшие после вердикта' , fmt ( Q . ingress_failed_late ) + ' из ' + fmt ( errTotal )],
[ 'Неполный набор проверок' , fmt ( Q . incomplete_addresses ) + ' адр.' ],
[ 'Вердикт pass, но есть проваленные проверки' , fmt ( Q . pass_with_failed_addresses ) + ' адр.' ],
[ 'pass по вердикту → по фактическим проверкам' , fmt ( S . pass ) + ' → ' + fmt ( Q . pass_by_facts )]
];
$ ( 'an-dq' ). innerHTML = rows . map ( function ( r ) { return '<div class="an-row"><span>' + r [ 0 ] + '</span><span>' + r [ 1 ] + '</span></div>' ; }). join ( '' );
}
function pbar ( a , n ) {
var p = pct ( a , n );
return '<div class="an-pb"><div class="an-track"><div class="an-fill" style="width:' + p + '%"></div></div><span class="an-num">' + p + '%</span></div>' ;
}
function renderSubnets () {
var list = R . subnets . slice ();
list . sort ( state . sort === 'worst'
? function ( a , b ) { return a . pass / a . addresses - b . pass / b . addresses || b . addresses - a . addresses ; }
: function ( a , b ) { return b . addresses - a . addresses ; });
var shown = state . all ? list : list . slice ( 0 , 10 );
$ ( 'an-allsub' ). textContent = state . all ? 'свернуть до 10' : 'показать все ' + list . length ;
$ ( 'an-allsub' ). hidden = list . length <= 10 ;
$ ( 'an-subtbl' ). innerHTML = '<thead><tr><th>Подсеть</th><th class="an-r">Адресов</th><th class="an-r">pass</th><th class="an-r">Egress OK</th><th class="an-r">Ingress OK</th></tr></thead><tbody>' +
shown . map ( function ( s ) {
var name = s . label ? s . cidr + ' · ' + s . label : s . cidr ;
var link = s . cidr === 'прочие' ? esc ( name ) : '<a href="' + M . registry_url + '&subnet=' + encodeURIComponent ( s . cidr ) + '" title="Открыть в реестре: запуск ' + M . run_id + ', подсеть ' + esc ( s . cidr ) + '">' + esc ( name ) + '</a>' ;
return '<tr><td class="an-a">' + link + '</td><td class="an-num an-r">' + fmt ( s . addresses ) + '</td>' +
'<td class="an-r" ' + tipAttr ( s . cidr + ': pass ' + fmt ( s . pass ) + ' из ' + fmt ( s . addresses )) + '>' + pbar ( s . pass , s . addresses ) + '</td>' +
'<td class="an-r" ' + tipAttr ( s . cidr + ': egress OK ' + fmt ( s . egress_ok ) + ' из ' + fmt ( s . addresses )) + '>' + pbar ( s . egress_ok , s . addresses ) + '</td>' +
'<td class="an-r" ' + tipAttr ( s . cidr + ': ingress OK ' + fmt ( s . ingress_ok ) + ' из ' + fmt ( s . addresses )) + '>' + pbar ( s . ingress_ok , s . addresses ) + '</td></tr>' ;
}). join ( '' ) + '</tbody>' ;
}
function heatClass ( v ) { return 'an-h' + ( v >= 90 ? 7 : v >= 75 ? 6 : v >= 60 ? 5 : v >= 45 ? 4 : v >= 30 ? 3 : v >= 15 ? 2 : 1 ); }
function renderTypes () {
$ ( 'an-types' ). innerHTML = R . targets . types . map ( function ( t ) {
return '<button type="button" data-type="' + esc ( t ) + '" aria-pressed="' + ( t === state . type ) + '">' + esc ( t ) + '</button>' ;
}). join ( '' );
}
function renderTargets () {
var t = state . type , T = R . targets , vals = T . failed [ t ] || [];
var max = Math . max . apply ( null , vals . concat ([ 1 ]));
$ ( 'an-targets' ). innerHTML = T . targets . map ( function ( n , i ) {
return '<div class="an-bar-row" ' + tipAttr ( n + ' · ' + t + ': провал у ' + fmt ( vals [ i ]) + ' адр. (' + pct ( vals [ i ], S . addresses ) + '% всех)' ) + '><span class="an-n">' + esc ( n ) + '</span>' + bar ( vals [ i ], max ) + '<span class="an-num">' + fmt ( vals [ i ]) + ' · ' + pct ( vals [ i ], S . addresses ) + '%</span></div>' ;
}). join ( '' );
var rows = ( R . matrix [ t ] || []). slice ( 0 , state . all ? 100000 : 10 );
$ ( 'an-matrixwrap' ). hidden = ! rows . length ;
$ ( 'an-hlegend' ). innerHTML = [ '0– 14' , '15– 29' , '30– 44' , '45– 59' , '60– 74' , '75– 89' , '90– 100' ]. map ( function ( l , i ) {
return '<span><i style="background:var(--an-h' + ( i + 1 ) + ')"></i> ' + l + '%</span>' ;
}). join ( '' );
$ ( 'an-matrix' ). innerHTML = '<thead><tr><th>Подсеть</th><th class="an-r">partial</th>' + T . targets . map ( function ( x ) {
return '<th class="an-r">' + esc ( x . replace ( '.com' , '' ). replace ( '.org' , '' )) + '</th>' ;
}). join ( '' ) + '</tr></thead><tbody>' + rows . map ( function ( m ) {
var link = m . cidr === 'прочие' ? esc ( m . cidr ) : '<a href="' + M . registry_url + '&subnet=' + encodeURIComponent ( m . cidr ) + '" title="Открыть адреса в реестре">' + esc ( m . cidr ) + '</a>' ;
return '<tr><td class="an-a">' + link + '</td><td class="an-num an-r">' + fmt ( m . partial ) + '</td>' + m . percent . map ( function ( v , j ) {
return '<td class="an-c"><span class="an-cell ' + heatClass ( v ) + '" ' + tipAttr ( m . cidr + ' → ' + T . targets [ j ] + ': ' + v + '% адресов partial провалили ' + t ) + '>' + v + '%</span></td>' ;
}). join ( '' ) + '</tr>' ;
}). join ( '' ) + '</tbody>' ;
$ ( 'an-matrixnote' ). textContent = 'Доля адресов partial подсети, провалившие ' + t + ' к цели. Строки — подсети с наибольшим числом partial; полный список включается кнопкой «показать все» выше.' ;
}
function renderSites () {
var T = R . sites ;
$ ( 'an-sites' ). innerHTML = '<thead><tr><th>Площадка</th>' + T . types . map ( function ( t ) { return '<th class="an-r">' + esc ( t ) + '</th>' ; }). join ( '' ) + '</tr></thead><tbody>' +
T . rows . map ( function ( row ) {
return '<tr><td class="an-a">' + esc ( row . site ) + '</td>' + row . stats . map ( function ( st , i ) {
return '<td class="an-r" ' + tipAttr ( row . site + ' · ' + T . types [ i ] + ': успешно ' + fmt ( st . ok ) + ' из ' + fmt ( st . total )) + '><span class="an-num">' + pct1 ( st . total - st . ok , st . total ) + '%</span> <span class="an-note">провал</span></td>' ;
}). join ( '' ) + '</tr>' ;
}). join ( '' ) + '</tbody>' ;
}
function renderErrors () {
var max = Math . max . apply ( null , R . errors . map ( function ( e ) { return e . count ; }). concat ([ 1 ]));
$ ( 'an-errs' ). innerHTML = R . errors . length ? R . errors . map ( function ( e ) {
return '<button type="button" class="an-bar-row" data-cls="' + esc ( e . name ) + '" aria-haspopup="dialog" ' + tipAttr ( e . name + ': ' + fmt ( e . count ) + ' проверок. Нажмите, чтобы открыть список' ) + '><span class="an-n">' + esc ( e . name ) + '</span>' + bar ( e . count , max ) + '<span class="an-num">' + fmt ( e . count ) + '</span></button>' ;
}). join ( '' ) : '<p class="an-note">Проваленных ingress-проверок нет.</p>' ;
}
function renderValidators () {
var V = R . validators ;
var f = V . map ( function ( v ) { return v . total ? 1 - v . ok / v . total : 0 ; });
var avg = f . length ? f . reduce ( function ( a , b ) { return a + b ; }, 0 ) / f . length : 0 ;
$ ( 'an-vals' ). innerHTML = f . map ( function ( x , i ) {
return '<div class="an-col" style="height:' + ( x * 100 ) + '%" ' + tipAttr ( vshort ( V [ i ]. validator ) + ': провал ' + Math . round ( x * 100 ) + '% из ' + fmt ( V [ i ]. total ) + ' https-проверок' ) + '></div>' ;
}). join ( '' ) + '<div class="an-avg" style="bottom:' + ( avg * 100 ) + '%"></div>' ;
$ ( 'an-vfirst' ). textContent = V . length ? 'валидатор ' + vshort ( V [ 0 ]. validator ) : '' ;
$ ( 'an-vlast' ). textContent = V . length > 1 ? 'валидатор ' + vshort ( V [ V . length - 1 ]. validator ) : '' ;
$ ( 'an-vavg' ). textContent = V . length ? 'среднее ' + Math . round ( avg * 100 ) + '% (шкала 0– 100%)' : 'нет данных' ;
}
/* ---- address lists in the dialog ---- */
var VERDICT_HINT = 'Итог всего адреса за цикл, который система выставила при агрегации. pass: все проверки (egress и ingress), полученные к этому моменту, успешны. partial: часть проверок провалена или результатов не хватает. fail: все проверки провалены. Это оценка адреса, а не этой проверки.' ;
var STATUS_HINT = 'Состояние именно этой проверки (она всегда проваленная). «в вердикте»: результат пришёл до того, как система выставила вердикт адресу, и повлиял на него. «после вердикта»: результат пришёл позже, вердикт уже был выставлен и не пересчитывался. Поэтому у адреса с вердиктом pass может быть проваленная проверка.' ;
var VERDICT_VAL = {
pass : 'pass: к моменту вердикта все полученные проверки адреса были успешны. Эта проверка провалилась позже и в вердикт не вошла.' ,
partial : 'partial: часть проверок адреса провалена или результатов не хватило, вердикт не pass.' ,
fail : 'fail: все проверки адреса провалены.'
};
function statusVal ( v ) { return String ( v ). indexOf ( 'после' ) >= 0 ? 'Результат пришёл после вердикта адреса и в него не вошёл.' : 'Результат пришёл до вердикта и учтён в нём.' ; }
2026-10-04 10:00:39 +03:00
var VERDICT_NOTE = ' Это вердикт адреса, который система выставила при агрегации, а не итог всех проверок: после него могли прийти ещё результаты, поэтому у адреса pass бывают проваленные проверки. Столбцы Egress и Ingress показывают фактические проверки адреса.' ;
var VERDICT_HINTS = {
2 : 'Валидатор, с которого шли egress-проверки адреса. «—» — у адреса нет https-проверок.' ,
3 : 'Сколько egress-проверок адреса успешно из всех записанных в цикле. «—» — egress-проверок нет.' ,
4 : 'Сколько ingress-проверок адреса успешно из всех записанных в цикле (со всех площадок и всех типов). «—» — ingress-проверок нет.' ,
5 : 'Сколько проверок записано в цикле из ожидаемых. Меньше ожидаемого значит, что часть результатов не пришла (неполный набор). Если ожидаемое число неизвестно, показано только записанное.'
};
function verdictList ( title , note , reason ) {
var hints = {};
Object . keys ( VERDICT_HINTS ). forEach ( function ( k ) { hints [ k ] = VERDICT_HINTS [ k ]; });
if ( reason ) hints [ 6 ] = 'Что не так у адреса: провалены egress-проверки, ingress-проверки или не хватает результатов (неполный набор). Те же названия, что в блоке «Почему partial».' ;
return { title : title , note : note + VERDICT_NOTE , hints : hints };
}
2026-10-03 18:36:03 +03:00
var LISTS = {
2026-10-04 10:00:39 +03:00
verdict_pass : verdictList ( 'Адреса с вердиктом pass' ,
'Вердикт pass: адрес за последний цикл запуска получил успех по всем проверкам, полученным к моменту агрегации.' ),
verdict_partial : verdictList ( 'Адреса с вердиктом partial' ,
'Вердикт partial: часть проверок адреса провалена или результатов не хватило.' , true ),
verdict_fail : verdictList ( 'Адреса с вердиктом fail' ,
'Вердикт fail: все проверки адреса провалены.' ),
2026-10-03 18:36:03 +03:00
egress_https_any : {
title : 'Egress https: адреса с проваленными проверками' ,
note : 'Хотя бы одна egress-проверка https адреса провалена в последнем цикле запуска.' ,
hints : { 2 : 'Валидатор, с которого шли egress-проверки адреса.' , 3 : 'Сколько https-проверок адреса провалено из всех записанных в цикле.' }
},
ingress_ssh_any : {
title : 'Ingress ssh: адреса с проваленными проверками' ,
note : 'ssh провален хотя бы с одной площадки в последнем цикле запуска.' ,
hints : { 2 : 'Сколько площадок не смогли выполнить ssh-проверку адреса из всех, где она выполнялась.' , 3 : 'Площадки пробера, с которых ssh-проверка адреса провалена.' , 4 : 'Класс ошибки: таймаут, баннер «Not allowed», нет маршрута.' }
},
egress_https_all : {
title : 'Egress https: адреса, провалившие все проверки' ,
note : 'Все записанные egress-проверки https адреса провалены в последнем цикле запуска.' ,
hints : { 2 : 'Валидатор, с которого шли egress-проверки адреса.' , 3 : 'Сколько https-проверок записано у адреса в цикле. Все они провалены. Меньше полного набора значит, что часть результатов не пришла.' }
},
ingress_ssh_all : {
title : 'Ingress ssh: адреса, провалившие все проверки' ,
note : 'ssh провален со всех площадок, на которых он проверялся, в последнем цикле запуска.' ,
hints : { 2 : 'Площадки пробера, с которых ssh-проверка адреса провалена.' , 3 : 'Класс ошибки ssh-проверки: таймаут, баннер «Not allowed», нет маршрута.' }
}
};
var cur = null ; // the list shown in the dialog
function listURL ( base , kind , cls ) {
return base + encodeURIComponent ( kind ) + '?run=' + M . run_id + ( cls ? '&class=' + encodeURIComponent ( cls ) : '' );
}
function csvText ( cols , rows ) {
function q ( v ) { return '"' + String ( v ). replace ( /"/g , '""' ) + '"' ; }
return [ cols ]. concat ( rows ). map ( function ( r ) { return r . map ( q ). join ( ',' ); }). join ( '\r\n' ) + '\r\n' ;
}
function openDialog () {
var d = $ ( 'an-dlg' );
if ( d . open ) return ;
if ( d . showModal ) d . showModal (); else d . setAttribute ( 'open' , '' );
}
function fillDialog ( m ) {
cur = m ;
$ ( 'an-dlg-title' ). textContent = m . title + ' · ' + fmt ( m . rows . length );
$ ( 'an-dlg-note' ). textContent = 'Запуск: ' + m . runLabel + '. ' + m . note ;
$ ( 'an-dlg-dist' ). hidden = ! m . dist ;
$ ( 'an-dlg-dist' ). innerHTML = m . dist || '' ;
var hint = m . hints || {};
$ ( 'an-dlg-tbl' ). innerHTML = '<thead><tr>' + m . cols . map ( function ( c , i ) {
return hint [ i ] ? '<th ' + tipAttr ( hint [ i ]) + ' tabindex="0">' + esc ( c ) + '<span class="an-hint" aria-hidden="true">ⓘ</span></th>' : '<th>' + esc ( c ) + '</th>' ;
}). join ( '' ) + '</tr></thead><tbody>' + m . rows . map ( function ( x ) {
return '<tr>' + x . map ( function ( v , i ) {
var h = m . cellHints && m . cellHints [ i ] ? m . cellHints [ i ]( v ) : '' ;
return '<td class="' + ( i < 2 ? 'an-a' : '' ) + '"' + ( h ? ' ' + tipAttr ( h ) : '' ) + '>' + esc ( v ) + '</td>' ;
}). join ( '' ) + '</tr>' ;
}). join ( '' ) + '</tbody>' ;
$ ( 'an-dlg-msg' ). textContent = '' ;
}
function loadList ( kind , cls , meta ) {
$ ( 'an-dlg-title' ). textContent = meta . title ;
$ ( 'an-dlg-note' ). textContent = 'Загрузка…' ;
$ ( 'an-dlg-dist' ). hidden = true ;
$ ( 'an-dlg-tbl' ). innerHTML = '' ;
$ ( 'an-dlg-msg' ). textContent = '' ;
cur = null ;
openDialog ();
return fetch ( listURL ( M . list_url , kind , cls ), { headers : { Accept : 'application/json' }, credentials : 'same-origin' })
. then ( function ( r ) { if ( ! r . ok ) throw new Error ( 'HTTP ' + r . status ); return r . json (); })
. then ( function ( l ) { return l ; })
. catch ( function ( e ) {
$ ( 'an-dlg-note' ). textContent = 'Не удалось загрузить список: ' + e . message ;
return null ;
});
}
function runLabel () {
var o = document . getElementById ( 'an-run' );
return o && o . selectedOptions [ 0 ] ? o . selectedOptions [ 0 ]. textContent : 'запуск ' + M . run_id ;
}
function openIndicator ( kind ) {
var meta = LISTS [ kind ];
loadList ( kind , '' , meta ). then ( function ( l ) {
if ( ! l ) return ;
fillDialog ({ title : meta . title , note : meta . note , hints : meta . hints , cols : l . columns , rows : l . rows , runLabel : runLabel (), file : kind , kind : kind , cls : '' });
});
}
function openError ( cls ) {
loadList ( 'error' , cls , { title : 'Ingress: ' + cls }). then ( function ( l ) {
if ( ! l ) return ;
var rows = l . rows , labels = R . validators . map ( function ( v ) { return vshort ( v . validator ); });
rows . forEach ( function ( x ) { if ( labels . indexOf ( x [ 3 ]) < 0 ) labels . push ( x [ 3 ]); });
var byVal = labels . map ( function ( lab ) { return rows . filter ( function ( x ) { return x [ 3 ] === lab ; }). length ; });
var max = Math . max . apply ( null , byVal . concat ([ 1 ]));
var late = rows . filter ( function ( x ) { return x [ 5 ]. indexOf ( 'после' ) >= 0 ; }). length ;
var pass = rows . filter ( function ( x ) { return x [ 4 ] === 'pass' ; }). length ;
var grid = 'grid-template-columns:repeat(' + labels . length + ',minmax(0,1fr))' ;
var dist = '<h2>Распределение по валидаторам</h2>' +
'<div class="an-cols20" style="' + grid + '" role="img" aria-label="Число проваленных проверок по валидаторам">' + byVal . map ( function ( v , i ) {
return '<div style="height:' + Math . max ( 1 , v / max * 100 ) + '%" ' + tipAttr ( 'валидатор ' + labels [ i ] + ': ' + v + ' проверок' ) + '></div>' ;
}). join ( '' ) + '</div>' +
'<div class="an-lab20" style="' + grid + '">' + labels . map ( function ( lab ) { return '<span>' + esc ( lab . replace ( /^v/ , '' )) + '</span>' ; }). join ( '' ) + '</div>' +
'<div class="an-chips"><span class="an-tag an-t-n">всего ' + fmt ( rows . length ) + '</span><span class="an-tag an-t-ok">вердикт pass: ' + fmt ( pass ) + '</span><span class="an-tag an-t-warn">вердикт partial: ' + fmt ( rows . length - pass ) + '</span><span class="an-tag an-t-bad">после вердикта: ' + fmt ( late ) + '</span><span class="an-tag an-t-n">в вердикте: ' + fmt ( rows . length - late ) + '</span></div>' ;
fillDialog ({
title : 'Ingress: ' + cls ,
note : 'Проваленные проверки этого класса в последнем цикле каждого адреса. Валидатор — тот, к которому был привязан адрес в этом цикле. «После вердикта» — результат пришёл позже агрегации и в вердикт адреса не вошёл.' ,
cols : l . columns , rows : rows , runLabel : runLabel (), dist : dist , kind : 'error' , cls : cls ,
hints : { 3 : 'Валидатор, к которому был привязан адрес в этом цикле (берётся из события привязки Floating IP).' , 4 : VERDICT_HINT , 5 : STATUS_HINT },
cellHints : { 4 : function ( v ) { return VERDICT_VAL [ v ] || '' ; }, 5 : statusVal }
});
});
}
function copyCsv () {
var msg = $ ( 'an-dlg-msg' );
if ( ! cur ) return ;
var text = csvText ( cur . cols , cur . rows );
var done = function () { msg . textContent = 'CSV скопирован в буфер обмена.' ; };
var fail = function () { msg . textContent = 'Буфер обмена недоступен в этом окне.' ; };
if ( navigator . clipboard && navigator . clipboard . writeText ) navigator . clipboard . writeText ( text ). then ( done , fail ); else fail ();
}
function downloadCsv () {
if ( ! cur ) return ;
window . location . href = listURL ( M . csv_url , cur . kind , cur . cls );
}
/* ---- wiring ---- */
function renderAll () {
renderSubnets ();
renderTypes ();
renderTargets ();
}
$ ( 'an-runnote' ). textContent = 'Тип: ' + M . kind + '. Начало ' + M . start + ', завершён ' + M . end + ', длительность ' + M . duration + '.' +
( M . rechecked ? ' Перепроверено внутри запуска: ' + M . rechecked + ' адр. (берётся последний цикл).' : '' );
renderKpis (); renderReasons (); renderQuality (); renderErrors (); renderSites (); renderValidators (); renderAll ();
$ ( 'an-kpis' ). addEventListener ( 'click' , function ( e ) { var b = e . target . closest ( '[data-list]' ); if ( b ) openIndicator ( b . dataset . list ); });
$ ( 'an-errs' ). addEventListener ( 'click' , function ( e ) { var b = e . target . closest ( '[data-cls]' ); if ( b ) openError ( b . dataset . cls ); });
document . querySelectorAll ( '[data-sort]' ). forEach ( function ( b ) {
b . addEventListener ( 'click' , function () {
state . sort = b . dataset . sort ;
document . querySelectorAll ( '[data-sort]' ). forEach ( function ( x ) { x . setAttribute ( 'aria-pressed' , x === b ); });
renderSubnets ();
});
});
$ ( 'an-types' ). addEventListener ( 'click' , function ( e ) {
var b = e . target . closest ( '[data-type]' );
if ( ! b ) return ;
state . type = b . dataset . type ;
renderTypes (); renderTargets ();
});
$ ( 'an-allsub' ). addEventListener ( 'click' , function () { state . all = ! state . all ; renderSubnets (); renderTargets (); });
$ ( 'an-dlg-csv' ). addEventListener ( 'click' , downloadCsv );
$ ( 'an-dlg-copy' ). addEventListener ( 'click' , copyCsv );
$ ( 'an-dlg-close' ). addEventListener ( 'click' , function () { $ ( 'an-dlg' ). close (); });
$ ( 'an-dlg' ). addEventListener ( 'click' , function ( e ) { if ( e . target === $ ( 'an-dlg' )) $ ( 'an-dlg' ). close (); });
/* tooltip: moved into the open dialog, otherwise the modal's top layer covers it */
var tip = $ ( 'an-tip' );
function tipHost () { var d = $ ( 'an-dlg' ); return d && d . open ? d : document . body ; }
document . addEventListener ( 'mouseover' , function ( e ) {
var t = e . target . closest ( '[data-tip]' );
if ( ! t ) return ;
var h = tipHost ();
if ( tip . parentNode !== h ) h . appendChild ( tip );
tip . textContent = t . dataset . tip ;
tip . style . opacity = 1 ;
});
document . addEventListener ( 'mousemove' , function ( e ) {
tip . style . left = Math . max ( 8 , Math . min ( e . clientX + 14 , window . innerWidth - 376 )) + 'px' ;
tip . style . top = ( e . clientY + 16 ) + 'px' ;
});
document . addEventListener ( 'mouseout' , function ( e ) { if ( e . target . closest ( '[data-tip]' )) tip . style . opacity = 0 ; });
})();