Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
134cd9001a | ||
|
|
1218df9467 |
@@ -21,6 +21,8 @@ trial run of the finished image: https://git.eeqj.de/sneak/dnswatcher/issues/149
|
||||
|
||||
- 2026-10-02: an apex domain is not counted or listed as a hostname; its records
|
||||
show under Domains, and notifications about them say `Domain:` (closes #224).
|
||||
- 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
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"cmp"
|
||||
"embed"
|
||||
"fmt"
|
||||
"html/template"
|
||||
"maps"
|
||||
"math"
|
||||
"net/http"
|
||||
"slices"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -129,16 +132,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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package handlers_test
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -80,6 +81,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()
|
||||
@@ -155,4 +195,13 @@ func TestDashboardShowsDomainRecordsUnderDomains(t *testing.T) {
|
||||
if !strings.Contains(words, footer) {
|
||||
t.Errorf("dashboard does not say %q", footer)
|
||||
}
|
||||
|
||||
// With the tags taken out, the summary bar starts "Domains 1
|
||||
// Hostnames 1".
|
||||
text := regexp.MustCompile(`<[^>]*>`).ReplaceAllString(page, " ")
|
||||
summary := "Domains 1 Hostnames 1"
|
||||
|
||||
if !strings.Contains(strings.Join(strings.Fields(text), " "), summary) {
|
||||
t.Errorf("summary bar does not say %q", summary)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user