fix: polish Pi-hole connection states
This commit is contained in:
1 parent
1c32ca0671
commit
93fdda19b9
8 files changed
+57
-16
No files matched your search
@@ -25,7 +25,7 @@ Beim ersten Start werden Datenbank und Verschlüsselungsschlüssel automatisch i
|
||||
```yaml
|
||||
services:
|
||||
dyndns:
|
||||
image: git.j-mahler.de/admin/domain-control-webinterface-prod:v1.2.0
|
||||
image: git.j-mahler.de/admin/domain-control-webinterface-prod:v1.2.1
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9090:8080"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
services:
|
||||
dyndns:
|
||||
image: git.j-mahler.de/admin/domain-control-webinterface-prod:v1.2.0
|
||||
image: git.j-mahler.de/admin/domain-control-webinterface-prod:v1.2.1
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9090:8080"
|
||||
|
||||
@@ -251,7 +251,7 @@ func TestDomainDetailPlacesStatusAfterTimeAndRendersTTLAndCNAMEControls(t *testi
|
||||
if timeAt < 0 || statusAt < timeAt {
|
||||
t.Fatalf("target time must be rendered before the status pill: %s", body)
|
||||
}
|
||||
for _, expected := range []string{`name="ttl"`, `value="300"`, `action="/cnames"`, `action="/domains/1/ttl"`, `action="/domains/1/delete"`, `action="/cnames/3/remove"`, `action="/cnames/3/delete"`, `data-static-ip-fields`, `data-static-ipv4`, `data-target-requirement`, `data-ipv6-enabled="true"`, `TTL für alle setzen`, `DynDNS aktivieren`, `Ziel vollständig löschen`, `Domain vollständig löschen`, `CNAME aktiv`, `Aus WebUI entfernen`, `cname-edit-grid`, `CNAME-Aliase`, `home.example.de.`} {
|
||||
for _, expected := range []string{`name="ttl"`, `value="300"`, `action="/cnames"`, `action="/domains/1/ttl"`, `action="/domains/1/delete"`, `action="/cnames/3/remove"`, `action="/cnames/3/delete"`, `data-static-ip-fields`, `data-static-ipv4`, `data-target-requirement`, `data-ipv6-enabled="true"`, `data-scroll-page-end`, `TTL für alle setzen`, `DynDNS aktivieren`, `Ziel vollständig löschen`, `Domain vollständig löschen`, `CNAME aktiv`, `Aus WebUI entfernen`, `cname-edit-grid`, `CNAME-Aliase`, `home.example.de.`} {
|
||||
if !strings.Contains(body, expected) {
|
||||
t.Errorf("domain detail is missing %q", expected)
|
||||
}
|
||||
@@ -317,7 +317,7 @@ func TestPiHolePageRendersConnectionAndLocalDNSControls(t *testing.T) {
|
||||
t.Fatalf("render Pi-hole page: %v", err)
|
||||
}
|
||||
body := rendered.String()
|
||||
for _, expected := range []string{`href="/pihole"`, `aria-current="page"`, `action="/pihole/7/records"`, `action="/pihole/7/records/update"`, `action="/pihole/7/records/archive"`, `action="/pihole/7/records/restore"`, `action="/pihole/7/records/delete"`, `action="/pihole/7/remove"`, `action="/pihole/7/delete"`, `action="/pihole/7/save"`, `action="/pihole/7/password"`, `name="active"`, `name="link_id" value="11"`, `name="insecure_tls"`, `data-dirty-form`, `data-dirty-submit disabled aria-disabled="true"`, `Anwendungspasswort anzeigen`, `Passwort entschlüsseln`, `Nur aus der WebUI entfernen`, `Anwendungspasswort`, `nas.lan`, `old.lan`, `192.168.1.20`} {
|
||||
for _, expected := range []string{`href="/pihole"`, `aria-current="page"`, `action="/pihole/7/records"`, `action="/pihole/7/records/update"`, `action="/pihole/7/records/archive"`, `action="/pihole/7/records/restore"`, `action="/pihole/7/records/delete"`, `action="/pihole/7/remove"`, `action="/pihole/7/delete"`, `action="/pihole/7/save"`, `action="/pihole/7/password"`, `name="active"`, `name="link_id" value="11"`, `name="insecure_tls"`, `data-dirty-form`, `data-dirty-submit disabled aria-disabled="true"`, `data-scroll-page-end`, `Anwendungspasswort anzeigen`, `Passwort entschlüsseln`, `Nur aus der WebUI entfernen`, `Anwendungspasswort`, `nas.lan`, `old.lan`, `192.168.1.20`} {
|
||||
if !strings.Contains(body, expected) {
|
||||
t.Errorf("Pi-hole page is missing %q", expected)
|
||||
}
|
||||
@@ -341,6 +341,36 @@ func TestPiHolePageRendersConnectionAndLocalDNSControls(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiHoleDisconnectedPageCentersConnectFormWithoutEmptyState(t *testing.T) {
|
||||
t.Parallel()
|
||||
templates, err := loadTemplates()
|
||||
if err != nil {
|
||||
t.Fatalf("load templates: %v", err)
|
||||
}
|
||||
var rendered bytes.Buffer
|
||||
data := viewData{
|
||||
Title: "Pi-hole",
|
||||
Page: "pihole",
|
||||
CSRF: "csrf",
|
||||
IsAdmin: true,
|
||||
User: model.User{Username: "admin", DisplayName: "Administrator", Role: "admin"},
|
||||
}
|
||||
if err := templates["pihole"].ExecuteTemplate(&rendered, "pihole", data); err != nil {
|
||||
t.Fatalf("render disconnected Pi-hole page: %v", err)
|
||||
}
|
||||
body := rendered.String()
|
||||
for _, expected := range []string{`class="pihole-connect-stage"`, `class="form-panel pihole-connect-panel"`, `action="/pihole/connect"`} {
|
||||
if !strings.Contains(body, expected) {
|
||||
t.Errorf("disconnected Pi-hole page is missing %q", expected)
|
||||
}
|
||||
}
|
||||
for _, removed := range []string{"Noch kein Pi-hole verbunden", "Verbinde genau einen Pi-hole", `href="#pihole-connect"`, `class="empty-board`} {
|
||||
if strings.Contains(body, removed) {
|
||||
t.Errorf("disconnected Pi-hole page still renders removed empty-state content %q", removed)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestPiHoleConnectValidatesRemoteAndEncryptsApplicationPassword(t *testing.T) {
|
||||
t.Parallel()
|
||||
api := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -198,6 +198,18 @@
|
||||
syncDirtyState();
|
||||
}
|
||||
|
||||
for (const details of document.querySelectorAll('details[data-scroll-page-end]')) {
|
||||
details.addEventListener('toggle', () => {
|
||||
if (!details.open) return;
|
||||
window.requestAnimationFrame(() => {
|
||||
window.scrollTo({
|
||||
top: document.documentElement.scrollHeight,
|
||||
behavior: window.matchMedia('(prefers-reduced-motion: reduce)').matches ? 'auto' : 'smooth',
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const copyButtons = document.querySelectorAll('[data-copy-target]');
|
||||
for (const button of copyButtons) {
|
||||
button.addEventListener('click', async () => {
|
||||
|
||||
@@ -256,10 +256,9 @@ h3 { margin-bottom: 2px; font-size: 14px; line-height: 1.35; }
|
||||
.pihole-connection-error { margin-bottom: 18px; padding: 12px; border: 1px solid #efc7ce; border-left: 3px solid #a4121a; border-radius: var(--radius-sm); background: #fff8f9; }
|
||||
.pihole-connection-error strong { color: var(--danger); font-size: 11px; }
|
||||
.pihole-connection-error p { margin: 3px 0 0; color: #865764; font-size: 10px; overflow-wrap: anywhere; }
|
||||
.pihole-connect-panel { max-width: 720px; margin-top: 20px; border-top: 3px solid #a4121a; scroll-margin-top: 18px; }
|
||||
.pihole-connect-stage { display: grid; min-height: clamp(390px, calc(100vh - 245px), 640px); place-items: center; }
|
||||
.pihole-connect-panel { width: min(560px, 100%); border-top: 3px solid #a4121a; }
|
||||
.pihole-api-badge { padding: 4px 7px; border-radius: 4px; background: #fff0f1; color: #98131b; font-family: var(--mono); font-size: 9px; font-weight: 800; }
|
||||
.pihole-empty { margin-bottom: 18px; }
|
||||
.pihole-empty-icon { border-color: #e4b7ba; background: #fff2f3; color: #9d1018; }
|
||||
|
||||
.stacked-form { display: grid; gap: 13px; }
|
||||
.wide-form { width: 100%; max-width: 850px; }
|
||||
@@ -420,6 +419,7 @@ input:focus, textarea:focus, select:focus { border-color: var(--accent); box-sha
|
||||
.error-item { grid-template-columns: 26px minmax(0,1fr); }
|
||||
.error-item > a { grid-column: 2; }
|
||||
.pihole-action-grid { grid-template-columns: 1fr; }
|
||||
.pihole-connect-stage { min-height: 0; }
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
|
||||
@@ -116,7 +116,7 @@
|
||||
<p class="panel-description">Der API-Token bleibt verschlüsselt. Eine Anzeige erfordert erneut das Admin-Passwort.</p>
|
||||
<details><summary class="button button-quiet">API-Token anzeigen</summary><form action="/domains/{{.Zone.ID}}/token" method="post" class="stacked-form inset-form"><input type="hidden" name="csrf" value="{{.CSRF}}"><label>Admin-Passwort <input type="password" name="password" autocomplete="current-password" required></label><button class="button button-danger-soft" type="submit">Token entschlüsseln</button></form></details>
|
||||
<div class="panel-divider"></div>
|
||||
<details><summary class="text-danger">Domain-Verbindung entfernen</summary><div class="danger-actions inset-form"><form action="/domains/{{.Zone.ID}}/remove" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{.CSRF}}"><strong>Nur aus der WebUI entfernen</strong><p>Löscht ausschließlich die lokale Konfiguration. Alle DNS-Records bei Hetzner bleiben bestehen.</p><button class="button button-danger-soft" type="submit">Lokale Verbindung entfernen</button></form><form action="/domains/{{.Zone.ID}}/delete" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{.CSRF}}"><strong>Domain vollständig löschen</strong><p>Löscht alle hier verwalteten A-, AAAA- und CNAME-Einträge aus der WebUI sowie die zugehörigen Records bei Hetzner. Andere Record-Typen bleiben bestehen.</p><button class="button button-danger-soft" type="submit">Domain und Records löschen</button></form></div></details>
|
||||
<details data-scroll-page-end><summary class="text-danger">Domain-Verbindung entfernen</summary><div class="danger-actions inset-form"><form action="/domains/{{.Zone.ID}}/remove" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{.CSRF}}"><strong>Nur aus der WebUI entfernen</strong><p>Löscht ausschließlich die lokale Konfiguration. Alle DNS-Records bei Hetzner bleiben bestehen.</p><button class="button button-danger-soft" type="submit">Lokale Verbindung entfernen</button></form><form action="/domains/{{.Zone.ID}}/delete" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{.CSRF}}"><strong>Domain vollständig löschen</strong><p>Löscht alle hier verwalteten A-, AAAA- und CNAME-Einträge aus der WebUI sowie die zugehörigen Records bei Hetzner. Andere Record-Typen bleiben bestehen.</p><button class="button button-danger-soft" type="submit">Domain und Records löschen</button></form></div></details>
|
||||
</article>
|
||||
{{end}}
|
||||
</section>
|
||||
|
||||
@@ -6,8 +6,8 @@
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta name="color-scheme" content="light">
|
||||
<title>{{.Title}} · DNS Control</title>
|
||||
<link rel="stylesheet" href="/static/styles.css?v=features-12">
|
||||
<script src="/static/app.js?v=features-12" defer></script>
|
||||
<link rel="stylesheet" href="/static/styles.css?v=features-14">
|
||||
<script src="/static/app.js?v=features-14" defer></script>
|
||||
</head>
|
||||
<body class="page-{{.Page}}">
|
||||
<a class="skip-link" href="#main">Zum Inhalt springen</a>
|
||||
|
||||
@@ -4,8 +4,6 @@
|
||||
<div><span class="page-kicker pihole-kicker">Lokales Netzwerk</span><h1>Pi-hole DNS</h1><p>Lokale A- und AAAA-Auflösungen ähnlich wie deine öffentlichen Domains verwalten.</p></div>
|
||||
{{if .PiHoleView}}
|
||||
<form action="/pihole/{{.PiHoleView.Integration.ID}}/refresh" method="post"><input type="hidden" name="csrf" value="{{.CSRF}}"><button class="button button-quiet" type="submit">Records neu einlesen</button></form>
|
||||
{{else}}
|
||||
<a class="button button-pihole" href="#pihole-connect">Pi-hole verbinden</a>
|
||||
{{end}}
|
||||
</header>
|
||||
|
||||
@@ -75,16 +73,17 @@
|
||||
<div class="panel-divider"></div>
|
||||
<details><summary class="button button-quiet">Anwendungspasswort anzeigen</summary><form action="/pihole/{{$integration.ID}}/password" method="post" class="stacked-form inset-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><label>Admin-Passwort <input type="password" name="password" autocomplete="current-password" required></label><button class="button button-danger-soft" type="submit">Passwort entschlüsseln</button></form></details>
|
||||
<div class="panel-divider"></div>
|
||||
<details><summary class="text-danger">Pi-hole-Verbindung entfernen</summary><div class="danger-actions inset-form"><form action="/pihole/{{$integration.ID}}/remove" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><strong>Nur aus der WebUI entfernen</strong><p>Löscht ausschließlich Zugang und lokale Verknüpfungen. Alle DNS-Einträge bei Pi-hole bleiben bestehen.</p><button class="button button-danger-soft" type="submit">Lokale Verbindung entfernen</button></form><form action="/pihole/{{$integration.ID}}/delete" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><strong>Pi-hole vollständig leeren</strong><p>Löscht alle lokalen DNS-Einträge bei Pi-hole und entfernt anschließend die Verbindung aus der WebUI.</p><button class="button button-danger-soft" type="submit">Verbindung und Einträge löschen</button></form></div></details>
|
||||
<details data-scroll-page-end><summary class="text-danger">Pi-hole-Verbindung entfernen</summary><div class="danger-actions inset-form"><form action="/pihole/{{$integration.ID}}/remove" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><strong>Nur aus der WebUI entfernen</strong><p>Löscht ausschließlich Zugang und lokale Verknüpfungen. Alle DNS-Einträge bei Pi-hole bleiben bestehen.</p><button class="button button-danger-soft" type="submit">Lokale Verbindung entfernen</button></form><form action="/pihole/{{$integration.ID}}/delete" method="post" class="unlink-form"><input type="hidden" name="csrf" value="{{$.CSRF}}"><strong>Pi-hole vollständig leeren</strong><p>Löscht alle lokalen DNS-Einträge bei Pi-hole und entfernt anschließend die Verbindung aus der WebUI.</p><button class="button button-danger-soft" type="submit">Verbindung und Einträge löschen</button></form></div></details>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
{{else}}
|
||||
<section class="empty-board pihole-empty"><span class="empty-icon pihole-empty-icon">PH</span><h2>Noch kein Pi-hole verbunden</h2><p>Verbinde genau einen Pi-hole über die v6-API. Danach lassen sich lokale DNS-Einträge wie Records im Domain-Tab verwalten.</p><a class="button button-pihole" href="#pihole-connect">Pi-hole verbinden</a></section>
|
||||
<section class="form-panel pihole-connect-panel" id="pihole-connect">
|
||||
<div class="panel-heading"><div><span class="section-label pihole-kicker">API-Verbindung</span><h2>Pi-hole verbinden</h2></div><span class="pihole-api-badge">API v6</span></div>
|
||||
<section class="pihole-connect-stage" aria-labelledby="pihole-connect-title">
|
||||
<div class="form-panel pihole-connect-panel" id="pihole-connect">
|
||||
<div class="panel-heading"><div><span class="section-label pihole-kicker">API-Verbindung</span><h2 id="pihole-connect-title">Pi-hole verbinden</h2></div><span class="pihole-api-badge">API v6</span></div>
|
||||
<p class="panel-description">Verwende ein eigenes Pi-hole-Anwendungspasswort. Es wird mit dem Master-Key dieser Anwendung verschlüsselt.</p>
|
||||
<form action="/pihole/connect" method="post" class="stacked-form"><input type="hidden" name="csrf" value="{{.CSRF}}"><label>Pi-hole-Adresse <input name="base_url" type="url" placeholder="https://pi.hole" required><small><code>/api</code> wird automatisch ergänzt.</small></label><label>Anwendungspasswort <input name="password" type="password" autocomplete="new-password" required><small>Keinen Session-Token eintragen.</small></label><label class="switch pihole-switch"><input type="checkbox" name="insecure_tls"><span></span> TLS-Zertifikat nicht prüfen</label><div class="warning-box">Nur für ein selbstsigniertes Zertifikat in einem vertrauenswürdigen internen Netz verwenden. Bei HTTP wird das Passwort auf dem Netzwerkweg unverschlüsselt übertragen.</div><button class="button button-pihole" type="submit">Verbindung testen und speichern</button></form>
|
||||
</div>
|
||||
</section>
|
||||
{{end}}
|
||||
{{end}}
|
||||
Reference in new issue
Block a user