package checker
import (
"encoding/json"
"fmt"
"html/template"
"sort"
"strings"
)
// ── HTML report ───────────────────────────────────────────────────────────────
// zmLevelDisplayOrder defines the severity order used for sorting and display.
var zmLevelDisplayOrder = []string{"CRITICAL", "ERROR", "WARNING", "NOTICE", "INFO", "DEBUG"}
var zmLevelRank = func() map[string]int {
m := make(map[string]int, len(zmLevelDisplayOrder))
for i, l := range zmLevelDisplayOrder {
m[l] = len(zmLevelDisplayOrder) - i
}
return m
}()
type zmLevelCount struct {
Level string
Count int
}
type zmModuleGroup struct {
Name string
Position int // first-seen index, used as tiebreaker in sort
Results []ZonemasterTestResult
Levels []zmLevelCount // sorted by severity desc, zeros omitted
Worst string
Open bool
}
type zmTemplateData struct {
Domain string
CreatedAt string
HashID string
Language string
Modules []zmModuleGroup
Totals []zmLevelCount // sorted by severity desc, zeros omitted
}
var zonemasterHTMLTemplate = template.Must(
template.New("zonemaster").
Funcs(template.FuncMap{
"badgeClass": func(level string) string {
switch strings.ToUpper(level) {
case "CRITICAL":
return "badge-critical"
case "ERROR":
return "badge-error"
case "WARNING":
return "badge-warning"
case "NOTICE":
return "badge-notice"
case "INFO":
return "badge-info"
default:
return "badge-debug"
}
},
}).
Parse(`
Zonemaster{{if .Domain}} — {{.Domain}}{{end}}
Zonemaster{{if .Domain}} — {{.Domain}}{{end}}
{{- if .CreatedAt}}Run at {{.CreatedAt}}{{end -}}
{{- if and .CreatedAt .HashID}} · {{end -}}
{{- if .HashID}}ID: {{.HashID}}{{end -}}
{{- range .Totals}}
{{.Level}} {{.Count}}
{{- end}}
{{range .Modules -}}
{{.Name}}
{{- range .Levels}}
{{.Count}}
{{- end}}
{{- range .Results}}
{{.Level}}
{{.Message}}
{{- if .Testcase}}
{{.Testcase}}
{{end}}
{{- end}}
{{end -}}
`),
)
// GetHTMLReport implements sdk.CheckerHTMLReporter.
func (p *zonemasterProvider) GetHTMLReport(raw json.RawMessage) (string, error) {
var data ZonemasterData
if err := json.Unmarshal(raw, &data); err != nil {
return "", fmt.Errorf("failed to unmarshal zonemaster results: %w", err)
}
// Group results by module, preserving first-seen order.
moduleOrder := []string{}
moduleMap := map[string][]ZonemasterTestResult{}
for _, r := range data.Results {
if _, seen := moduleMap[r.Module]; !seen {
moduleOrder = append(moduleOrder, r.Module)
}
moduleMap[r.Module] = append(moduleMap[r.Module], r)
}
totalCounts := map[string]int{}
var modules []zmModuleGroup
for _, name := range moduleOrder {
rs := moduleMap[name]
counts := map[string]int{}
for _, r := range rs {
lvl := strings.ToUpper(r.Level)
counts[lvl]++
totalCounts[lvl]++
}
// Find worst level and build sorted level-count slice.
worst := ""
worstRank := -1
var levels []zmLevelCount
for _, l := range zmLevelDisplayOrder {
if n, ok := counts[l]; ok && n > 0 {
levels = append(levels, zmLevelCount{Level: l, Count: n})
if zmLevelRank[l] > worstRank {
worstRank = zmLevelRank[l]
worst = l
}
}
}
// Append any unknown levels last.
for l, n := range counts {
if _, known := zmLevelRank[l]; !known {
levels = append(levels, zmLevelCount{Level: l, Count: n})
}
}
modules = append(modules, zmModuleGroup{
Name: name,
Position: len(modules),
Results: rs,
Levels: levels,
Worst: worst,
Open: worst == "CRITICAL" || worst == "ERROR",
})
}
// Sort modules: most severe first, then by original appearance order.
sort.Slice(modules, func(i, j int) bool {
ri, rj := zmLevelRank[modules[i].Worst], zmLevelRank[modules[j].Worst]
if ri != rj {
return ri > rj
}
return modules[i].Position < modules[j].Position
})
// Build sorted totals slice.
var totals []zmLevelCount
for _, l := range zmLevelDisplayOrder {
if n, ok := totalCounts[l]; ok && n > 0 {
totals = append(totals, zmLevelCount{Level: l, Count: n})
}
}
domain := ""
if d, ok := data.Params["domain"]; ok {
domain = fmt.Sprintf("%v", d)
}
lang := data.Language
if lang == "" {
lang = "en"
}
td := zmTemplateData{
Domain: domain,
CreatedAt: data.CreatedAt,
HashID: data.HashID,
Language: lang,
Modules: modules,
Totals: totals,
}
var buf strings.Builder
if err := zonemasterHTMLTemplate.Execute(&buf, td); err != nil {
return "", fmt.Errorf("failed to render zonemaster HTML report: %w", err)
}
return buf.String(), nil
}