dashboard: list record types in the README's order (closes #226) #243

Merged
clawbot merged 1 commits from issue-226-record-type-order into next 2026-10-02 09:55:07 +02:00
3 changed files with 68 additions and 3 deletions
+2
View File
@@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
# Completed Steps # Completed Steps
- 2026-10-02: the dashboard lists each nameserver's record types in one fixed
order, the README's, then any other type, not a random one (closes #226).
- 2026-10-02: the dashboard and `/api/v1/status` show why a nameserver query or - 2026-10-02: the dashboard and `/api/v1/status` show why a nameserver query or
a certificate check failed, which only the state file showed (closes #225). a certificate check failed, which only the state file showed (closes #225).
- 2026-10-02: a name's CNAME is stored once per nameserver, not once per record - 2026-10-02: a name's CNAME is stored once per nameserver, not once per record
+27 -3
View File
@@ -1,11 +1,14 @@
package handlers package handlers
import ( import (
"cmp"
"embed" "embed"
"fmt" "fmt"
"html/template" "html/template"
"maps"
"math" "math"
"net/http" "net/http"
"slices"
"strings" "strings"
"time" "time"
@@ -122,16 +125,37 @@ func joinStrings(items []string, sep string) string {
} }
// formatRecords formats a map of record type → values into a // formatRecords formats a map of record type → values into a
// compact display string. // compact display string. Record types are listed in the order the
// README lists them, any other type after them in alphabetical order,
// so rows of nameservers with the same records read the same.
func formatRecords(records map[string][]string) string { func formatRecords(records map[string][]string) string {
if len(records) == 0 { if len(records) == 0 {
return "-" return "-"
} }
order := []string{"A", "AAAA", "CNAME", "MX", "TXT", "SRV", "CAA", "NS"}
position := func(rtype string) int {
i := slices.Index(order, rtype)
if i < 0 {
return len(order)
}
return i
}
rtypes := slices.Collect(maps.Keys(records))
slices.SortFunc(rtypes, func(a, b string) int {
return cmp.Or(
cmp.Compare(position(a), position(b)),
strings.Compare(a, b),
)
})
var parts []string var parts []string
for rtype, values := range records { for _, rtype := range rtypes {
for _, v := range values { for _, v := range records[rtype] {
parts = append(parts, rtype+": "+v) parts = append(parts, rtype+": "+v)
} }
} }
+39
View File
@@ -80,6 +80,45 @@ func TestFormatRecords(t *testing.T) {
} }
} }
// TestFormatRecordsTypeOrder checks that record types are listed in
// the README's order (A, AAAA, CNAME, MX, TXT, SRV, CAA, NS), with
// any other type after them in alphabetical order.
func TestFormatRecordsTypeOrder(t *testing.T) {
t.Parallel()
got := handlers.FormatRecords(map[string][]string{
"SOA": {"ns1.example.com. hostmaster.example.com. 1 2 3 4 5"},
"NS": {"ns1.example.com.", "ns2.example.com."},
"CAA": {`0 issue "letsencrypt.org"`},
"DNAME": {"example.net."},
"TXT": {"v=spf1 -all"},
"SRV": {"10 5 443 www.example.com."},
"MX": {"10 mail.example.com."},
"CNAME": {"www.example.com."},
"AAAA": {"2001:db8::1"},
"A": {"192.0.2.1"},
})
want := strings.Join([]string{
"A: 192.0.2.1",
"AAAA: 2001:db8::1",
"CNAME: www.example.com.",
"MX: 10 mail.example.com.",
"TXT: v=spf1 -all",
"SRV: 10 5 443 www.example.com.",
`CAA: 0 issue "letsencrypt.org"`,
"NS: ns1.example.com.",
"NS: ns2.example.com.",
"DNAME: example.net.",
"SOA: ns1.example.com. hostmaster.example.com. 1 2 3 4 5",
}, ", ")
if got != want {
t.Errorf("FormatRecords lists types out of order:\n got %q\nwant %q",
got, want)
}
}
// dashboardRow returns the table row of page that contains name. // dashboardRow returns the table row of page that contains name.
func dashboardRow(t *testing.T, page string, name string) string { func dashboardRow(t *testing.T, page string, name string) string {
t.Helper() t.Helper()