Letzte Update-Läufe
Die letzten Änderungen, Fehler und Teilerfolge.
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.Letzte Update-Läufe
Ö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}}