Serves a page explaining why a domain was blocked instead of leaving a sinkholed client with a dead connection. Binds its own HTTP/HTTPS listeners with self-signed, per-hostname TLS certs generated on the fly, re-evaluates the requesting client against the policy engine per request, and renders an HTML template editable from Settings with a live preview. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01TTKpGMQzpfDsvedu1hvSUf
186 lines
8.2 KiB
HTML
186 lines
8.2 KiB
HTML
{{/* Shared building blocks used across pages. */}}
|
||
|
||
{{define "sidebar"}}
|
||
<nav class="sidebar-nav" aria-label="Main navigation">
|
||
<a class="sidebar-link {{if eq .Nav "dashboard"}}active{{end}}" href="/">
|
||
<i class="bi bi-speedometer2"></i><span>Dashboard</span>
|
||
</a>
|
||
|
||
<div class="sidebar-heading">DNS</div>
|
||
<a class="sidebar-link {{if eq .Nav "zones"}}active{{end}}" href="/zones">
|
||
<i class="bi bi-diagram-3"></i><span>Forward Zones</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "zones-reverse"}}active{{end}}" href="/zones/reverse">
|
||
<i class="bi bi-arrow-left-right"></i><span>Reverse Zones</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "records"}}active{{end}}" href="/records">
|
||
<i class="bi bi-list-columns-reverse"></i><span>Records</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "resolver"}}active{{end}}" href="/resolver">
|
||
<i class="bi bi-globe2"></i><span>Resolver</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "cache"}}active{{end}}" href="/cache">
|
||
<i class="bi bi-lightning-charge"></i><span>Cache</span>
|
||
</a>
|
||
|
||
<div class="sidebar-heading">Policies</div>
|
||
<a class="sidebar-link {{if eq .Nav "networks"}}active{{end}}" href="/policies/networks">
|
||
<i class="bi bi-router"></i><span>Client Networks</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "blacklists"}}active{{end}}" href="/policies/blacklists">
|
||
<i class="bi bi-shield-slash"></i><span>Blacklists</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "allowlists"}}active{{end}}" href="/policies/allowlists">
|
||
<i class="bi bi-shield-check"></i><span>Allowlists</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "policies"}}active{{end}}" href="/policies">
|
||
<i class="bi bi-sliders"></i><span>Rules</span>
|
||
</a>
|
||
|
||
<div class="sidebar-heading">Operations</div>
|
||
<a class="sidebar-link {{if eq .Nav "querylog"}}active{{end}}" href="/querylog">
|
||
<i class="bi bi-journal-text"></i><span>Query Log</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "audit"}}active{{end}}" href="/audit">
|
||
<i class="bi bi-clipboard-check"></i><span>Audit Log</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "tools"}}active{{end}}" href="/tools">
|
||
<i class="bi bi-tools"></i><span>Tools</span>
|
||
</a>
|
||
|
||
<div class="sidebar-heading">Settings</div>
|
||
<a class="sidebar-link {{if eq .Nav "settings"}}active{{end}}" href="/settings/dns">
|
||
<i class="bi bi-gear"></i><span>Settings</span>
|
||
</a>
|
||
<a class="sidebar-link {{if eq .Nav "account"}}active{{end}}" href="/account">
|
||
<i class="bi bi-person-badge"></i><span>Account</span>
|
||
</a>
|
||
</nav>
|
||
{{end}}
|
||
|
||
|
||
{{/* Page header: title, optional subtitle and a slot for actions. */}}
|
||
{{define "pageheader"}}
|
||
<div class="d-flex flex-wrap justify-content-between align-items-start gap-3 mb-4">
|
||
<div>
|
||
<h1 class="page-title">{{.Title}}</h1>
|
||
{{if .Subtitle}}<p class="text-body-secondary mb-0">{{.Subtitle}}</p>{{end}}
|
||
</div>
|
||
{{if .Actions}}<div class="d-flex flex-wrap gap-2">{{.Actions}}</div>{{end}}
|
||
</div>
|
||
{{end}}
|
||
|
||
|
||
{{/* A statistic tile. Pass: Label, Value, Icon, Sub, Tone. */}}
|
||
{{define "stat"}}
|
||
<div class="col-6 col-md-4 col-xl-3">
|
||
<div class="card stat-card h-100">
|
||
<div class="card-body">
|
||
<div class="d-flex justify-content-between align-items-start">
|
||
<div class="stat-label">{{.Label}}</div>
|
||
{{if .Icon}}<i class="bi {{.Icon}} stat-icon {{with .Tone}}text-{{.}}{{end}}"></i>{{end}}
|
||
</div>
|
||
<div class="stat-value">{{.Value}}</div>
|
||
{{if .Sub}}<div class="stat-sub">{{.Sub}}</div>{{end}}
|
||
</div>
|
||
</div>
|
||
</div>
|
||
{{end}}
|
||
|
||
|
||
{{/* Empty state. Pass: Icon, Title, Message, optional Action (safe HTML). */}}
|
||
{{define "empty"}}
|
||
<div class="empty-state text-center py-5">
|
||
<i class="bi {{.Icon}} empty-icon"></i>
|
||
<h2 class="h5 mt-3 mb-2">{{.Title}}</h2>
|
||
<p class="text-body-secondary mx-auto" style="max-width: 34rem;">{{.Message}}</p>
|
||
{{if .Action}}<div class="mt-3">{{.Action}}</div>{{end}}
|
||
</div>
|
||
{{end}}
|
||
|
||
|
||
{{/* Pagination. Pass: P (pagination struct), Q (url.Values). */}}
|
||
{{define "pagination"}}
|
||
{{$p := .P}}{{$q := .Q}}
|
||
{{if gt $p.Pages 1}}
|
||
<nav class="d-flex flex-wrap justify-content-between align-items-center gap-2 mt-3" aria-label="Pagination">
|
||
<div class="text-body-secondary small">
|
||
Showing {{num $p.From}}–{{num $p.To}} of {{num $p.Total}}
|
||
</div>
|
||
<ul class="pagination pagination-sm mb-0">
|
||
<li class="page-item {{if not $p.HasPrev}}disabled{{end}}">
|
||
<a class="page-link" href="{{withQuery $q "page" 1}}" aria-label="First page">«</a>
|
||
</li>
|
||
<li class="page-item {{if not $p.HasPrev}}disabled{{end}}">
|
||
<a class="page-link" href="{{withQuery $q "page" (sub $p.Page 1)}}" aria-label="Previous page">‹</a>
|
||
</li>
|
||
{{range $n := pages $p.Page $p.Pages}}
|
||
<li class="page-item {{if eq $n $p.Page}}active{{end}}">
|
||
<a class="page-link" href="{{withQuery $q "page" $n}}">{{$n}}</a>
|
||
</li>
|
||
{{end}}
|
||
<li class="page-item {{if not $p.HasNext}}disabled{{end}}">
|
||
<a class="page-link" href="{{withQuery $q "page" (add $p.Page 1)}}" aria-label="Next page">›</a>
|
||
</li>
|
||
<li class="page-item {{if not $p.HasNext}}disabled{{end}}">
|
||
<a class="page-link" href="{{withQuery $q "page" $p.Pages}}" aria-label="Last page">»</a>
|
||
</li>
|
||
</ul>
|
||
</nav>
|
||
{{else if gt $p.Total 0}}
|
||
<div class="text-body-secondary small mt-3">{{num $p.Total}} result{{if ne $p.Total 1}}s{{end}}</div>
|
||
{{end}}
|
||
{{end}}
|
||
|
||
|
||
{{/* A destructive submit button that opens the shared confirmation modal.
|
||
Pass: Action, CSRF, Label, Message, optional Icon, Class, ReturnTo. */}}
|
||
{{define "confirmform"}}
|
||
<form method="post" action="{{.Action}}" class="d-inline js-confirm" data-confirm="{{.Message}}">
|
||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||
{{if .ReturnTo}}<input type="hidden" name="return_to" value="{{.ReturnTo}}">{{end}}
|
||
{{range $k, $v := .Fields}}<input type="hidden" name="{{$k}}" value="{{$v}}">{{end}}
|
||
<button type="submit" class="{{if .Class}}{{.Class}}{{else}}btn btn-sm btn-outline-danger{{end}}"
|
||
{{if .Title}}title="{{.Title}}"{{end}}>
|
||
{{if .Icon}}<i class="bi {{.Icon}}"></i>{{end}}{{if .Label}} {{.Label}}{{end}}
|
||
</button>
|
||
</form>
|
||
{{end}}
|
||
|
||
|
||
{{/* A plain post button with no confirmation. Same fields as confirmform. */}}
|
||
{{define "postform"}}
|
||
<form method="post" action="{{.Action}}" class="d-inline">
|
||
<input type="hidden" name="_csrf" value="{{.CSRF}}">
|
||
{{if .ReturnTo}}<input type="hidden" name="return_to" value="{{.ReturnTo}}">{{end}}
|
||
{{range $k, $v := .Fields}}<input type="hidden" name="{{$k}}" value="{{$v}}">{{end}}
|
||
<button type="submit" class="{{if .Class}}{{.Class}}{{else}}btn btn-sm btn-outline-secondary{{end}}"
|
||
{{if .Title}}title="{{.Title}}"{{end}}>
|
||
{{if .Icon}}<i class="bi {{.Icon}}"></i>{{end}}{{if .Label}} {{.Label}}{{end}}
|
||
</button>
|
||
</form>
|
||
{{end}}
|
||
|
||
|
||
{{/* Settings sub-navigation. Pass the active key as .Subnav. */}}
|
||
{{define "settingsnav"}}
|
||
<ul class="nav nav-pills settings-nav mb-4 flex-wrap gap-1">
|
||
<li class="nav-item"><a class="nav-link {{if eq .Subnav "dns"}}active{{end}}" href="/settings/dns">DNS</a></li>
|
||
<li class="nav-item"><a class="nav-link {{if eq .Subnav "resolver"}}active{{end}}" href="/settings/resolver">Resolver</a></li>
|
||
<li class="nav-item"><a class="nav-link {{if eq .Subnav "cache"}}active{{end}}" href="/settings/cache">Cache</a></li>
|
||
<li class="nav-item"><a class="nav-link {{if eq .Subnav "http"}}active{{end}}" href="/settings/http">Web Server</a></li>
|
||
<li class="nav-item"><a class="nav-link {{if eq .Subnav "blockpage"}}active{{end}}" href="/settings/blockpage">Block Page</a></li>
|
||
<li class="nav-item"><a class="nav-link {{if eq .Subnav "logging"}}active{{end}}" href="/settings/logging">Logging</a></li>
|
||
<li class="nav-item"><a class="nav-link {{if eq .Subnav "database"}}active{{end}}" href="/settings/database">Database</a></li>
|
||
<li class="nav-item"><a class="nav-link {{if eq .Subnav "api"}}active{{end}}" href="/settings/api">API</a></li>
|
||
<li class="nav-item"><a class="nav-link {{if eq .Subnav "account"}}active{{end}}" href="/account">Authentication</a></li>
|
||
</ul>
|
||
{{end}}
|
||
|
||
|
||
{{/* Marks a field that needs a restart to take effect. */}}
|
||
{{define "restartbadge"}}
|
||
<span class="badge text-bg-warning-subtle border border-warning-subtle text-warning-emphasis ms-1"
|
||
title="Changing this requires a restart">restart required</span>
|
||
{{end}}
|