dashboard: list record types in the README's order (closes #226) #243
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
Reference in New Issue
Block a user