2026-10-03 18:36:03 +03:00
package analytics
import (
"fmt"
"net/netip"
"sort"
"strings"
"cloudipvalidator/internal/db"
)
// List kinds served by Analysis.List.
const (
ListEgressHTTPSAny = "egress_https_any"
ListEgressHTTPSAll = "egress_https_all"
ListIngressSSHAny = "ingress_ssh_any"
ListIngressSSHAll = "ingress_ssh_all"
ListError = "error"
2026-10-04 10:00:39 +03:00
// Addresses by the verdict the system gave them at aggregation.
ListVerdictPass = "verdict_" + db . ResultPass
ListVerdictPartial = "verdict_" + db . ResultPartial
ListVerdictFail = "verdict_" + db . ResultFail
2026-10-03 18:36:03 +03:00
)
// List is a table of addresses behind one indicator or one error class.
type List struct {
Kind string `json:"kind"`
Class string `json:"class,omitempty"`
Columns [] string `json:"columns"`
Rows [][] string `json:"rows"`
}
// ErrUnknownList is returned for a list kind that does not exist.
type ErrUnknownList string
func ( e ErrUnknownList ) Error () string { return fmt . Sprintf ( "unknown list %q" , string ( e )) }
// List builds the table for a kind; class is only used with ListError.
func ( an * Analysis ) List ( kind , class string ) ( * List , error ) {
l := & List { Kind : kind , Class : class , Rows : [][] string {}}
switch kind {
case ListEgressHTTPSAny , ListEgressHTTPSAll :
l . Columns = [] string { "Адрес" , "Подсеть" , "Валидатор" , "https-проверок" , "Проваленные цели" }
if kind == ListEgressHTTPSAny {
l . Columns [ 3 ] = "Провалено https"
}
for _ , a := range an . sorted () {
if a . https . n == 0 || a . https . ok == a . https . n || ( kind == ListEgressHTTPSAll && a . https . ok != 0 ) {
continue
}
targets := append ([] string ( nil ), a . https . failedTargets ... )
sort . Strings ( targets )
count := fmt . Sprint ( a . https . n )
if kind == ListEgressHTTPSAny {
count = fmt . Sprintf ( "%d из %d" , a . https . n - a . https . ok , a . https . n )
}
l . Rows = append ( l . Rows , [] string { a . res . IPAddress , a . subnet , ShortValidator ( a . https . validator ), count , strings . Join ( targets , ", " )})
}
case ListIngressSSHAny , ListIngressSSHAll :
l . Columns = [] string { "Адрес" , "Подсеть" , "Провалено ssh" , "Площадки с провалом" , "Ошибка" }
if kind == ListIngressSSHAll {
l . Columns = [] string { "Адрес" , "Подсеть" , "Площадки с провалом ssh" , "Ошибка" }
}
for _ , a := range an . sorted () {
if a . ssh . n == 0 || a . ssh . ok == a . ssh . n || ( kind == ListIngressSSHAll && a . ssh . ok != 0 ) {
continue
}
sites := an . sortSites ( a . ssh . sites )
errs := make ([] string , 0 , len ( a . ssh . errs ))
for e := range a . ssh . errs {
errs = append ( errs , e )
}
sort . Strings ( errs )
if kind == ListIngressSSHAny {
l . Rows = append ( l . Rows , [] string { a . res . IPAddress , a . subnet , fmt . Sprintf ( "%d из %d" , len ( a . ssh . sites ), a . ssh . n ), strings . Join ( sites , ", " ), strings . Join ( errs , ", " )})
} else {
l . Rows = append ( l . Rows , [] string { a . res . IPAddress , a . subnet , strings . Join ( sites , ", " ), strings . Join ( errs , ", " )})
}
}
2026-10-04 10:00:39 +03:00
case ListVerdictPass , ListVerdictPartial , ListVerdictFail :
verdict := strings . TrimPrefix ( kind , "verdict_" )
l . Columns = [] string { "Адрес" , "Подсеть" , "Валидатор" , "Egress" , "Ingress" , "Проверок в цикле" }
if kind == ListVerdictPartial {
l . Columns = append ( l . Columns , "Причина" )
}
for _ , a := range an . sorted () {
if a . res . Verdict != verdict {
continue
}
row := [] string { a . res . IPAddress , a . subnet , orDash ( ShortValidator ( a . https . validator )), okOf ( a . egress ), okOf ( a . ingress ), a . checksCell ()}
if kind == ListVerdictPartial {
row = append ( row , reasonName ( a . egress . ok < a . egress . n , a . ingress . ok < a . ingress . n , a . incomplete ()))
}
l . Rows = append ( l . Rows , row )
}
2026-10-03 18:36:03 +03:00
case ListError :
if class == "" {
return nil , ErrUnknownList ( "error without class" )
}
l . Columns = [] string { "Адрес" , "Подсеть" , "Площадка" , "Валидатор" , "Вердикт адреса" , "Статус проверки" }
for _ , a := range an . sorted () {
fs := append ([] failedIngress ( nil ), a . failedIngress ... )
sort . SliceStable ( fs , func ( i , j int ) bool { return an . siteIndex ( fs [ i ]. site ) < an . siteIndex ( fs [ j ]. site ) })
for _ , f := range fs {
if f . class != class {
continue
}
status := "провал, в вердикте"
if f . late {
status = "провал, после вердикта"
}
l . Rows = append ( l . Rows , [] string { a . res . IPAddress , a . subnet , f . site , ShortValidator ( f . validator ), a . res . Verdict , status })
}
}
default :
return nil , ErrUnknownList ( kind )
}
return l , nil
}
2026-10-04 10:00:39 +03:00
// checksCell is "stored из expected", or just stored when expected is unknown.
func ( a * addr ) checksCell () string {
if a . res . ExpectedChecks < 0 {
return fmt . Sprint ( a . stored )
}
return fmt . Sprintf ( "%d из %d" , a . stored , a . res . ExpectedChecks )
}
// okOf is "successful из all" of one level, "—" when it has no checks.
func okOf ( t typeStat ) string {
if t . n == 0 {
return "—"
}
return fmt . Sprintf ( "%d из %d" , t . ok , t . n )
}
func orDash ( s string ) string {
if s == "" {
return "—"
}
return s
}
2026-10-03 18:36:03 +03:00
// sorted returns the non-cancelled addresses in numeric address order.
func ( an * Analysis ) sorted () [] * addr {
out := make ([] * addr , 0 , len ( an . addrs ))
for _ , a := range an . addrs {
if a . res . Verdict != db . ResultCancelled {
out = append ( out , a )
}
}
sort . Slice ( out , func ( i , j int ) bool {
x , errX := netip . ParseAddr ( out [ i ]. res . IPAddress )
y , errY := netip . ParseAddr ( out [ j ]. res . IPAddress )
if errX != nil || errY != nil {
return out [ i ]. res . IPAddress < out [ j ]. res . IPAddress
}
return x . Less ( y )
})
return out
}
func ( an * Analysis ) siteIndex ( site string ) int { return siteIndexOf ( an . siteNames , site ) }
func ( an * Analysis ) sortSites ( sites [] string ) [] string {
out := append ([] string ( nil ), sites ... )
sort . SliceStable ( out , func ( i , j int ) bool { return an . siteIndex ( out [ i ]) < an . siteIndex ( out [ j ]) })
return out
}