From 42085dcde0101bc7beb51641733edc5eaa44c5f9 Mon Sep 17 00:00:00 2001 From: admin Date: Mon, 13 Jul 2026 03:25:22 +0200 Subject: [PATCH] fix: polish Pi-hole connection states --- README.md | 2 +- compose.yml | 2 +- internal/web/server_test.go | 34 +++++++++++++++++++++-- internal/web/static/app.js | 12 ++++++++ internal/web/static/styles.css | 6 ++-- internal/web/templates/domain_detail.html | 2 +- internal/web/templates/layout.html | 4 +-- internal/web/templates/pihole.html | 15 +++++----- 8 files changed, 59 insertions(+), 18 deletions(-) diff --git a/README.md b/README.md index d3634e4..6da4475 100644 --- a/README.md +++ b/README.md @@ -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" diff --git a/compose.yml b/compose.yml index 3d5263e..a71020b 100644 --- a/compose.yml +++ b/compose.yml @@ -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" diff --git a/internal/web/server_test.go b/internal/web/server_test.go index 23c3a8f..0f917d9 100644 --- a/internal/web/server_test.go +++ b/internal/web/server_test.go @@ -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) { diff --git a/internal/web/static/app.js b/internal/web/static/app.js index 4111121..a16197d 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -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 () => { diff --git a/internal/web/static/styles.css b/internal/web/static/styles.css index dd7e383..b1b9c11 100644 --- a/internal/web/static/styles.css +++ b/internal/web/static/styles.css @@ -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) { diff --git a/internal/web/templates/domain_detail.html b/internal/web/templates/domain_detail.html index 148a9f4..e45f454 100644 --- a/internal/web/templates/domain_detail.html +++ b/internal/web/templates/domain_detail.html @@ -116,7 +116,7 @@

Der API-Token bleibt verschlüsselt. Eine Anzeige erfordert erneut das Admin-Passwort.

API-Token anzeigen
-
Domain-Verbindung entfernen
+
Domain-Verbindung entfernen
{{end}} diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index 6bf4a1a..4769f31 100644 --- a/internal/web/templates/layout.html +++ b/internal/web/templates/layout.html @@ -6,8 +6,8 @@ {{.Title}} · DNS Control - - + + diff --git a/internal/web/templates/pihole.html b/internal/web/templates/pihole.html index 3a91322..640d350 100644 --- a/internal/web/templates/pihole.html +++ b/internal/web/templates/pihole.html @@ -4,8 +4,6 @@
Lokales Netzwerk

Pi-hole DNS

Lokale A- und AAAA-Auflösungen ähnlich wie deine öffentlichen Domains verwalten.

{{if .PiHoleView}}
- {{else}} - Pi-hole verbinden {{end}} @@ -75,16 +73,17 @@
Anwendungspasswort anzeigen
-
Pi-hole-Verbindung entfernen
+
Pi-hole-Verbindung entfernen
{{else}} -
PH

Noch kein Pi-hole verbunden

Verbinde genau einen Pi-hole über die v6-API. Danach lassen sich lokale DNS-Einträge wie Records im Domain-Tab verwalten.

Pi-hole verbinden
-
-

Pi-hole verbinden

API v6
-

Verwende ein eigenes Pi-hole-Anwendungspasswort. Es wird mit dem Master-Key dieser Anwendung verschlüsselt.

-
Nur für ein selbstsigniertes Zertifikat in einem vertrauenswürdigen internen Netz verwenden. Bei HTTP wird das Passwort auf dem Netzwerkweg unverschlüsselt übertragen.
+
+
+

Pi-hole verbinden

API v6
+

Verwende ein eigenes Pi-hole-Anwendungspasswort. Es wird mit dem Master-Key dieser Anwendung verschlüsselt.

+
Nur für ein selbstsigniertes Zertifikat in einem vertrauenswürdigen internen Netz verwenden. Bei HTTP wird das Passwort auf dem Netzwerkweg unverschlüsselt übertragen.
+
{{end}} {{end}}