From bef669f3d9dd94afbf31b53474c386b95e128da8 Mon Sep 17 00:00:00 2001 From: admin Date: Sat, 10 Oct 2026 02:46:58 +0200 Subject: [PATCH] fix: show meaningful update runs and tidy update access layout --- Dockerfile | 2 +- README.md | 6 ++- UPDATE.md | 6 +-- compose.yml | 2 +- internal/buildinfo/version.go | 2 +- internal/store/store.go | 19 +++++++++- internal/web/server.go | 4 +- internal/web/server_test.go | 53 ++++++++++++++++++++++++--- internal/web/static/app.js | 2 +- internal/web/templates/dashboard.html | 3 +- internal/web/templates/layout.html | 4 +- internal/web/templates/system.html | 2 +- 12 files changed, 85 insertions(+), 20 deletions(-) diff --git a/Dockerfile b/Dockerfile index 4d6c7d1..e3eedfd 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,5 +1,5 @@ # syntax=docker/dockerfile:1.7 -ARG VERSION=v1.5.1 +ARG VERSION=v1.5.2 ARG MINIMUM_UPDATE_VERSION=v1.4.0 ARG MINIMUM_AGENT_PROTOCOL=2 ARG RUNTIME_CONTRACT_VERSION=2 diff --git a/README.md b/README.md index 08a0768..a496225 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: &image git.j-mahler.de/admin/domain-control-webinterface-prod:v1.5.1 + image: &image git.j-mahler.de/admin/domain-control-webinterface-prod:v1.5.2 restart: unless-stopped stop_grace_period: 20s ports: ["9090:8080"] @@ -102,6 +102,10 @@ Port 9090 verwendet zunächst unverschlüsseltes HTTP. Den Port daher nur in ein ## Netzwerkausfälle und Diagnose +Die Übersicht zeigt unter **Letzte Update-Läufe** die letzten sechs Läufe mit +Änderungen, Fehlern oder Teilerfolgen. Erfolgreiche Prüfungen ohne Änderungen +werden ausgeblendet; die vollständige Laufhistorie bleibt für die Diagnose erhalten. + Der Build und die Container-Prüfungen verwenden Go 1.26.9. Damit sind auch die vom Sicherheitscheck gemeldeten HTTP-/Template-Probleme der bisherigen Go-1.25.13-Basis behoben (siehe etwa [GO-2026-6617](https://pkg.go.dev/vuln/GO-2026-6617)). diff --git a/UPDATE.md b/UPDATE.md index e47482c..5b38c85 100644 --- a/UPDATE.md +++ b/UPDATE.md @@ -1,6 +1,6 @@ # Domain Control aktualisieren -Die aktuelle Compose-Version ist `v1.5.1`. Datenbank, Einstellungen und +Die aktuelle Compose-Version ist `v1.5.2`. Datenbank, Einstellungen und verschlüsselte API-Zugänge liegen in benannten Docker-Volumes und bleiben bei den folgenden Verfahren erhalten. @@ -63,7 +63,7 @@ In `compose.yml` wird nur die zentrale, konkret gepinnte Image-Zeile geändert: ```yaml services: dyndns: - image: &image git.j-mahler.de/admin/domain-control-webinterface-prod:v1.5.1 + image: &image git.j-mahler.de/admin/domain-control-webinterface-prod:v1.5.2 update-agent: image: *image ``` @@ -97,7 +97,7 @@ Die erwartete Antwort lautet `ok`. ## Portainer oder Dockge 1. Den bestehenden Stack bearbeiten. -2. Nur den Tag der zentralen Image-Zeile auf `v1.5.1` setzen. +2. Nur den Tag der zentralen Image-Zeile auf `v1.5.2` setzen. 3. Den Stack mit vorherigem Image-Pull erneut bereitstellen. 4. Kontrollieren, dass beide Container laufen und die Web-App `healthy` ist. diff --git a/compose.yml b/compose.yml index ba04c74..29268c7 100644 --- a/compose.yml +++ b/compose.yml @@ -1,6 +1,6 @@ services: dyndns: - image: &image git.j-mahler.de/admin/domain-control-webinterface-prod:v1.5.1 + image: &image git.j-mahler.de/admin/domain-control-webinterface-prod:v1.5.2 restart: unless-stopped stop_grace_period: 20s ports: ["9090:8080"] diff --git a/internal/buildinfo/version.go b/internal/buildinfo/version.go index baa6da5..c6ec57a 100644 --- a/internal/buildinfo/version.go +++ b/internal/buildinfo/version.go @@ -18,4 +18,4 @@ const ( // Version is replaced through -ldflags for release images. The source fallback // keeps local builds and tests honest about the currently checked-out release. -var Version = "v1.5.1" +var Version = "v1.5.2" diff --git a/internal/store/store.go b/internal/store/store.go index 3d27563..635dc1b 100644 --- a/internal/store/store.go +++ b/internal/store/store.go @@ -1480,7 +1480,24 @@ func (s *Store) RecoverInterruptedRuns(ctx context.Context) error { } func (s *Store) RecentRuns(ctx context.Context, limit int) ([]model.UpdateRun, error) { - rows, err := s.db.QueryContext(ctx, `SELECT id, started_at, finished_at, status, ipv4, ipv6, summary FROM update_runs ORDER BY id DESC LIMIT ?`, limit) + return s.recentRuns(ctx, limit, false) +} + +// RecentSignificantRuns keeps changes and failures visible even after many +// successful checks without changes. The existing history stores counts only +// in its summary, so exclude the scheduler's two known successful no-op results. +// Unknown summaries and all failures are retained rather than silently hidden. +func (s *Store) RecentSignificantRuns(ctx context.Context, limit int) ([]model.UpdateRun, error) { + return s.recentRuns(ctx, limit, true) +} + +func (s *Store) recentRuns(ctx context.Context, limit int, significantOnly bool) ([]model.UpdateRun, error) { + query := `SELECT id, started_at, finished_at, status, ipv4, ipv6, summary FROM update_runs` + if significantOnly { + query += ` WHERE status IN ('error', 'partial') OR (status = 'ok' AND summary NOT IN ('0 Ziel(e) aktualisiert, 0 Fehler', 'Keine aktiven DynDNS-Ziele'))` + } + query += ` ORDER BY id DESC LIMIT ?` + rows, err := s.db.QueryContext(ctx, query, limit) if err != nil { return nil, err } diff --git a/internal/web/server.go b/internal/web/server.go index f0b39ca..4f80d7e 100644 --- a/internal/web/server.go +++ b/internal/web/server.go @@ -393,7 +393,7 @@ func (s *Server) dashboard(w http.ResponseWriter, r *http.Request, session *mode var events []model.AuditEvent errorCount := 0 if session.Role == "admin" { - runs, _ = s.store.RecentRuns(r.Context(), 6) + runs, _ = s.store.RecentSignificantRuns(r.Context(), 6) events, _ = s.store.RecentAudit(r.Context(), 6) errorCount, _ = s.store.CountErrors(r.Context()) } @@ -1597,7 +1597,7 @@ func (s *Server) schedulerAPI(w http.ResponseWriter, r *http.Request, session *m LastIPv6: settings.LastIPv6, } if session.Role == "admin" && r.URL.Query().Get("runs") == "1" { - runs, err := s.store.RecentRuns(r.Context(), 6) + runs, err := s.store.RecentSignificantRuns(r.Context(), 6) if err != nil { s.serverError(w, err) return diff --git a/internal/web/server_test.go b/internal/web/server_test.go index 5b81d2f..a53890c 100644 --- a/internal/web/server_test.go +++ b/internal/web/server_test.go @@ -364,7 +364,7 @@ func TestDashboardRendersLiveUpdateHooks(t *testing.T) { `data-dashboard-ipv6`, `data-recent-runs aria-live="polite" aria-busy="true"`, `Aktualisierung läuft`, - `features-30`, + `features-31`, `data-local-time="1788001920"`, `dashboard-zone-item`, `DNS-Zone · example.de`, @@ -375,7 +375,7 @@ func TestDashboardRendersLiveUpdateHooks(t *testing.T) { } } -func TestSchedulerAPIIncludesRecentRunsOnlyForAdministrators(t *testing.T) { +func TestRecentRunsShowChangesAndErrorsOnlyForAdministrators(t *testing.T) { t.Parallel() ctx := context.Background() database, err := store.Open(ctx, filepath.Join(t.TempDir(), "app.db")) @@ -390,8 +390,38 @@ func TestSchedulerAPIIncludesRecentRunsOnlyForAdministrators(t *testing.T) { if err := database.FinishUpdateRun(ctx, runID, "ok", "198.51.100.10", "", "1 Ziel aktualisiert"); err != nil { t.Fatal(err) } + // More no-op runs than the display limit must not push out older changes. + for index := 0; index < 12; index++ { + id, err := database.CreateUpdateRun(ctx) + if err != nil { + t.Fatal(err) + } + summary := "0 Ziel(e) aktualisiert, 0 Fehler" + if index%2 == 0 { + summary = "Keine aktiven DynDNS-Ziele" + } + if err := database.FinishUpdateRun(ctx, id, "ok", "", "", summary); err != nil { + t.Fatal(err) + } + } + for _, status := range []string{"partial", "error"} { + id, err := database.CreateUpdateRun(ctx) + if err != nil { + t.Fatal(err) + } + if err := database.FinishUpdateRun(ctx, id, status, "", "", "0 Ziel(e) aktualisiert, 0 Fehler"); err != nil { + t.Fatal(err) + } + } + if _, err := database.CreateUpdateRun(ctx); err != nil { + t.Fatal(err) + } schedule := scheduler.New(database, nil, nil, nil, slog.Default()) - server := &Server{store: database, scheduler: schedule} + templates, err := loadTemplates() + if err != nil { + t.Fatal(err) + } + server := &Server{store: database, scheduler: schedule, templates: templates, logger: slog.Default()} adminResponse := httptest.NewRecorder() adminRequest := httptest.NewRequest(http.MethodGet, "/api/v1/scheduler?runs=1", nil) @@ -406,13 +436,26 @@ func TestSchedulerAPIIncludesRecentRunsOnlyForAdministrators(t *testing.T) { if err := json.NewDecoder(adminResponse.Body).Decode(&adminState); err != nil { t.Fatal(err) } - if adminState.RecentRuns == nil || len(*adminState.RecentRuns) != 1 { + if adminState.RecentRuns == nil || len(*adminState.RecentRuns) != 3 { t.Fatalf("administrator did not receive recent runs: %#v", adminState.RecentRuns) } - if run := (*adminState.RecentRuns)[0]; run.ID != runID || run.Status != "ok" || run.Summary != "1 Ziel aktualisiert" { + if run := (*adminState.RecentRuns)[2]; run.ID != runID || run.Status != "ok" || run.Summary != "1 Ziel aktualisiert" { t.Fatalf("unexpected recent run: %#v", run) } + if (*adminState.RecentRuns)[0].Status != "error" || (*adminState.RecentRuns)[1].Status != "partial" { + t.Fatal("zero-change errors or partial runs were hidden") + } + allRuns, err := database.RecentRuns(ctx, 100) + if err != nil || len(allRuns) != 16 { + t.Fatalf("filter changed diagnostic history: count=%d err=%v", len(allRuns), err) + } + pageResponse := httptest.NewRecorder() + server.dashboard(pageResponse, httptest.NewRequest(http.MethodGet, "/", nil), &model.Session{Role: "admin"}) + if pageResponse.Code != http.StatusOK || !strings.Contains(pageResponse.Body.String(), "1 Ziel aktualisiert") || strings.Contains(pageResponse.Body.String(), "Keine aktiven DynDNS-Ziele") || strings.Count(pageResponse.Body.String(), "0 Ziel(e) aktualisiert, 0 Fehler") != 2 { + t.Fatal("initial dashboard differs from the filtered live history") + } + memberResponse := httptest.NewRecorder() memberRequest := httptest.NewRequest(http.MethodGet, "/api/v1/scheduler?runs=1", nil) server.schedulerAPI(memberResponse, memberRequest, &model.Session{Role: "member"}) diff --git a/internal/web/static/app.js b/internal/web/static/app.js index a70e103..03e3081 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -636,7 +636,7 @@ if (runs.length === 0) { const empty = document.createElement('div'); empty.className = 'empty-state'; - empty.textContent = 'Noch kein Update-Lauf protokolliert.'; + empty.textContent = 'Noch keine Änderungen oder Fehler protokolliert.'; recentRuns.replaceChildren(empty); return; } diff --git a/internal/web/templates/dashboard.html b/internal/web/templates/dashboard.html index 61bda16..703811b 100644 --- a/internal/web/templates/dashboard.html +++ b/internal/web/templates/dashboard.html @@ -28,6 +28,7 @@ {{if .IsAdmin}}
+

Die letzten Änderungen, Fehler und Teilerfolge.

{{if .Dashboard.RecentRuns}}
@@ -35,7 +36,7 @@
{{if .Summary}}{{.Summary}}{{else if eq .Status "running"}}Aktualisierung läuft{{else}}Update-Lauf{{end}}{{.Status}}
{{end}}
- {{else}}
Noch kein Update-Lauf protokolliert.
{{end}} + {{else}}
Noch keine Änderungen oder Fehler protokolliert.
{{end}}
{{end}} diff --git a/internal/web/templates/layout.html b/internal/web/templates/layout.html index c59af1c..f7203b5 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/system.html b/internal/web/templates/system.html index 497949b..156ea83 100644 --- a/internal/web/templates/system.html +++ b/internal/web/templates/system.html @@ -43,7 +43,6 @@
-

Öffentliche Releases und Container-Images werden ohne Anmeldung geladen. Für private Repositories oder Images kannst du einen Zugang hinterlegen; er wird nur bei Bedarf verwendet.

{{if .SoftwareUpdate.CredentialConfigured}}
@@ -57,6 +56,7 @@ {{end}}
+

Öffentliche Updates benötigen keine Anmeldung. Dieser Zugang wird nur für private Updates verwendet.