All checks were successful
check / check (push) Successful in 1m27s
## Summary
Enhances the `/api/v1/status` endpoint to return comprehensive monitoring state instead of just `{"status": "ok"}`.
## Changes
The endpoint now returns:
- **Summary counts**: domains, hostnames, ports (total + open), certificates (total + ok + error)
- **Domains**: each monitored domain with its discovered nameservers and last check timestamp
- **Hostnames**: each monitored hostname with per-nameserver DNS records, status, and last check timestamps
- **Ports**: each monitored IP:port with open/closed state, associated hostnames, and last check timestamp
- **Certificates**: each TLS certificate with CN, issuer, expiry, SANs, status, and last check timestamp
- **Last updated**: timestamp of the overall monitoring state
All data is derived from the existing `state.GetSnapshot()`, consistent with how the dashboard works. No configuration details (webhook URLs, API tokens) are exposed.
## Example response structure
```json
{
"status": "ok",
"lastUpdated": "2026-03-10T12:00:00Z",
"counts": {
"domains": 2,
"hostnames": 3,
"ports": 10,
"portsOpen": 8,
"certificates": 4,
"certificatesOk": 3,
"certificatesError": 1
},
"domains": { ... },
"hostnames": { ... },
"ports": { ... },
"certificates": { ... }
}
```
closes #73
Co-authored-by: clawbot <clawbot@noreply.git.eeqj.de>
Reviewed-on: #86
Co-authored-by: clawbot <clawbot@noreply.example.org>
Co-committed-by: clawbot <clawbot@noreply.example.org>
219 lines
5.5 KiB
Go
219 lines
5.5 KiB
Go
package handlers
|
|
|
|
import (
|
|
"net/http"
|
|
"sort"
|
|
"time"
|
|
|
|
"sneak.berlin/go/dnswatcher/internal/state"
|
|
)
|
|
|
|
// statusDomainInfo holds status information for a monitored domain.
|
|
type statusDomainInfo struct {
|
|
Nameservers []string `json:"nameservers"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusHostnameNSInfo holds per-nameserver status for a hostname.
|
|
type statusHostnameNSInfo struct {
|
|
Records map[string][]string `json:"records"`
|
|
Status string `json:"status"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusHostnameInfo holds status information for a monitored hostname.
|
|
type statusHostnameInfo struct {
|
|
Nameservers map[string]*statusHostnameNSInfo `json:"nameservers"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusPortInfo holds status information for a monitored port.
|
|
type statusPortInfo struct {
|
|
Open bool `json:"open"`
|
|
Hostnames []string `json:"hostnames"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusCertificateInfo holds status information for a TLS certificate.
|
|
type statusCertificateInfo struct {
|
|
CommonName string `json:"commonName"`
|
|
Issuer string `json:"issuer"`
|
|
NotAfter time.Time `json:"notAfter"`
|
|
SubjectAlternativeNames []string `json:"subjectAlternativeNames"`
|
|
Status string `json:"status"`
|
|
LastChecked time.Time `json:"lastChecked"`
|
|
}
|
|
|
|
// statusCounts holds summary counts of monitored resources.
|
|
type statusCounts struct {
|
|
Domains int `json:"domains"`
|
|
Hostnames int `json:"hostnames"`
|
|
Ports int `json:"ports"`
|
|
PortsOpen int `json:"portsOpen"`
|
|
Certificates int `json:"certificates"`
|
|
CertsOK int `json:"certificatesOk"`
|
|
CertsError int `json:"certificatesError"`
|
|
}
|
|
|
|
// statusResponse is the full /api/v1/status response.
|
|
type statusResponse struct {
|
|
Status string `json:"status"`
|
|
LastUpdated time.Time `json:"lastUpdated"`
|
|
Counts statusCounts `json:"counts"`
|
|
Domains map[string]*statusDomainInfo `json:"domains"`
|
|
Hostnames map[string]*statusHostnameInfo `json:"hostnames"`
|
|
Ports map[string]*statusPortInfo `json:"ports"`
|
|
Certificates map[string]*statusCertificateInfo `json:"certificates"`
|
|
}
|
|
|
|
// HandleStatus returns the monitoring status handler.
|
|
func (h *Handlers) HandleStatus() http.HandlerFunc {
|
|
return func(
|
|
writer http.ResponseWriter,
|
|
request *http.Request,
|
|
) {
|
|
snap := h.state.GetSnapshot()
|
|
|
|
resp := buildStatusResponse(snap)
|
|
|
|
h.respondJSON(
|
|
writer, request,
|
|
resp,
|
|
http.StatusOK,
|
|
)
|
|
}
|
|
}
|
|
|
|
// buildStatusResponse constructs the full status response from
|
|
// the current monitoring snapshot.
|
|
func buildStatusResponse(
|
|
snap state.Snapshot,
|
|
) *statusResponse {
|
|
resp := &statusResponse{
|
|
Status: "ok",
|
|
LastUpdated: snap.LastUpdated,
|
|
Domains: make(map[string]*statusDomainInfo),
|
|
Hostnames: make(map[string]*statusHostnameInfo),
|
|
Ports: make(map[string]*statusPortInfo),
|
|
Certificates: make(map[string]*statusCertificateInfo),
|
|
}
|
|
|
|
buildDomains(snap, resp)
|
|
buildHostnames(snap, resp)
|
|
buildPorts(snap, resp)
|
|
buildCertificates(snap, resp)
|
|
buildCounts(resp)
|
|
|
|
return resp
|
|
}
|
|
|
|
func buildDomains(
|
|
snap state.Snapshot,
|
|
resp *statusResponse,
|
|
) {
|
|
for name, ds := range snap.Domains {
|
|
ns := make([]string, len(ds.Nameservers))
|
|
copy(ns, ds.Nameservers)
|
|
sort.Strings(ns)
|
|
|
|
resp.Domains[name] = &statusDomainInfo{
|
|
Nameservers: ns,
|
|
LastChecked: ds.LastChecked,
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildHostnames(
|
|
snap state.Snapshot,
|
|
resp *statusResponse,
|
|
) {
|
|
for name, hs := range snap.Hostnames {
|
|
info := &statusHostnameInfo{
|
|
Nameservers: make(map[string]*statusHostnameNSInfo),
|
|
LastChecked: hs.LastChecked,
|
|
}
|
|
|
|
for ns, nsState := range hs.RecordsByNameserver {
|
|
recs := make(map[string][]string, len(nsState.Records))
|
|
for rtype, vals := range nsState.Records {
|
|
copied := make([]string, len(vals))
|
|
copy(copied, vals)
|
|
recs[rtype] = copied
|
|
}
|
|
|
|
info.Nameservers[ns] = &statusHostnameNSInfo{
|
|
Records: recs,
|
|
Status: nsState.Status,
|
|
LastChecked: nsState.LastChecked,
|
|
}
|
|
}
|
|
|
|
resp.Hostnames[name] = info
|
|
}
|
|
}
|
|
|
|
func buildPorts(
|
|
snap state.Snapshot,
|
|
resp *statusResponse,
|
|
) {
|
|
for key, ps := range snap.Ports {
|
|
hostnames := make([]string, len(ps.Hostnames))
|
|
copy(hostnames, ps.Hostnames)
|
|
sort.Strings(hostnames)
|
|
|
|
resp.Ports[key] = &statusPortInfo{
|
|
Open: ps.Open,
|
|
Hostnames: hostnames,
|
|
LastChecked: ps.LastChecked,
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildCertificates(
|
|
snap state.Snapshot,
|
|
resp *statusResponse,
|
|
) {
|
|
for key, cs := range snap.Certificates {
|
|
sans := make([]string, len(cs.SubjectAlternativeNames))
|
|
copy(sans, cs.SubjectAlternativeNames)
|
|
|
|
resp.Certificates[key] = &statusCertificateInfo{
|
|
CommonName: cs.CommonName,
|
|
Issuer: cs.Issuer,
|
|
NotAfter: cs.NotAfter,
|
|
SubjectAlternativeNames: sans,
|
|
Status: cs.Status,
|
|
LastChecked: cs.LastChecked,
|
|
}
|
|
}
|
|
}
|
|
|
|
func buildCounts(resp *statusResponse) {
|
|
var portsOpen, certsOK, certsError int
|
|
|
|
for _, ps := range resp.Ports {
|
|
if ps.Open {
|
|
portsOpen++
|
|
}
|
|
}
|
|
|
|
for _, cs := range resp.Certificates {
|
|
switch cs.Status {
|
|
case "ok":
|
|
certsOK++
|
|
case "error":
|
|
certsError++
|
|
}
|
|
}
|
|
|
|
resp.Counts = statusCounts{
|
|
Domains: len(resp.Domains),
|
|
Hostnames: len(resp.Hostnames),
|
|
Ports: len(resp.Ports),
|
|
PortsOpen: portsOpen,
|
|
Certificates: len(resp.Certificates),
|
|
CertsOK: certsOK,
|
|
CertsError: certsError,
|
|
}
|
|
}
|