Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a000152423 | ||
|
|
2082b68794 | ||
|
|
42fa6efc84 | ||
|
|
42085dcde0 |
No files matched your search
@@ -0,0 +1,271 @@
|
||||
# Arbeitsanweisung für Agenten
|
||||
|
||||
Diese Datei gilt für das gesamte Repository. Lies sie vor Änderungen am Projekt vollständig.
|
||||
|
||||
## Release-Grundsätze
|
||||
|
||||
- Veröffentliche nur, wenn der Benutzer das Release ausdrücklich freigegeben hat.
|
||||
- Verwende Semantic Versioning mit führendem `v`, beispielsweise `v1.2.2`.
|
||||
- Bestehende Tags und veröffentlichte Versions-Images werden niemals verschoben, überschrieben oder erneut verwendet.
|
||||
- `compose.yml` bleibt auf eine konkrete Version gepinnt. Verwende dort nicht `stable`.
|
||||
- Veröffentliche genau zwei Registry-Tags: die konkrete Version und `stable`.
|
||||
- Ein Release muss weiterhin `linux/amd64` und `linux/arm64` enthalten.
|
||||
- Verändere oder lösche beim Release keine Datenbank-, Daten- oder Secret-Volumes.
|
||||
- Gib niemals Git-, Registry-, Hetzner- oder Pi-hole-Zugangsdaten in Befehlsausgaben, URLs, Commits oder Release Notes aus.
|
||||
- Verwende kein Force-Push und keine destruktiven Git-Befehle.
|
||||
|
||||
## Ziele
|
||||
|
||||
- Quell-Repository: `https://git.j-mahler.de/admin/hetzner-api-dyndns-records-webinterface.git`
|
||||
- Container-Image: `git.j-mahler.de/admin/domain-control-webinterface-prod`
|
||||
- Release-Workflow: `.gitea/workflows/release.yml`
|
||||
- Deployment: `compose.yml`, standardmäßig Port `9090`
|
||||
- Healthcheck: `http://127.0.0.1:9090/healthz`
|
||||
|
||||
## 1. Release-Kandidaten prüfen
|
||||
|
||||
Arbeite nur auf `main`. Prüfe zuerst den vollständigen Zustand und erhalte alle vorhandenen Benutzeränderungen:
|
||||
|
||||
```bash
|
||||
git status --short --branch
|
||||
git diff --stat
|
||||
git diff --check
|
||||
git remote -v
|
||||
git tag --sort=-v:refname | head
|
||||
```
|
||||
|
||||
Kontrolliere den tatsächlichen Diff. Stage oder committe keine fremden oder sachfremden Änderungen. Falls Frontend-Dateien geändert wurden, erhöhe in `internal/web/templates/layout.html` die Cache-Version beider Assets gemeinsam, beispielsweise von `features-14` auf `features-15`.
|
||||
|
||||
## 2. Version anheben
|
||||
|
||||
Lege eine noch nicht vorhandene Version fest. Für reine Fehler- und UI-Korrekturen wird normalerweise die Patch-Version erhöht.
|
||||
|
||||
Ersetze die bisherige Version mindestens an diesen Stellen:
|
||||
|
||||
- `compose.yml`
|
||||
- Image-Beispiel in `README.md`
|
||||
|
||||
Danach dürfen diese Dateien die vorherige Version nicht mehr enthalten. Setze `PREVIOUS` auf die tatsächlich vorherige Version:
|
||||
|
||||
```bash
|
||||
PREVIOUS=v1.2.1
|
||||
if grep -n "$PREVIOUS" README.md compose.yml; then
|
||||
echo "Alte Version $PREVIOUS ist noch eingetragen." >&2
|
||||
exit 1
|
||||
fi
|
||||
docker compose -f compose.yml config --quiet
|
||||
git diff --check
|
||||
```
|
||||
|
||||
## 3. Pflichtprüfungen vor dem Tag
|
||||
|
||||
JavaScript und Compose:
|
||||
|
||||
```bash
|
||||
node --check internal/web/static/app.js
|
||||
docker compose -f compose.yml config --quiet
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Go-Quelltext formatieren, falls Go-Dateien geändert wurden. Formatiere nur bewusst geänderte Dateien, bei Bedarf ebenfalls im Container:
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v "$PWD":/src \
|
||||
-w /src golang:1.25.12-alpine \
|
||||
gofmt -w PFAD/ZUR/GEAENDERTEN_DATEI.go
|
||||
```
|
||||
|
||||
Das Projekt wird in einem Go-Container geprüft, weil auf dem Host nicht zwingend Go installiert ist:
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v "$PWD":/src \
|
||||
-v /tmp/dyndns-go-mod:/go/pkg/mod \
|
||||
-v /tmp/dyndns-go-build:/root/.cache/go-build \
|
||||
-w /src golang:1.25.12-alpine \
|
||||
sh -c 'apk add --no-cache build-base >/dev/null && go vet ./... && go test -race -count=1 ./...'
|
||||
```
|
||||
|
||||
Bei Abhängigkeits-, Kryptografie-, Authentifizierungs- oder sonstigen Sicherheitsänderungen zusätzlich:
|
||||
|
||||
```bash
|
||||
docker run --rm \
|
||||
-v "$PWD":/src \
|
||||
-v /tmp/dyndns-go-mod:/go/pkg/mod \
|
||||
-v /tmp/dyndns-go-build:/root/.cache/go-build \
|
||||
-w /src golang:1.25.12-alpine \
|
||||
sh -c 'apk add --no-cache build-base >/dev/null && go run golang.org/x/vuln/cmd/govulncheck@latest ./...'
|
||||
```
|
||||
|
||||
Bei einem Fehler wird nicht getaggt oder veröffentlicht. Behebe den Fehler und beginne die finalen Prüfungen erneut.
|
||||
|
||||
## 4. Commit, Tag und Git-Push
|
||||
|
||||
Stage nur die geprüften Dateien und kontrolliere den Index:
|
||||
|
||||
```bash
|
||||
git add DATEI1 DATEI2
|
||||
git diff --cached --check
|
||||
git diff --cached --stat
|
||||
git diff --cached
|
||||
```
|
||||
|
||||
Erstelle einen sachlichen Commit und einen annotierten Tag:
|
||||
|
||||
```bash
|
||||
git commit -m "fix: kurze Beschreibung"
|
||||
git tag -a vX.Y.Z -m "Release vX.Y.Z"
|
||||
git show --no-patch --format=fuller vX.Y.Z
|
||||
```
|
||||
|
||||
Pushe Branch und Tag atomar, damit nicht nur einer von beiden veröffentlicht wird:
|
||||
|
||||
```bash
|
||||
git push --atomic origin main refs/tags/vX.Y.Z
|
||||
```
|
||||
|
||||
Prüfe anschließend, dass Remote-Branch und aufgelöster Tag auf denselben Release-Commit zeigen:
|
||||
|
||||
```bash
|
||||
git ls-remote origin refs/heads/main refs/tags/vX.Y.Z 'refs/tags/vX.Y.Z^{}'
|
||||
```
|
||||
|
||||
## 5. Container-Image veröffentlichen
|
||||
|
||||
Ein `v*`-Tag startet den Gitea-Workflow. Prüfe zuerst in Gitea, ob der Run tatsächlich läuft und erfolgreich endet. Wenn der Workflow erfolgreich ist, darf nicht zusätzlich manuell in dieselben Tags gepusht werden.
|
||||
|
||||
### Bekannter Gitea-Runner-Zustand
|
||||
|
||||
Stand 13.07.2026 ist kein Gitea-Actions-Runner aktiv. Die Runs bleiben auf `queued`. Der übliche Cancel-Endpunkt ist in dieser Gitea-Version nicht vorhanden; das Löschen wartender Runs antwortete mit HTTP 400. Verändere deshalb nicht direkt die Gitea-Datenbank. Prüfe den Zustand bei jedem Release neu.
|
||||
|
||||
Bevor später ein Runner aktiviert wird, müssen die alten wartenden Release-Runs in Gitea bereinigt werden. Andernfalls könnten sie veraltete Images erneut als `stable` veröffentlichen.
|
||||
|
||||
### Manueller Multi-Arch-Fallback
|
||||
|
||||
Melde Docker vorab interaktiv an, falls noch keine Registry-Anmeldung besteht:
|
||||
|
||||
```bash
|
||||
docker login git.j-mahler.de
|
||||
```
|
||||
|
||||
Gib den Token nur über die verdeckte Passwortabfrage ein. Der Token benötigt mindestens Paket-Schreibzugriff.
|
||||
|
||||
Prüfe den vorhandenen Builder:
|
||||
|
||||
```bash
|
||||
docker buildx inspect domain-control-release-builder --bootstrap
|
||||
```
|
||||
|
||||
Falls er nicht existiert:
|
||||
|
||||
```bash
|
||||
docker buildx create --name domain-control-release-builder --driver docker-container --use
|
||||
docker buildx inspect domain-control-release-builder --bootstrap
|
||||
```
|
||||
|
||||
Falls `linux/arm64` nicht unter `Platforms` erscheint, registriere einmalig die ARM64-Emulation:
|
||||
|
||||
```bash
|
||||
docker run --privileged --rm tonistiigi/binfmt:qemu-v10.0.4-56 --install arm64
|
||||
docker buildx inspect domain-control-release-builder --bootstrap
|
||||
```
|
||||
|
||||
Setze Version und Commit bewusst und prüfe beide Werte vor dem Build:
|
||||
|
||||
```bash
|
||||
VERSION=vX.Y.Z
|
||||
REVISION="$(git rev-parse HEAD)"
|
||||
printf 'Version: %s\nCommit: %s\n' "$VERSION" "$REVISION"
|
||||
```
|
||||
|
||||
Baue und pushe anschließend beide Plattformen in einem gemeinsamen Manifest:
|
||||
|
||||
```bash
|
||||
docker buildx build \
|
||||
--builder domain-control-release-builder \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--tag "git.j-mahler.de/admin/domain-control-webinterface-prod:$VERSION" \
|
||||
--tag "git.j-mahler.de/admin/domain-control-webinterface-prod:stable" \
|
||||
--label org.opencontainers.image.source=https://git.j-mahler.de/admin/hetzner-api-dyndns-records-webinterface \
|
||||
--label "org.opencontainers.image.revision=$REVISION" \
|
||||
--label "org.opencontainers.image.version=$VERSION" \
|
||||
--provenance=false \
|
||||
--push .
|
||||
```
|
||||
|
||||
Der emulierte ARM64-Build mit CGO und SQLite kann etwa acht Minuten lang ohne neue Ausgabe rechnen. Nicht allein deshalb abbrechen. Prüfe bei Zweifeln, ob der BuildKit-Container CPU verwendet.
|
||||
|
||||
## 6. Registry verifizieren
|
||||
|
||||
Prüfe sowohl die konkrete Version als auch `stable`:
|
||||
|
||||
```bash
|
||||
docker buildx imagetools inspect "git.j-mahler.de/admin/domain-control-webinterface-prod:$VERSION"
|
||||
docker buildx imagetools inspect git.j-mahler.de/admin/domain-control-webinterface-prod:stable
|
||||
```
|
||||
|
||||
Beide Ausgaben müssen denselben Manifest-Digest und genau diese Plattformen zeigen:
|
||||
|
||||
- `linux/amd64`
|
||||
- `linux/arm64`
|
||||
|
||||
Ein erfolgreicher Build ohne diese Manifestprüfung gilt nicht als abgeschlossenes Release.
|
||||
|
||||
## 7. Gitea-Releaseeintrag
|
||||
|
||||
Lege für den neuen Tag einen veröffentlichten, nicht als Entwurf und nicht als Pre-Release markierten Gitea-Releaseeintrag an.
|
||||
|
||||
Die Release Notes enthalten mindestens:
|
||||
|
||||
- kurze, benutzerverständliche Änderungen seit der vorherigen Version
|
||||
- vollständigen Image-Namen mit Version
|
||||
- `linux/amd64` und `linux/arm64`
|
||||
|
||||
Nutze vorhandene Anmeldedaten sicher über Gitea UI oder API. Schreibe Tokens niemals in die URL oder die Release Notes. Lies den erzeugten Eintrag anschließend zurück und kontrolliere Tag, Titel und Beschreibung.
|
||||
|
||||
## 8. Laufenden Container auf das Release umstellen
|
||||
|
||||
Erst nach erfolgreicher Registry-Prüfung:
|
||||
|
||||
```bash
|
||||
docker compose pull
|
||||
docker compose up -d --force-recreate
|
||||
```
|
||||
|
||||
Das Compose verwendet dieselben benannten Daten- und Secret-Volumes. Erstelle sie nicht neu und lösche sie nicht.
|
||||
|
||||
Prüfe abschließend:
|
||||
|
||||
```bash
|
||||
docker compose ps
|
||||
docker inspect --format 'image={{.Config.Image}} digest={{.Image}} health={{if .State.Health}}{{.State.Health.Status}}{{end}} readonly={{.HostConfig.ReadonlyRootfs}} user={{.Config.User}}' hetzner-api-dyndns-records-webinterface-dyndns-1
|
||||
docker image inspect --format '{{json .RepoDigests}}' "git.j-mahler.de/admin/domain-control-webinterface-prod:$VERSION"
|
||||
curl --silent --show-error --fail http://127.0.0.1:9090/healthz
|
||||
docker logs --tail 80 hetzner-api-dyndns-records-webinterface-dyndns-1
|
||||
git status --short --branch
|
||||
```
|
||||
|
||||
Erwartet werden:
|
||||
|
||||
- Containerstatus `healthy`
|
||||
- Image-Name mit der konkreten Version, nicht mit einem lokalen Testtag
|
||||
- schreibgeschütztes Root-Dateisystem
|
||||
- Benutzer `dyndns:dyndns`
|
||||
- Registry-Digest identisch mit dem zuvor geprüften Versionsmanifest
|
||||
- HTTP-Antwort `ok`
|
||||
- sauberer und mit `origin/main` synchroner Git-Arbeitsbaum
|
||||
|
||||
## Abschlussmeldung
|
||||
|
||||
Melde dem Benutzer kompakt:
|
||||
|
||||
- Version und Commit
|
||||
- Link zum Gitea-Release
|
||||
- veröffentlichte Tags
|
||||
- Manifest-Digest und Plattformen
|
||||
- bestandene Prüfungen
|
||||
- Healthstatus des laufenden Containers
|
||||
- einen weiterhin bestehenden Runner- oder Infrastrukturhinweis
|
||||
|
||||
Ein Git-Tag allein, ein nur lokal gebautes Image oder ein lediglich gestarteter Workflow ist kein abgeschlossenes Release.
|
||||
@@ -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.2
|
||||
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.2
|
||||
restart: unless-stopped
|
||||
ports:
|
||||
- "9090:8080"
|
||||
|
||||
@@ -71,7 +71,11 @@ func New(rawURL string, insecureTLS bool) (*Client, error) {
|
||||
}
|
||||
|
||||
func normalizeBaseURL(raw string) (string, error) {
|
||||
parsed, err := url.Parse(strings.TrimSpace(raw))
|
||||
raw = strings.TrimSpace(raw)
|
||||
if address := net.ParseIP(raw); address != nil && address.To4() != nil {
|
||||
raw = "http://" + raw
|
||||
}
|
||||
parsed, err := url.Parse(raw)
|
||||
if err != nil || parsed.Host == "" || (parsed.Scheme != "http" && parsed.Scheme != "https") {
|
||||
return "", errors.New("Pi-hole-Adresse muss eine vollständige HTTP- oder HTTPS-URL sein")
|
||||
}
|
||||
|
||||
@@ -101,6 +101,10 @@ func TestNewValidatesAndNormalizesPiHoleURL(t *testing.T) {
|
||||
if err != nil || client.BaseURL() != "http://pihole.local/custom/api" {
|
||||
t.Fatalf("custom path not normalized: %q err=%v", client.BaseURL(), err)
|
||||
}
|
||||
client, err = New("10.0.20.100", false)
|
||||
if err != nil || client.BaseURL() != "http://10.0.20.100/api" {
|
||||
t.Fatalf("bare IPv4 address not normalized: %q err=%v", client.BaseURL(), err)
|
||||
}
|
||||
for _, invalid := range []string{"pi.hole", "ftp://pi.hole", "https://user:pass@pi.hole", "https://pi.hole?sid=secret"} {
|
||||
if _, err := New(invalid, false); err == nil {
|
||||
t.Errorf("invalid URL accepted: %s", invalid)
|
||||
|
||||
@@ -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"`, `name="base_url" type="text"`, `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"`, `name="base_url" type="text"`, `10.0.20.100 oder https://pi.hole`} {
|
||||
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-15">
|
||||
<script src="/static/app.js?v=features-15" 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>
|
||||
|
||||
@@ -71,20 +69,21 @@
|
||||
</article>
|
||||
<article class="panel" id="pihole-connection"><div class="panel-heading"><div><span class="section-label pihole-kicker">API-Zugang</span><h2>Verbindung verwalten</h2></div><span class="status-pill {{if $view.FetchError}}status-bad{{else}}pihole-status-pill{{end}}">{{if $view.FetchError}}Nicht erreichbar{{else}}Verbunden{{end}}</span></div>
|
||||
<p class="panel-description">Das Anwendungspasswort bleibt verschlüsselt und standardmäßig verborgen. Eine Anzeige erfordert erneut dein Admin-Passwort.</p>
|
||||
<form action="/pihole/{{$integration.ID}}/save" method="post" class="stacked-form" data-dirty-form><input type="hidden" name="csrf" value="{{.CSRF}}"><label>Pi-hole-Adresse <input name="base_url" type="url" value="{{$integration.BaseURL}}" required><small>Basis-URL oder vollständiger <code>/api</code>-Pfad.</small></label><label>Neues Anwendungspasswort <input name="password" type="password" autocomplete="new-password"><small>Leer lassen, um das gespeicherte Passwort beizubehalten.</small></label><label class="switch pihole-switch"><input type="checkbox" name="insecure_tls" {{if $integration.InsecureTLS}}checked{{end}}><span></span> TLS-Zertifikat nicht prüfen</label><button class="button button-pihole" type="submit" data-dirty-submit disabled aria-disabled="true">Verbindung prüfen und speichern</button></form>
|
||||
<form action="/pihole/{{$integration.ID}}/save" method="post" class="stacked-form" data-dirty-form><input type="hidden" name="csrf" value="{{.CSRF}}"><label>Pi-hole-Adresse <input name="base_url" type="text" inputmode="url" autocapitalize="none" spellcheck="false" value="{{$integration.BaseURL}}" required><small>IPv4-Adresse oder Basis-URL; <code>/api</code> wird automatisch ergänzt.</small></label><label>Neues Anwendungspasswort <input name="password" type="password" autocomplete="new-password"><small>Leer lassen, um das gespeicherte Passwort beizubehalten.</small></label><label class="switch pihole-switch"><input type="checkbox" name="insecure_tls" {{if $integration.InsecureTLS}}checked{{end}}><span></span> TLS-Zertifikat nicht prüfen</label><button class="button button-pihole" type="submit" data-dirty-submit disabled aria-disabled="true">Verbindung prüfen und speichern</button></form>
|
||||
<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>
|
||||
<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>
|
||||
<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="text" inputmode="url" autocapitalize="none" spellcheck="false" placeholder="10.0.20.100 oder https://pi.hole" required><small>IPv4-Adressen werden automatisch über HTTP verbunden; <code>/api</code> wird 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