Files
dnswatcher/internal/handlers/templates/dashboard.html
clawbot c15ca77bd7
All checks were successful
check / check (push) Successful in 45s
vendor Tailwind CSS, embed all static assets in binary
Remove CDN dependency (cdn.tailwindcss.com) and replace with a
pre-built, minified Tailwind CSS file embedded in the Go binary
via go:embed.

Changes:
- Add static/static.go with go:embed for css/ directory
- Add static/css/tailwind.min.css (9KB, contains only classes
  used by the dashboard template)
- Remove <script src="https://cdn.tailwindcss.com"> and inline
  tailwind.config from dashboard.html
- Replace with <link rel="stylesheet" href="/s/css/tailwind.min.css">
- Mount /s/ route for embedded static file serving (go-chi)
- Add /.well-known/healthcheck endpoint per GO_HTTP_SERVER conventions

Zero external HTTP requests from the browser. All assets served
from the binary itself.

Closes #82
2026-03-04 03:21:31 -08:00

371 lines
14 KiB
HTML

<!doctype html>
<html lang="en" class="bg-slate-950">
<head>
<meta charset="utf-8" />
<meta http-equiv="refresh" content="30" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>dnswatcher</title>
<link rel="stylesheet" href="/s/css/tailwind.min.css" />
</head>
<body
class="bg-surface-950 text-slate-300 font-mono text-sm min-h-screen antialiased"
>
<div class="max-w-6xl mx-auto px-4 py-8">
{{/* ---- Header ---- */}}
<div class="mb-8">
<h1 class="text-2xl font-bold text-teal-400 tracking-tight">
dnswatcher
</h1>
<p class="text-xs text-slate-500 mt-1">
state updated {{ .StateAge }} &middot; page generated
{{ .GeneratedAt }} UTC &middot; auto-refresh 30s
</p>
</div>
{{/* ---- Summary bar ---- */}}
<div
class="grid grid-cols-2 sm:grid-cols-4 gap-3 mb-8"
>
<div class="bg-surface-800 border border-slate-700/50 rounded-lg p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">
Domains
</div>
<div class="text-2xl font-bold text-teal-400 mt-1">
{{ len .Snapshot.Domains }}
</div>
</div>
<div class="bg-surface-800 border border-slate-700/50 rounded-lg p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">
Hostnames
</div>
<div class="text-2xl font-bold text-teal-400 mt-1">
{{ len .Snapshot.Hostnames }}
</div>
</div>
<div class="bg-surface-800 border border-slate-700/50 rounded-lg p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">
Ports
</div>
<div class="text-2xl font-bold text-teal-400 mt-1">
{{ len .Snapshot.Ports }}
</div>
</div>
<div class="bg-surface-800 border border-slate-700/50 rounded-lg p-4">
<div class="text-xs text-slate-500 uppercase tracking-wider">
Certificates
</div>
<div class="text-2xl font-bold text-teal-400 mt-1">
{{ len .Snapshot.Certificates }}
</div>
</div>
</div>
{{/* ---- Domains ---- */}}
<section class="mb-8">
<h2
class="text-sm font-semibold text-teal-300 uppercase tracking-wider mb-3 border-b border-slate-700/50 pb-2"
>
Domains
</h2>
{{ if .Snapshot.Domains }}
<div class="overflow-x-auto">
<table class="w-full text-left text-xs">
<thead>
<tr class="text-slate-500 uppercase tracking-wider">
<th class="py-2 px-3">Domain</th>
<th class="py-2 px-3">Nameservers</th>
<th class="py-2 px-3">Checked</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
{{ range $name, $ds := .Snapshot.Domains }}
<tr class="hover:bg-surface-800/50">
<td class="py-2 px-3 text-slate-200 font-medium">
{{ $name }}
</td>
<td class="py-2 px-3 text-slate-400 break-all">
{{ joinStrings $ds.Nameservers ", " }}
</td>
<td class="py-2 px-3 text-slate-500 whitespace-nowrap">
{{ relTime $ds.LastChecked }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ else }}
<p class="text-slate-600 italic text-xs">
No domains configured.
</p>
{{ end }}
</section>
{{/* ---- Hostnames ---- */}}
<section class="mb-8">
<h2
class="text-sm font-semibold text-teal-300 uppercase tracking-wider mb-3 border-b border-slate-700/50 pb-2"
>
Hostnames
</h2>
{{ if .Snapshot.Hostnames }}
<div class="overflow-x-auto">
<table class="w-full text-left text-xs">
<thead>
<tr class="text-slate-500 uppercase tracking-wider">
<th class="py-2 px-3">Hostname</th>
<th class="py-2 px-3">NS</th>
<th class="py-2 px-3">Status</th>
<th class="py-2 px-3">Records</th>
<th class="py-2 px-3">Checked</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
{{ range $name, $hs := .Snapshot.Hostnames }}
{{ range $ns, $nsr := $hs.RecordsByNameserver }}
<tr class="hover:bg-surface-800/50">
<td class="py-2 px-3 text-slate-200 font-medium">
{{ $name }}
</td>
<td class="py-2 px-3 text-slate-400 break-all">
{{ $ns }}
</td>
<td class="py-2 px-3">
{{ if eq $nsr.Status "ok" }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-teal-900/50 text-teal-400 border border-teal-700/30"
>ok</span
>
{{ else }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-red-900/50 text-red-400 border border-red-700/30"
>{{ $nsr.Status }}</span
>
{{ end }}
</td>
<td
class="py-2 px-3 text-slate-400 break-all max-w-xs"
>
{{ formatRecords $nsr.Records }}
</td>
<td class="py-2 px-3 text-slate-500 whitespace-nowrap">
{{ relTime $nsr.LastChecked }}
</td>
</tr>
{{ end }}
{{ end }}
</tbody>
</table>
</div>
{{ else }}
<p class="text-slate-600 italic text-xs">
No hostnames configured.
</p>
{{ end }}
</section>
{{/* ---- Ports ---- */}}
<section class="mb-8">
<h2
class="text-sm font-semibold text-teal-300 uppercase tracking-wider mb-3 border-b border-slate-700/50 pb-2"
>
Ports
</h2>
{{ if .Snapshot.Ports }}
<div class="overflow-x-auto">
<table class="w-full text-left text-xs">
<thead>
<tr class="text-slate-500 uppercase tracking-wider">
<th class="py-2 px-3">Address</th>
<th class="py-2 px-3">State</th>
<th class="py-2 px-3">Hostnames</th>
<th class="py-2 px-3">Checked</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
{{ range $key, $ps := .Snapshot.Ports }}
<tr class="hover:bg-surface-800/50">
<td class="py-2 px-3 text-slate-200 font-medium">
{{ $key }}
</td>
<td class="py-2 px-3">
{{ if $ps.Open }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-teal-900/50 text-teal-400 border border-teal-700/30"
>open</span
>
{{ else }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-red-900/50 text-red-400 border border-red-700/30"
>closed</span
>
{{ end }}
</td>
<td class="py-2 px-3 text-slate-400 break-all">
{{ joinStrings $ps.Hostnames ", " }}
</td>
<td class="py-2 px-3 text-slate-500 whitespace-nowrap">
{{ relTime $ps.LastChecked }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ else }}
<p class="text-slate-600 italic text-xs">
No port data yet.
</p>
{{ end }}
</section>
{{/* ---- Certificates ---- */}}
<section class="mb-8">
<h2
class="text-sm font-semibold text-teal-300 uppercase tracking-wider mb-3 border-b border-slate-700/50 pb-2"
>
Certificates
</h2>
{{ if .Snapshot.Certificates }}
<div class="overflow-x-auto">
<table class="w-full text-left text-xs">
<thead>
<tr class="text-slate-500 uppercase tracking-wider">
<th class="py-2 px-3">Endpoint</th>
<th class="py-2 px-3">Status</th>
<th class="py-2 px-3">CN</th>
<th class="py-2 px-3">Issuer</th>
<th class="py-2 px-3">Expires</th>
<th class="py-2 px-3">Checked</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-800">
{{ range $key, $cs := .Snapshot.Certificates }}
<tr class="hover:bg-surface-800/50">
<td class="py-2 px-3 text-slate-400 break-all">
{{ $key }}
</td>
<td class="py-2 px-3">
{{ if eq $cs.Status "ok" }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-teal-900/50 text-teal-400 border border-teal-700/30"
>ok</span
>
{{ else }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-red-900/50 text-red-400 border border-red-700/30"
>{{ $cs.Status }}</span
>
{{ end }}
</td>
<td class="py-2 px-3 text-slate-200">
{{ $cs.CommonName }}
</td>
<td class="py-2 px-3 text-slate-400 break-all">
{{ $cs.Issuer }}
</td>
<td class="py-2 px-3 whitespace-nowrap">
{{ if not $cs.NotAfter.IsZero }}
{{ $days := expiryDays $cs.NotAfter }}
{{ if lt $days 7 }}
<span class="text-red-400 font-medium"
>{{ $cs.NotAfter.Format "2006-01-02" }}
({{ $days }}d)</span
>
{{ else if lt $days 30 }}
<span class="text-amber-400"
>{{ $cs.NotAfter.Format "2006-01-02" }}
({{ $days }}d)</span
>
{{ else }}
<span class="text-slate-400"
>{{ $cs.NotAfter.Format "2006-01-02" }}
({{ $days }}d)</span
>
{{ end }}
{{ end }}
</td>
<td class="py-2 px-3 text-slate-500 whitespace-nowrap">
{{ relTime $cs.LastChecked }}
</td>
</tr>
{{ end }}
</tbody>
</table>
</div>
{{ else }}
<p class="text-slate-600 italic text-xs">
No certificate data yet.
</p>
{{ end }}
</section>
{{/* ---- Recent Alerts ---- */}}
<section class="mb-8">
<h2
class="text-sm font-semibold text-teal-300 uppercase tracking-wider mb-3 border-b border-slate-700/50 pb-2"
>
Recent Alerts ({{ len .Alerts }})
</h2>
{{ if .Alerts }}
<div class="space-y-2">
{{ range .Alerts }}
<div
class="bg-surface-800 border rounded-lg px-4 py-3 {{ if eq .Priority "error" }}border-red-700/40{{ else if eq .Priority "warning" }}border-amber-700/40{{ else if eq .Priority "success" }}border-teal-700/40{{ else }}border-blue-700/40{{ end }}"
>
<div class="flex items-center gap-3 mb-1">
{{ if eq .Priority "error" }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-red-900/50 text-red-400 border border-red-700/30"
>error</span
>
{{ else if eq .Priority "warning" }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-amber-900/50 text-amber-400 border border-amber-700/30"
>warning</span
>
{{ else if eq .Priority "success" }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-teal-900/50 text-teal-400 border border-teal-700/30"
>success</span
>
{{ else }}
<span
class="inline-block px-1.5 py-0.5 rounded text-[10px] font-bold uppercase bg-blue-900/50 text-blue-400 border border-blue-700/30"
>info</span
>
{{ end }}
<span class="text-slate-200 text-xs font-medium">
{{ .Title }}
</span>
<span class="text-slate-600 text-[11px] ml-auto whitespace-nowrap">
{{ .Timestamp.Format "2006-01-02 15:04:05" }} UTC
({{ relTime .Timestamp }})
</span>
</div>
<p
class="text-slate-400 text-xs whitespace-pre-line pl-0.5"
>
{{ .Message }}
</p>
</div>
{{ end }}
</div>
{{ else }}
<p class="text-slate-600 italic text-xs">
No alerts recorded since last restart.
</p>
{{ end }}
</section>
{{/* ---- Footer ---- */}}
<div
class="text-[11px] text-slate-700 border-t border-slate-800 pt-4 mt-8"
>
dnswatcher &middot; monitoring {{ len .Snapshot.Domains }} domains +
{{ len .Snapshot.Hostnames }} hostnames
</div>
</div>
</body>
</html>