Files
2026-08-16 21:18:45 -05:00

271 lines
11 KiB
HTML

{{define "content"}}
{{$d := .Data.D}}
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
<div>
<h1 class="page-title">Dashboard</h1>
<p class="text-body-secondary mb-0">
{{$d.Hostname}} · up {{$d.UptimeText}} · VibeDNS {{.Version}}
</p>
</div>
<div class="d-flex flex-wrap gap-2">
<a href="/tools" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-search me-1"></i>Test a lookup
</a>
<a href="/querylog" class="btn btn-outline-secondary btn-sm">
<i class="bi bi-journal-text me-1"></i>Query log
</a>
</div>
</div>
{{/* Service status strip */}}
<div class="card mb-4">
<div class="card-body py-3">
<div class="row g-3 align-items-center">
<div class="col-12 col-md-4 d-flex align-items-center gap-2">
<span class="status-dot {{if $d.DNSRunning}}status-up{{else}}status-down{{end}}"></span>
<div>
<div class="fw-semibold">{{if $d.DNSRunning}}DNS listeners running{{else}}DNS listeners down{{end}}</div>
<div class="small text-body-secondary mono">
UDP {{.Data.UDPAddr}} · TCP {{.Data.TCPAddr}}
</div>
</div>
</div>
<div class="col-6 col-md-3 d-flex align-items-center gap-2">
<span class="status-dot {{if $d.RecursionEnabled}}status-up{{else}}status-down{{end}}"></span>
<div>
<div class="fw-semibold">Recursion {{if $d.RecursionEnabled}}on{{else}}off{{end}}</div>
<div class="small text-body-secondary">
{{len $d.Upstreams}} upstream{{if ne (len $d.Upstreams) 1}}s{{end}}
</div>
</div>
</div>
<div class="col-6 col-md-3 d-flex align-items-center gap-2">
<span class="status-dot {{if $d.CacheEnabled}}status-up{{else}}status-down{{end}}"></span>
<div>
<div class="fw-semibold">Cache {{if $d.CacheEnabled}}on{{else}}off{{end}}</div>
<div class="small text-body-secondary">{{num $d.CacheEntries}} entries</div>
</div>
</div>
<div class="col-12 col-md-2 text-md-end">
<div class="small text-body-secondary">Queries per second</div>
<div class="fs-5 fw-semibold">{{printf "%.1f" $d.QueriesPerSec}}</div>
</div>
</div>
</div>
</div>
{{/* Headline counters */}}
<div class="row g-3 mb-4">
{{template "stat" dict "Label" "Total queries" "Value" (num $d.TotalQueries) "Icon" "bi-arrow-left-right"
"Sub" (printf "%s avg response" (ms $d.AvgQueryMS))}}
{{template "stat" dict "Label" "Blocked" "Value" (num $d.Blocked) "Icon" "bi-shield-slash" "Tone" "danger"
"Sub" (printf "%s of all queries" (pct $d.BlockRate))}}
{{template "stat" dict "Label" "Cache hit rate" "Value" (pct $d.CacheHitRate) "Icon" "bi-lightning-charge" "Tone" "success"
"Sub" (printf "%s hits · %s misses" (num $d.CacheHits) (num $d.CacheMisses))}}
{{template "stat" dict "Label" "Authoritative" "Value" (num $d.Authoritative) "Icon" "bi-diagram-3" "Tone" "primary"
"Sub" (printf "%s zones · %s records" (num $d.Zones) (num $d.Records))}}
{{template "stat" dict "Label" "Recursive" "Value" (num $d.Recursive) "Icon" "bi-globe2"
"Sub" (printf "%s upstream latency" (ms $d.AvgResolverMS))}}
{{template "stat" dict "Label" "Cache entries" "Value" (num $d.CacheEntries) "Icon" "bi-database"
"Sub" (bytes $d.CacheBytes)}}
{{template "stat" dict "Label" "Blacklist domains" "Value" (num $d.BlacklistDomains) "Icon" "bi-list-ul"
"Sub" (printf "across %s blacklists" (num $d.Blacklists))}}
{{template "stat" dict "Label" "Refused" "Value" (num $d.Refused) "Icon" "bi-hand-index" "Tone" "warning"
"Sub" (printf "%s rate limited · %s errors" (num $d.RateLimited) (num $d.Errors))}}
</div>
{{/* Activity + type breakdown */}}
<div class="row g-3 mb-4">
<div class="col-12 col-xl-8">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Query activity — last 24 hours</span>
{{if not $d.QueryLogEnabled}}
<span class="badge text-bg-secondary">query logging off</span>
{{end}}
</div>
<div class="card-body">
{{if and $d.QueryLogEnabled $d.Activity}}
<div class="chart-wrap"><canvas id="activityChart" aria-label="Query activity chart" role="img"></canvas></div>
{{else}}
{{template "empty" dict "Icon" "bi-graph-up" "Title" "No activity to chart yet"
"Message" "Query logging must be enabled for the activity chart. Once queries arrive they appear here."}}
{{end}}
</div>
</div>
</div>
<div class="col-12 col-xl-4">
<div class="card h-100">
<div class="card-header">Queries by type</div>
<div class="card-body">
{{if $d.QueriesByType}}
<div class="chart-wrap-sm"><canvas id="typeChart" aria-label="Queries by record type" role="img"></canvas></div>
{{else}}
{{template "empty" dict "Icon" "bi-pie-chart" "Title" "No queries yet"
"Message" "Point a client at this server and the breakdown appears here."}}
{{end}}
</div>
</div>
</div>
</div>
{{/* Top lists */}}
<div class="row g-3 mb-4">
<div class="col-12 col-lg-4">
<div class="card h-100">
<div class="card-header">Top queried domains</div>
{{if $d.TopDomains}}
<div class="table-responsive">
<table class="table table-sm table-compact table-hover">
<tbody>
{{range $d.TopDomains}}
<tr>
<td class="mono truncate-cell" title="{{.Name}}">{{trimDot .Name}}</td>
<td class="text-end fw-semibold">{{num .Count}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="card-body">
{{template "empty" dict "Icon" "bi-bar-chart" "Title" "Nothing yet" "Message" "Queries from the last 24 hours appear here."}}
</div>
{{end}}
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card h-100">
<div class="card-header">Top blocked domains</div>
{{if $d.TopBlocked}}
<div class="table-responsive">
<table class="table table-sm table-compact table-hover">
<tbody>
{{range $d.TopBlocked}}
<tr>
<td class="mono truncate-cell" title="{{.Name}}">{{trimDot .Name}}</td>
<td class="text-end fw-semibold text-danger">{{num .Count}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="card-body">
{{template "empty" dict "Icon" "bi-shield-check" "Title" "Nothing blocked"
"Message" "Assign a blacklist to a client network to start filtering."}}
</div>
{{end}}
</div>
</div>
<div class="col-12 col-lg-4">
<div class="card h-100">
<div class="card-header">Top clients</div>
{{if $d.TopClients}}
<div class="table-responsive">
<table class="table table-sm table-compact table-hover">
<tbody>
{{range $d.TopClients}}
<tr>
<td>
<div class="mono">{{.Name}}</div>
{{if .Extra}}<div class="small text-body-secondary">{{.Extra}}</div>{{end}}
</td>
<td class="text-end fw-semibold align-middle">{{num .Count}}</td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="card-body">
{{template "empty" dict "Icon" "bi-people" "Title" "No clients yet" "Message" "Clients that query this server appear here."}}
</div>
{{end}}
</div>
</div>
</div>
{{/* Upstreams and recent activity */}}
<div class="row g-3">
<div class="col-12 col-xl-5">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Upstream resolvers</span>
<a href="/settings/resolver" class="btn btn-sm btn-outline-secondary">Configure</a>
</div>
<div class="table-responsive">
<table class="table table-sm table-compact">
<thead>
<tr><th>Server</th><th>Status</th><th class="text-end">Latency</th><th class="text-end">Queries</th></tr>
</thead>
<tbody>
{{range $d.Upstreams}}
<tr>
<td class="mono">{{.Address}}</td>
<td>
{{if .Healthy}}<span class="badge text-bg-success">healthy</span>
{{else}}<span class="badge text-bg-danger" title="{{.LastError}}">resting</span>{{end}}
</td>
<td class="text-end">{{ms .LatencyMS}}</td>
<td class="text-end">{{num .Queries}}</td>
</tr>
{{else}}
<tr><td colspan="4" class="text-center text-body-secondary py-3">No upstream resolvers configured.</td></tr>
{{end}}
</tbody>
</table>
</div>
</div>
</div>
<div class="col-12 col-xl-7">
<div class="card h-100">
<div class="card-header d-flex justify-content-between align-items-center">
<span>Recent DNS activity</span>
<a href="/querylog" class="btn btn-sm btn-outline-secondary">View all</a>
</div>
{{if $d.Recent}}
<div class="table-responsive">
<table class="table table-sm table-compact table-hover">
<thead>
<tr><th>Time</th><th>Client</th><th>Query</th><th>Type</th><th>Result</th><th>Source</th></tr>
</thead>
<tbody>
{{range $d.Recent}}
<tr>
<td class="text-body-secondary small text-nowrap">{{timeOnly .Timestamp}}</td>
<td class="mono small">{{.ClientIP}}</td>
<td class="mono truncate-cell" title="{{.QName}}">{{trimDot .QName}}</td>
<td><span class="type-chip {{typeBadge .QType}}">{{.QType}}</span></td>
<td><span class="badge {{rcodeBadge .Rcode}}">{{.Rcode}}</span></td>
<td><span class="badge {{sourceBadge .Source}}">{{.Source}}</span></td>
</tr>
{{end}}
</tbody>
</table>
</div>
{{else}}
<div class="card-body">
{{template "empty" dict "Icon" "bi-clock-history" "Title" "No recent activity"
"Message" "Recent queries appear here once query logging is enabled and clients start resolving."}}
</div>
{{end}}
</div>
</div>
</div>
{{/* Chart data travels in data- attributes rather than an inline script, so the
page needs no CSP exception. app.js reads and parses it. */}}
<div id="chartData" hidden
data-activity="{{json $d.Activity}}"
data-types="{{json $d.QueriesByType}}"></div>
{{end}}
{{define "scripts"}}
<script src="/static/js/chart.umd.js"></script>
{{end}}