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 ", " }}