From 58a992b49a07765d751c566f6069662226f4294b 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 | 47 ++++++++++++++++++++- internal/handlers/templates/dashboard.html | 8 +++- 7 files changed, 140 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index 4461a44..8347db1 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, @@ -556,8 +559,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 1ddb111..3abd192 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: 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 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..2458db0 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, "]*>`) + + var cells []string + + for _, cell := range strings.Split(row, tag)[1:] { + 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 }} +
Address StateDomains Hostnames Checked
{{ $key }} @@ -187,6 +188,9 @@ > {{ end }} + {{ joinStrings $ps.Domains ", " }} + {{ joinStrings $ps.Hostnames ", " }}