diff --git a/CHANGELOG.md b/CHANGELOG.md index 3d5defb..0dd550e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,6 +6,40 @@ and the project follows [Semantic Versioning](https://semver.org/). ## [Unreleased] +## [1.2.0] - 2026-08-22 + +### Added + +- Fleet-wide agent updates are now discoverable from Settings and the + dashboard, with arbitrary host subsets, name/tag/version/eligibility + filtering, explicit exclusion reasons, server-side membership validation, + and a canary-first pause after the first verified reconnect. Halted rolls + expose retry and resume actions. ([#43]) +- Running jobs with no recent persisted activity are detected server-side and + surfaced through deduplicated alerts. Kind-aware thresholds protect normal + long-running work, terminal jobs self-resolve, and Prometheus exports stuck + job count and oldest inactivity age. ([#41]) +- Snapshot projections now retain host-level refresh timestamps—including + authoritative empty reports—expose a derived stale flag, refresh after + backup, forget, and prune, and support an explicit operator reconciliation + endpoint. ([#40]) + +### Fixed + +- Raised the bounded WebSocket read limit on both peers so ordinary large + snapshot reports and restic events no longer disconnect otherwise healthy + agents. Regression coverage exercises payloads beyond the library's former + 32 KiB default in both directions. ([#44]) +- Fleet-update timeout verification now performs a final authoritative read of + the agent version delivered by the reconnect `hello`, avoiding a poll/deadline + race and reporting the last observed version when verification fails. ([#43]) + +### Changed + +- CI runner images and release images use the anonymous-pull `public` + namespace at `docker.dcglab.co.uk`; registry authentication remains required + only for publishing. + ## [1.1.1] - 2026-08-22 ### Fixed @@ -140,7 +174,8 @@ with a web UI, JSON API, and self-updating agent fleet. go vet, golangci-lint). - Threat model published (`docs/threat-model.md`). -[Unreleased]: https://gitea.dcglab.co.uk/steve/restic-manager/compare/v1.1.1...HEAD +[Unreleased]: https://gitea.dcglab.co.uk/steve/restic-manager/compare/v1.2.0...HEAD +[1.2.0]: https://gitea.dcglab.co.uk/steve/restic-manager/compare/v1.1.1...v1.2.0 [1.1.1]: https://gitea.dcglab.co.uk/steve/restic-manager/compare/v1.1.0...v1.1.1 [1.1.0]: https://gitea.dcglab.co.uk/steve/restic-manager/releases/tag/v1.1.0 [1.0.0]: https://gitea.dcglab.co.uk/steve/restic-manager/releases/tag/v1.0.0 @@ -148,3 +183,7 @@ with a web UI, JSON API, and self-updating agent fleet. [#37]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/37 [#36]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/36 [#34]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/34 +[#40]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/40 +[#41]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/41 +[#43]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/43 +[#44]: https://gitea.dcglab.co.uk/steve/restic-manager/issues/44 diff --git a/internal/alert/engine.go b/internal/alert/engine.go index 8a536c9..807019d 100644 --- a/internal/alert/engine.go +++ b/internal/alert/engine.go @@ -61,9 +61,20 @@ type Engine struct { stuckThresholds map[string]time.Duration closeOnce sync.Once + notifyWG sync.WaitGroup done chan struct{} } +func (e *Engine) dispatchNotification(ctx context.Context, payload notification.Payload) { + e.notifyWG.Add(1) + go func() { + defer e.notifyWG.Done() + e.hub.Dispatch(ctx, payload) + }() +} + +func (e *Engine) waitNotifications() { e.notifyWG.Wait() } + // NewEngine builds the engine. agentOfflineFloor + tickPeriod default // to 15min and 60s respectively when zero. func NewEngine(st *store.Store, hub *notification.Hub) *Engine { diff --git a/internal/alert/rules.go b/internal/alert/rules.go index d00c91e..6c30067 100644 --- a/internal/alert/rules.go +++ b/internal/alert/rules.go @@ -60,7 +60,7 @@ func (e *Engine) raiseAndNotify(ctx context.Context, hostID, kind, dedupKey, sev if err == nil { hostName = host.Name } - go e.hub.Dispatch(ctx, notification.Payload{ + e.dispatchNotification(ctx, notification.Payload{ Event: notification.EventRaised, AlertID: id, Severity: severity, @@ -85,7 +85,7 @@ func (e *Engine) Acknowledge(ctx context.Context, alertID, userID string, when t return nil //nolint:nilerr } p := alertPayload(ctx, e.store, notification.EventAcknowledged, a) - go e.hub.Dispatch(context.WithoutCancel(ctx), p) + e.dispatchNotification(context.WithoutCancel(ctx), p) return nil } @@ -99,7 +99,7 @@ func (e *Engine) Resolve(ctx context.Context, alertID string, when time.Time) er return nil } p := alertPayload(ctx, e.store, notification.EventResolved, a) - go e.hub.Dispatch(context.WithoutCancel(ctx), p) + e.dispatchNotification(context.WithoutCancel(ctx), p) return nil } @@ -164,7 +164,7 @@ func (e *Engine) resolveAndNotify(ctx context.Context, hostID, kind, dedupKey st if a.Kind != kind || a.DedupKey != dedupKey { continue } - go e.hub.Dispatch(ctx, notification.Payload{ + e.dispatchNotification(ctx, notification.Payload{ Event: notification.EventResolved, AlertID: a.ID, Severity: a.Severity, diff --git a/internal/alert/rules_test.go b/internal/alert/rules_test.go index ec6a2d3..8a0fa0e 100644 --- a/internal/alert/rules_test.go +++ b/internal/alert/rules_test.go @@ -25,6 +25,7 @@ func setupEngine(t *testing.T) (*Engine, *store.Store, string) { aead, _ := crypto.NewAEAD(key) hub := notification.NewHub(st, aead, "https://rm.example") eng := NewEngine(st, hub) + t.Cleanup(eng.waitNotifications) hostID := ulid.Make().String() if err := st.CreateHost(context.Background(), store.Host{ ID: hostID, Name: "alfa-01", OS: "linux", Arch: "amd64",