feat(panel): show each domain's DNS status in the domain list
The list gave no hint which domains still needed records published — the verdict lived only on the domain page, one click away per domain. Each row now carries a badge with the worst of that domain's DKIM, SPF and DMARC checks, in the panel's shared ok/warn/error/unknown vocabulary, linking to that domain's DNS status card. The checks run concurrently across the listed domains: each carries its own timeout, so in series a dead resolver would multiply that wait by the number of domains and the list would look hung. They share the checker's cache with the domain page, so a repeat view costs no lookups and opening a domain after the list is free. A domain whose DKIM key cannot be read stays "unknown" rather than being reported as misconfigured — the missing half of the comparison is this server's, not the domain's. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -7,6 +7,16 @@ Format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); version
|
||||
|
||||
### Added
|
||||
|
||||
- A **DNS** badge in the domain list, one per row, carrying the same
|
||||
ok/warn/error/unknown vocabulary as the rest of the panel: the worst of that
|
||||
domain's DKIM, SPF and DMARC checks, so a domain whose records were never
|
||||
published is visible without opening it. The badge links to that domain's
|
||||
DNS status card. The checks run concurrently across the listed domains —
|
||||
each carries its own timeout, and in series a dead resolver would multiply
|
||||
that wait by the number of domains — and share the checker's cache with the
|
||||
domain page, so a repeat view costs no lookups. A domain whose DKIM key
|
||||
cannot be read stays "unknown" rather than being reported as misconfigured,
|
||||
since the missing half is this server's.
|
||||
- Machine metrics on the status page: a **Machine** card reporting the
|
||||
processor (busy percentage, core count, load average), memory and swap, and
|
||||
network throughput and totals per interface, read from the kernel's counters
|
||||
|
||||
@@ -5,10 +5,20 @@ import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"sync"
|
||||
|
||||
"github.com/mixeme/selfpost/internal/health"
|
||||
"github.com/mixeme/selfpost/internal/store"
|
||||
)
|
||||
|
||||
// domainRow is one line of the domain list: the stored domain plus the rolled-up
|
||||
// verdict of its published DNS records, so the operator sees which domains still
|
||||
// need a record published without opening each one.
|
||||
type domainRow struct {
|
||||
store.Domain
|
||||
DNS health.Status
|
||||
}
|
||||
|
||||
// handleDashboard is the authenticated landing page: the list of sending
|
||||
// domains with their DKIM/selector and application counts, plus the add-domain
|
||||
// form (product.md).
|
||||
@@ -30,13 +40,43 @@ func (s *Server) renderDashboard(w http.ResponseWriter, r *http.Request, status
|
||||
"Title": "SelfPost",
|
||||
"User": currentUser(r),
|
||||
"Active": "domains",
|
||||
"Domains": domains,
|
||||
"Domains": s.domainRows(domains),
|
||||
"Error": formErr,
|
||||
"FormName": formName,
|
||||
"Flash": dashboardFlash(r),
|
||||
})
|
||||
}
|
||||
|
||||
// domainRows attaches each domain's DNS verdict to its row. The checks run
|
||||
// concurrently rather than one after another: each carries its own timeout, so
|
||||
// in series a dead resolver would multiply that wait by the number of domains
|
||||
// and the list would look hung. The checker caches results for a few minutes,
|
||||
// so a repeat view of the list costs no lookups at all, and it is the same
|
||||
// cache the domain page fills — opening a domain after the list is free.
|
||||
func (s *Server) domainRows(domains []store.Domain) []domainRow {
|
||||
rows := make([]domainRow, len(domains))
|
||||
var wg sync.WaitGroup
|
||||
for i, d := range domains {
|
||||
rows[i] = domainRow{Domain: d, DNS: health.StatusUnknown}
|
||||
wg.Add(1)
|
||||
go func() {
|
||||
defer wg.Done()
|
||||
record, err := s.domains.DKIMRecord(d)
|
||||
if err != nil {
|
||||
// Without the expected key there is nothing to compare DNS
|
||||
// against; leave the row unknown rather than accusing the
|
||||
// domain of a misconfiguration this server caused.
|
||||
logf("panel: dashboard: domain %d: dkim record: %v", d.ID, err)
|
||||
return
|
||||
}
|
||||
dns, _ := s.domainDNS(d, record, false)
|
||||
rows[i].DNS = dns.Overall
|
||||
}()
|
||||
}
|
||||
wg.Wait()
|
||||
return rows
|
||||
}
|
||||
|
||||
// dashboardFlash maps a fixed redirect flag to a fixed message, so status text
|
||||
// after a redirect is never attacker-influenced.
|
||||
func dashboardFlash(r *http.Request) string {
|
||||
|
||||
@@ -336,6 +336,11 @@ button.danger:hover, a.danger:hover { background: #912018; }
|
||||
.st-warn { background: var(--st-warn-bg); color: var(--st-warn-fg); border-color: var(--st-warn-border); }
|
||||
.st-error { background: var(--st-error-bg); color: var(--st-error-fg); border-color: var(--st-error-border); }
|
||||
.st-unknown { background: var(--st-unknown-bg); color: var(--st-unknown-fg); border-color: var(--st-unknown-border); }
|
||||
/* In the domain list the badge is also the link to that domain's DNS section,
|
||||
so it must not pick up the link colour and underline that would fight with
|
||||
the badge's own palette. */
|
||||
a.st, a.st:hover { color: inherit; text-decoration: none; }
|
||||
a.st:hover { filter: brightness(1.08); }
|
||||
/* Usage bars on the status page's machine card. <meter> rather than a div sized
|
||||
from the reading, because the CSP forbids inline styles (see the note at the
|
||||
top of this file) and a bar's length has to travel on an attribute. The
|
||||
|
||||
@@ -22,12 +22,13 @@
|
||||
{{if .Domains}}
|
||||
<table>
|
||||
<thead>
|
||||
<tr><th>Domain</th><th>Selector</th><th>Apps</th><th></th></tr>
|
||||
<tr><th>Domain</th><th>DNS</th><th>Selector</th><th>Apps</th><th></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Domains}}
|
||||
<tr>
|
||||
<td><a href="/domains/{{.ID}}">{{.Name}}</a></td>
|
||||
<td><a class="st st-{{.DNS}}" href="/domains/{{.ID}}#dns-status">{{.DNS}}</a></td>
|
||||
<td class="muted">{{.DKIMSelector}}</td>
|
||||
<td>{{.AppCount}}</td>
|
||||
<td class="actions"><a class="danger" href="/domains/{{.ID}}/delete">Delete</a></td>
|
||||
@@ -35,6 +36,9 @@
|
||||
{{end}}
|
||||
</tbody>
|
||||
</table>
|
||||
<p class="muted">The <em>DNS</em> badge is the worst of the domain's DKIM, SPF
|
||||
and DMARC checks. Results are cached for a few minutes; open a domain for the
|
||||
details and a <em>Re-check</em> button.</p>
|
||||
{{else}}
|
||||
<p class="muted">No domains yet. Add one above to get started.</p>
|
||||
{{end}}
|
||||
|
||||
Reference in New Issue
Block a user