From bd9e3e27b9f019cc302ecccda224919b4fe36f23 Mon Sep 17 00:00:00 2001 From: sneak Date: Fri, 2 Oct 2026 08:16:50 +0000 Subject: [PATCH] dashboard and status API list a port's domains apart from its hostnames (closes #245) A port entry in the state saves the apex domains that resolve to its address with its hostnames. The dashboard's Ports table now has a Domains column next to Hostnames, and a port entry in /api/v1/status has a `domains` list, with `hostnames` no longer holding a domain. A name is taken as a domain when it has a domain entry, as the dashboard and API already tell a domain's own records from a hostname's. Both read the split from one function, buildPorts. The state file is unchanged; README says its port `hostnames` include domains. Model: opus-5-5 --- README.md | 12 ++-- TODO.md | 2 + internal/handlers/dashboard.go | 6 +- internal/handlers/dashboard_test.go | 48 +++++++++++++++ internal/handlers/status.go | 35 ++++++++--- internal/handlers/status_test.go | 72 ++++++++++++++++++---- internal/handlers/templates/dashboard.html | 8 ++- 7 files changed, 155 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 5939483..54db1ad 100644 --- a/README.md +++ b/README.md @@ -239,7 +239,8 @@ dnswatcher includes an unauthenticated, read-only web dashboard at the root URL per nameserver and status, shown as a hostname's are. - **Hostnames** with per-nameserver DNS records and status. For a nameserver whose query failed, the reason is shown in place of the records. -- **Ports** with open/closed state and associated hostnames. +- **Ports** with open/closed state and the domains and hostnames that resolve to + each address, in separate columns. - **TLS certificates** with CN, issuer, expiry, and status. For a failed check, the reason is shown in place of CN, issuer and expiry. - **Recent alerts** (last 100 notifications sent since the process started), @@ -272,7 +273,9 @@ In `/api/v1/status`, each nameserver entry and certificate entry whose `status` is `error` also has `error`, the reason, as in the state file (see State File Format). A domain's own records are in its entry in `domains`, under `recordsByNameserver`, in the form a hostname's entry in `hostnames` has them -under `nameservers`; `hostnames` and `counts.hostnames` hold no domain. +under `nameservers`; `hostnames` and `counts.hostnames` hold no domain. A port +entry lists the domains that resolve to its address in `domains`, and the +hostnames in `hostnames`. `/metrics` is served only when `DNSWATCHER_METRICS_USERNAME` is set, behind Basic Auth. It has the Prometheus Go client's default metrics only (Go runtime, @@ -561,8 +564,9 @@ previous check's list is kept, or `null` when no earlier check saved one. A state file without it loads, and the first check after that saves it without a notification. -A port entry in the older format, with one `hostname` instead of the `hostnames` -list, loads as a list of that one name. +A port entry's `hostnames` lists every name that resolves to its address, +domains included. A port entry in the older format, with one `hostname` instead +of the `hostnames` list, loads as a list of that one name. --- diff --git a/TODO.md b/TODO.md index 397b53c..54a414c 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's Ports table and `/api/v1/status` port entries list + a port's domains apart from its hostnames (closes #245). - 2026-10-02: nameservers a referral names without addresses are looked up, three deep at most; `pool.ntp.org`'s nameservers resolve (closes #221). - 2026-10-02: an apex domain is not counted or listed as a hostname; its records diff --git a/internal/handlers/dashboard.go b/internal/handlers/dashboard.go index fa7e8d5..a5068c8 100644 --- a/internal/handlers/dashboard.go +++ b/internal/handlers/dashboard.go @@ -45,11 +45,14 @@ func newDashboardTemplate() *template.Template { // dashboardData is the data passed to the dashboard template. Hostnames // and DomainRecords split the records in Snapshot.Hostnames, which also -// holds the apex domains' own (see splitHostnames). +// holds the apex domains' own (see splitHostnames). Ports holds +// Snapshot.Ports with each port's names split into domains and +// hostnames, as /api/v1/status gives them (see buildPorts). type dashboardData struct { Snapshot state.Snapshot Hostnames map[string]*state.HostnameState DomainRecords map[string]*state.HostnameState + Ports map[string]*statusPortInfo Alerts []notify.AlertEntry StateAge string GeneratedAt string @@ -71,6 +74,7 @@ func (h *Handlers) HandleDashboard() http.HandlerFunc { Snapshot: snap, Hostnames: hostnames, DomainRecords: domainRecords, + Ports: buildPorts(snap), Alerts: alerts, StateAge: relTime(snap.LastUpdated), GeneratedAt: time.Now().UTC().Format("2006-01-02 15:04:05"), diff --git a/internal/handlers/dashboard_test.go b/internal/handlers/dashboard_test.go index 27149d8..d49278f 100644 --- a/internal/handlers/dashboard_test.go +++ b/internal/handlers/dashboard_test.go @@ -205,3 +205,51 @@ func TestDashboardShowsDomainRecordsUnderDomains(t *testing.T) { t.Errorf("summary bar does not say %q", summary) } } + +// rowCells returns the text of each cell of a dashboard table row +// whose cells start with tag, "]*>`) + parts := strings.Split(row, tag)[1:] + cells := make([]string, 0, len(parts)) + + for _, cell := range parts { + text := tags.ReplaceAllString(tag+cell, " ") + cells = append(cells, strings.Join(strings.Fields(text), " ")) + } + + return cells +} + +// TestDashboardPortsTellDomainsFromHostnames checks that the Ports +// table lists an apex domain under Domains and a hostname under +// Hostnames when both resolve to the port's address. +func TestDashboardPortsTellDomainsFromHostnames(t *testing.T) { + t.Parallel() + + page := get(t, newHandlersWithFailures(t).HandleDashboard()) + ports := dashboardSection(t, page, "Ports") + + headings := rowCells(dashboardRow(t, ports, "Address"), " Ports - {{ if .Snapshot.Ports }} + {{ if .Ports }}
+ - {{ range $key, $ps := .Snapshot.Ports }} + {{ range $key, $ps := .Ports }} + -- 2.54.0
Address StateDomains Hostnames Checked
{{ $key }} @@ -187,6 +188,9 @@ > {{ end }} + {{ joinStrings $ps.Domains ", " }} + {{ joinStrings $ps.Hostnames ", " }}