package checker import ( "bytes" "encoding/json" "fmt" "html/template" sdk "git.happydns.org/checker-sdk-go/checker" ) // GetHTMLReport renders the legacy-records observation as a self-contained // HTML page suitable for iframe embedding. // // The "fix this first" card is driven by the most-severe finding (no fixed // rule wins by name): SeverityCrit > SeverityWarn > SeverityInfo, with the // alphabetically-first type name as a stable tie-break. This matches what // the rule sorter produces, so the top card and the rule output never // disagree on which finding is "the" priority. func (p *legacyProvider) GetHTMLReport(ctx sdk.ReportContext) (string, error) { var data LegacyData if raw := ctx.Data(); len(raw) > 0 { if err := json.Unmarshal(raw, &data); err != nil { return "", fmt.Errorf("parse legacy-records data: %w", err) } } view := buildReportView(&data, ctx.States()) buf := &bytes.Buffer{} if err := reportTmpl.Execute(buf, view); err != nil { return "", err } return buf.String(), nil } type reportView struct { Zone string ServicesScanned int Total int OverallStatus string OverallText string OverallClass string Top *findingCard Others []findingCard CollectErrors []string } type findingCard struct { TypeName string Reason string Replacement string HowToFix string Severity string SeverityCSS string Count int Locations []FindingLocation } func buildReportView(data *LegacyData, states []sdk.CheckState) *reportView { v := &reportView{ Zone: data.Zone, ServicesScanned: data.ServicesScanned, Total: len(data.Findings), CollectErrors: data.CollectErrors, } groups := groupFindings(data.Findings) cards := make([]findingCard, 0, len(groups)) worst := SeverityInfo for _, g := range groups { info := deprecatedTypes[g.Rrtype] if info.Severity > worst { worst = info.Severity } cards = append(cards, findingCard{ TypeName: g.TypeName, Reason: info.Reason, Replacement: info.Replacement, HowToFix: info.HowToFix, Severity: severityLabel(info.Severity), SeverityCSS: info.Severity.String(), Count: len(g.Locations), Locations: g.Locations, }) } if len(cards) > 0 { v.Top = &cards[0] v.Others = cards[1:] v.OverallStatus = worst.String() v.OverallText, v.OverallClass = overallLabel(worst) } else { // Honour the rule's status when present: an Error from the rule // (e.g. observation load failure) must not be masked as "OK". if errState, ok := firstErrorState(states); ok { v.OverallStatus = "error" v.OverallText = errState.Message v.OverallClass = "status-crit" } else { v.OverallStatus = "ok" v.OverallText = fmt.Sprintf("No legacy record types found across %d service(s).", data.ServicesScanned) v.OverallClass = "status-ok" } } return v } func firstErrorState(states []sdk.CheckState) (sdk.CheckState, bool) { for i := range states { if states[i].Status == sdk.StatusError { return states[i], true } } return sdk.CheckState{}, false } func severityLabel(s DeprecatedSeverity) string { switch s { case SeverityCrit: return "Critical" case SeverityWarn: return "Warning" default: return "Informational" } } func overallLabel(s DeprecatedSeverity) (text, css string) { switch s { case SeverityCrit: return "Legacy records require urgent migration", "status-crit" case SeverityWarn: return "Legacy records should be migrated", "status-warn" default: return "Only informational legacy records found", "status-info" } } var reportTmpl = template.Must(template.New("legacy-records-report").Funcs(template.FuncMap{ "display": func(s string) string { if s == "" || s == "@" { return "@" } return s }, }).Parse(reportTemplate)) const reportTemplate = `
{{.Zone}} · {{end}}{{.ServicesScanned}} service(s) scanned · {{.Total}} legacy record(s) found{{.Top.Replacement}} instead{{end}}| Subdomain | Owner | Service |
|---|---|---|
{{display .Subdomain}} |
{{if .Name}}{{.Name}}{{else}}—{{end}} |
{{if .ServiceType}}{{.ServiceType}}{{else}}—{{end}} |
{{.Replacement}} instead{{end}}{{display $l.Subdomain}}{{end}}