From 1218df9467d37dcd4a9dfba653e3bf6678feac3c Mon Sep 17 00:00:00 2001 From: clawbot <35+clawbot@noreply.example.org> Date: Fri, 2 Oct 2026 09:55:07 +0200 Subject: [PATCH] dashboard: list record types in the README's order (closes #226) Each row of the Hostnames table listed a nameserver's record types in the order Go happens to walk the record map, which changed from row to row and on every page load, so two nameservers with the same records looked different. formatRecords now sorts the types by their place in the README's list (A, AAAA, CNAME, MX, TXT, SRV, CAA, NS); any other type comes after them in alphabetical order. Values within a type were already sorted by the resolver. Model: opus-5-5 --- TODO.md | 2 ++ internal/handlers/dashboard.go | 30 +++++++++++++++++++--- internal/handlers/dashboard_test.go | 39 +++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 3 deletions(-) diff --git a/TODO.md b/TODO.md index f71bbcf..2b8c2eb 100644 --- a/TODO.md +++ b/TODO.md @@ -19,6 +19,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149 # 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 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 diff --git a/internal/handlers/dashboard.go b/internal/handlers/dashboard.go index 6d4ac57..6b65e51 100644 --- a/internal/handlers/dashboard.go +++ b/internal/handlers/dashboard.go @@ -1,11 +1,14 @@ package handlers import ( + "cmp" "embed" "fmt" "html/template" + "maps" "math" "net/http" + "slices" "strings" "time" @@ -122,16 +125,37 @@ func joinStrings(items []string, sep string) string { } // 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 { if len(records) == 0 { 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 - for rtype, values := range records { - for _, v := range values { + for _, rtype := range rtypes { + for _, v := range records[rtype] { parts = append(parts, rtype+": "+v) } } diff --git a/internal/handlers/dashboard_test.go b/internal/handlers/dashboard_test.go index d60a36e..01e8d3f 100644 --- a/internal/handlers/dashboard_test.go +++ b/internal/handlers/dashboard_test.go @@ -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. func dashboardRow(t *testing.T, page string, name string) string { t.Helper()