diff --git a/agent/docker_test.go b/agent/docker_test.go
index 0ba67aff..d38ce2b3 100644
--- a/agent/docker_test.go
+++ b/agent/docker_test.go
@@ -729,6 +729,7 @@ func TestGetDockerStatsChecksDockerVersionAfterContainerList(t *testing.T) {
stats, err := dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
+ require.NotNil(t, stats, "A successful empty snapshot must remain distinguishable from a collection failure")
assert.Empty(t, stats)
assert.True(t, dm.dockerVersionChecked)
assert.Equal(t, tt.expectedGood, dm.goodDockerVersion)
@@ -742,6 +743,7 @@ func TestGetDockerStatsChecksDockerVersionAfterContainerList(t *testing.T) {
stats, err = dm.getDockerStats(defaultCacheTimeMs)
require.NoError(t, err)
+ require.NotNil(t, stats, "A successful empty snapshot must remain distinguishable from a collection failure")
assert.Empty(t, stats)
assert.Equal(t, tt.expectedGood, dm.goodDockerVersion)
assert.Equal(t, tt.expectedPodman, dm.usingPodman)
diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go
index 35739702..fa4d1148 100644
--- a/internal/alerts/alerts.go
+++ b/internal/alerts/alerts.go
@@ -20,10 +20,10 @@ type hubLike interface {
}
type AlertManager struct {
- hub hubLike
- stopOnce sync.Once
- pendingAlerts sync.Map
- alertsCache *AlertsCache
+ hub hubLike
+ stopOnce sync.Once
+ pendingAlerts sync.Map
+ alertsCache *AlertsCache
}
type AlertMessageData struct {
diff --git a/internal/alerts/alerts_cache.go b/internal/alerts/alerts_cache.go
index cff46f3b..0fce5c64 100644
--- a/internal/alerts/alerts_cache.go
+++ b/internal/alerts/alerts_cache.go
@@ -1,6 +1,8 @@
package alerts
import (
+ "time"
+
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/store"
@@ -8,13 +10,14 @@ import (
// CachedAlertData represents the relevant fields of an alert record for status checking and updates.
type CachedAlertData struct {
- Id string
- SystemID string
- UserID string
- Name string
- Value float64
- Triggered bool
- Min uint8
+ Id string
+ SystemID string
+ UserID string
+ Name string
+ Value float64
+ Triggered bool
+ Min uint8
+ PendingSince time.Time
// Created types.DateTime
}
@@ -26,6 +29,7 @@ func (a *CachedAlertData) PopulateFromRecord(record *core.Record) {
a.Value = record.GetFloat("value")
a.Triggered = record.GetBool("triggered")
a.Min = uint8(record.GetInt("min"))
+ a.PendingSince = record.GetDateTime("pending_since").Time()
// a.Created = record.GetDateTime("created")
}
diff --git a/internal/alerts/alerts_container.go b/internal/alerts/alerts_container.go
new file mode 100644
index 00000000..6e5d1ba8
--- /dev/null
+++ b/internal/alerts/alerts_container.go
@@ -0,0 +1,318 @@
+package alerts
+
+import (
+ "errors"
+ "fmt"
+ "strings"
+ "time"
+
+ "github.com/henrygd/beszel/internal/entities/container"
+ "github.com/henrygd/beszel/internal/entities/system"
+
+ "github.com/pocketbase/pocketbase/core"
+)
+
+const (
+ // containerAlertName is the value stored in the alerts.name field for this alert type.
+ containerAlertName = "ContainerHealth"
+
+ // containerLogMaxLines caps how many matched (error/fatal) log lines are kept.
+ containerLogMaxLines = 12
+ // containerLogFallbackLines is how many trailing raw log lines are used when no
+ // line matches "error" or "fatal", so the notification still carries some context.
+ containerLogFallbackLines = 6
+ // containerLogExcerptMaxChars bounds a single container's log excerpt so a
+ // handful of containers can't blow past Discord's message size limit.
+ containerLogExcerptMaxChars = 500
+ // containerAlertMaxLogged is the max number of unhealthy containers we fetch
+ // and embed logs for in a single alert message.
+ containerAlertMaxLogged = 2
+ // containerAlertMessageMaxChars is a final safety cap on the whole message body.
+ containerAlertMessageMaxChars = 1800
+)
+
+// FetchContainerLogsFunc retrieves recent logs for a container ID from its
+// connected agent. Implementations should apply their own timeout. This is a
+// type alias (not a defined type) so it satisfies the hubLike interface in
+// internal/hub/systems, which declares the same func signature without
+// importing this package.
+type FetchContainerLogsFunc = func(containerID string) (string, error)
+
+// containerAlertTarget is an immutable snapshot of the fields needed after the
+// alert fires. Keeping agent-owned container records out of notification work
+// avoids retaining and concurrently reading data that is refreshed in place.
+type containerAlertTarget struct {
+ id string
+ name string
+}
+
+// HandleContainerAlerts checks configured "ContainerHealth" alerts for a system
+// against the Docker container health data included in the latest agent update.
+// It persists when containers first become unhealthy, fires from a fresh poll
+// once the configured delay has elapsed, and resolves once containers recover.
+// fetchLogs is used when an alert actually fires so the notification can include
+// a log excerpt (prioritizing lines containing "error"/"fatal") for context.
+func (am *AlertManager) HandleContainerAlerts(systemRecord *core.Record, data *system.CombinedData, fetchLogs FetchContainerLogsFunc) error {
+ alerts := am.alertsCache.GetAlertsByName(systemRecord.Id, containerAlertName)
+ if len(alerts) == 0 {
+ return nil
+ }
+ if data.Containers == nil {
+ // An unknown Docker state must not resolve a triggered alert or count
+ // toward the minimum unhealthy duration.
+ var result error
+ for _, alertData := range alerts {
+ if err := am.clearPendingContainerAlert(alertData); err != nil {
+ result = errors.Join(result, err)
+ }
+ }
+ return result
+ }
+
+ var unhealthy []*container.Stats
+ for _, c := range data.Containers {
+ if c.Health == container.DockerHealthUnhealthy {
+ unhealthy = append(unhealthy, c)
+ }
+ }
+
+ systemName := systemRecord.GetString("name")
+ now := time.Now().UTC()
+ var result error
+ for _, alertData := range alerts {
+ if len(unhealthy) > 0 {
+ if alertData.Triggered {
+ continue
+ }
+ min := max(1, int(alertData.Min))
+ if alertData.PendingSince.IsZero() {
+ pendingSince, err := am.setPendingContainerAlert(alertData, now)
+ if err != nil {
+ result = errors.Join(result, err)
+ continue
+ }
+ if pendingSince.IsZero() {
+ continue
+ }
+ alertData.PendingSince = pendingSince
+ if min > 1 {
+ continue
+ }
+ }
+ if min > 1 && now.Before(alertData.PendingSince.Add(time.Duration(min)*time.Minute)) {
+ continue
+ }
+ if err := am.sendContainerHealthAlert(true, systemName, alertData, snapshotContainerAlertTargets(unhealthy), fetchLogs); err != nil {
+ result = errors.Join(result, err)
+ }
+ continue
+ }
+
+ // no unhealthy containers right now
+ if err := am.clearPendingContainerAlert(alertData); err != nil {
+ result = errors.Join(result, err)
+ }
+ if !alertData.Triggered {
+ continue
+ }
+ if err := am.sendContainerHealthAlert(false, systemName, alertData, nil, fetchLogs); err != nil {
+ result = errors.Join(result, err)
+ }
+ }
+ return result
+}
+
+func snapshotContainerAlertTargets(containers []*container.Stats) []containerAlertTarget {
+ targets := make([]containerAlertTarget, len(containers))
+ for i, c := range containers {
+ targets[i] = containerAlertTarget{id: c.Id, name: c.Name}
+ }
+ return targets
+}
+
+// setPendingContainerAlert durably records the first unhealthy observation and
+// returns the persisted generation used to claim delivery.
+func (am *AlertManager) setPendingContainerAlert(alertData CachedAlertData, since time.Time) (time.Time, error) {
+ record, err := am.hub.FindRecordById("alerts", alertData.Id)
+ if err != nil {
+ return time.Time{}, err
+ }
+ if record.GetBool("triggered") {
+ return time.Time{}, nil
+ }
+ if pendingSince := record.GetDateTime("pending_since").Time(); !pendingSince.IsZero() {
+ return pendingSince, nil
+ }
+ // PocketBase date fields are persisted with millisecond precision. Normalize
+ // before saving so the update-hook cache and a subsequent database read agree.
+ since = since.Truncate(time.Millisecond)
+ record.Set("pending_since", since)
+ return since, am.hub.Save(record)
+}
+
+func (am *AlertManager) clearPendingContainerAlert(alertData CachedAlertData) error {
+ if alertData.PendingSince.IsZero() {
+ return nil
+ }
+ record, err := am.hub.FindRecordById("alerts", alertData.Id)
+ if err != nil {
+ return err
+ }
+ if record.GetDateTime("pending_since").Time().IsZero() {
+ return nil
+ }
+ record.Set("pending_since", nil)
+ return am.hub.Save(record)
+}
+
+// claimPendingContainerAlert marks an alert triggered only if the pending
+// generation is still current. A healthy/unknown update can clear the timestamp
+// while logs are being fetched, causing this claim to become a no-op.
+func (am *AlertManager) claimPendingContainerAlert(alertData CachedAlertData) (bool, error) {
+ record, err := am.hub.FindRecordById("alerts", alertData.Id)
+ if err != nil {
+ return false, err
+ }
+ pendingSince := record.GetDateTime("pending_since").Time()
+ if record.GetBool("triggered") || pendingSince.IsZero() || pendingSince.UnixMilli() != alertData.PendingSince.UnixMilli() {
+ return false, nil
+ }
+ record.Set("pending_since", nil)
+ record.Set("triggered", true)
+ return true, am.hub.Save(record)
+}
+
+// CancelPendingContainerAlerts clears pending container-health durations for a
+// system. Called when monitoring pauses or the system goes down.
+func (am *AlertManager) CancelPendingContainerAlerts(systemID string) {
+ for _, alertData := range am.alertsCache.GetAlertsByName(systemID, containerAlertName) {
+ if err := am.clearPendingContainerAlert(alertData); err != nil {
+ am.hub.Logger().Error("Failed to clear pending container alert", "err", err)
+ }
+ }
+}
+
+// sendContainerHealthAlert updates the alert's triggered state and sends the
+// notification. When unhealthy is true, it embeds a log excerpt (prioritizing
+// error/fatal lines) for up to containerAlertMaxLogged of the affected containers.
+func (am *AlertManager) sendContainerHealthAlert(unhealthy bool, systemName string, alertData CachedAlertData, containers []containerAlertTarget, fetchLogs FetchContainerLogsFunc) error {
+ link := am.hub.MakeLink("system", alertData.SystemID)
+ linkText := "View " + systemName
+
+ if !unhealthy {
+ if err := am.setAlertTriggered(alertData, false); err != nil {
+ return err
+ }
+ title := fmt.Sprintf("%s containers are healthy ✅", systemName)
+ return am.SendAlert(AlertMessageData{
+ UserID: alertData.UserID,
+ SystemID: alertData.SystemID,
+ Title: title,
+ Message: strings.TrimSuffix(title, " ✅"),
+ Link: link,
+ LinkText: linkText,
+ })
+ }
+
+ names := make([]string, len(containers))
+ for i, c := range containers {
+ names[i] = c.name
+ }
+
+ var title string
+ if len(names) == 1 {
+ title = fmt.Sprintf("Unhealthy container %s on %s \U0001F534", names[0], systemName)
+ } else {
+ title = fmt.Sprintf("%d unhealthy containers on %s \U0001F534", len(names), systemName)
+ }
+
+ var body strings.Builder
+ fmt.Fprintf(&body, "Unhealthy: %s", strings.Join(names, ", "))
+ body.WriteString(am.buildContainerLogsSection(containers, fetchLogs))
+
+ message := body.String()
+ if len(message) > containerAlertMessageMaxChars {
+ message = message[:containerAlertMessageMaxChars] + "\n…(truncated)"
+ }
+
+ claimed, err := am.claimPendingContainerAlert(alertData)
+ if err != nil || !claimed {
+ return err
+ }
+
+ return am.SendAlert(AlertMessageData{
+ UserID: alertData.UserID,
+ SystemID: alertData.SystemID,
+ Title: title,
+ Message: message,
+ Link: link,
+ LinkText: linkText,
+ })
+}
+
+// buildContainerLogsSection attempts to fetch and format log excerpts for up to
+// containerAlertMaxLogged unhealthy containers, to append to an alert message.
+func (am *AlertManager) buildContainerLogsSection(containers []containerAlertTarget, fetchLogs FetchContainerLogsFunc) string {
+ if fetchLogs == nil {
+ return ""
+ }
+
+ var section strings.Builder
+ attempts := min(len(containers), containerAlertMaxLogged)
+ for _, c := range containers[:attempts] {
+ rawLogs, err := fetchLogs(c.id)
+ if err != nil {
+ am.hub.Logger().Warn("Failed to fetch container logs for alert", "container", c.name, "err", err)
+ continue
+ }
+ excerpt := buildContainerLogExcerpt(rawLogs)
+ if excerpt == "" {
+ continue
+ }
+ fmt.Fprintf(§ion, "\n\n%s logs:\n```\n%s\n```", c.name, excerpt)
+ }
+
+ if len(containers) > containerAlertMaxLogged {
+ fmt.Fprintf(§ion, "\n\n(+%d more unhealthy container(s), logs omitted)", len(containers)-containerAlertMaxLogged)
+ }
+
+ return section.String()
+}
+
+// buildContainerLogExcerpt filters raw container log output down to the lines
+// most likely to explain why the container is unhealthy: lines containing
+// "error" or "fatal" (case-insensitive) are preferred. If none match, the tail
+// of the raw output is used instead so the notification still carries context.
+func buildContainerLogExcerpt(raw string) string {
+ raw = strings.TrimSpace(raw)
+ if raw == "" {
+ return ""
+ }
+ lines := strings.Split(raw, "\n")
+
+ var matched []string
+ for _, line := range lines {
+ line = strings.TrimRight(line, "\r")
+ if line == "" {
+ continue
+ }
+ lower := strings.ToLower(line)
+ if strings.Contains(lower, "error") || strings.Contains(lower, "fatal") {
+ matched = append(matched, line)
+ }
+ }
+
+ selected := matched
+ if len(selected) == 0 {
+ start := max(0, len(lines)-containerLogFallbackLines)
+ selected = lines[start:]
+ } else if len(selected) > containerLogMaxLines {
+ selected = selected[len(selected)-containerLogMaxLines:]
+ }
+
+ excerpt := strings.TrimSpace(strings.Join(selected, "\n"))
+ if len(excerpt) > containerLogExcerptMaxChars {
+ excerpt = "…" + excerpt[len(excerpt)-containerLogExcerptMaxChars:]
+ }
+ return excerpt
+}
diff --git a/internal/alerts/alerts_container_test.go b/internal/alerts/alerts_container_test.go
new file mode 100644
index 00000000..ee5bdd49
--- /dev/null
+++ b/internal/alerts/alerts_container_test.go
@@ -0,0 +1,349 @@
+//go:build testing
+
+package alerts_test
+
+import (
+ "fmt"
+ "strings"
+ "testing"
+ "testing/synctest"
+ "time"
+
+ "github.com/henrygd/beszel/internal/alerts"
+ "github.com/henrygd/beszel/internal/entities/container"
+ "github.com/henrygd/beszel/internal/entities/system"
+ beszelTests "github.com/henrygd/beszel/internal/tests"
+ "github.com/pocketbase/pocketbase/core"
+ "github.com/stretchr/testify/assert"
+ "github.com/stretchr/testify/require"
+)
+
+type containerAlertTestFixture struct {
+ hub *beszelTests.TestHub
+ am *alerts.AlertManager
+ alertID string
+ systemRecord *core.Record
+}
+
+func newContainerAlertTestFixture(t *testing.T, min int) *containerAlertTestFixture {
+ t.Helper()
+
+ hub, user := beszelTests.GetHubWithUser(t)
+
+ systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
+ require.NoError(t, err)
+ systemRecord := systems[0]
+
+ userSettings, err := hub.FindFirstRecordByFilter("user_settings", "user={:user}", map[string]any{"user": user.Id})
+ require.NoError(t, err)
+ userSettings.Set("settings", `{"emails":["test@example.com"],"webhooks":[]}`)
+ require.NoError(t, hub.Save(userSettings))
+
+ alertRecord, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
+ "name": "ContainerHealth",
+ "system": systemRecord.Id,
+ "user": user.Id,
+ "min": min,
+ })
+ require.NoError(t, err)
+ assert.False(t, alertRecord.GetBool("triggered"), "Alert should not be triggered initially")
+
+ return &containerAlertTestFixture{
+ hub: hub,
+ am: alerts.NewTestAlertManagerWithoutWorker(hub),
+ alertID: alertRecord.Id,
+ systemRecord: systemRecord,
+ }
+}
+
+func (f *containerAlertTestFixture) cleanup() {
+ f.hub.Cleanup()
+}
+
+func (f *containerAlertTestFixture) submit(t *testing.T, containers []*container.Stats, fetchLogs alerts.FetchContainerLogsFunc) {
+ t.Helper()
+ data := &system.CombinedData{Containers: containers}
+ require.NoError(t, f.am.HandleContainerAlerts(f.systemRecord, data, fetchLogs))
+}
+
+func (f *containerAlertTestFixture) submitInvalid(t *testing.T) {
+ t.Helper()
+ require.NoError(t, f.am.HandleContainerAlerts(f.systemRecord, &system.CombinedData{}, nil))
+}
+
+func (f *containerAlertTestFixture) assertTriggered(t *testing.T, triggered bool, message string) {
+ t.Helper()
+ alertRecord, err := f.hub.FindRecordById("alerts", f.alertID)
+ require.NoError(t, err)
+ assert.Equal(t, triggered, alertRecord.GetBool("triggered"), message)
+}
+
+func (f *containerAlertTestFixture) assertPending(t *testing.T, pending bool) {
+ t.Helper()
+ alertRecord, err := f.hub.FindRecordById("alerts", f.alertID)
+ require.NoError(t, err)
+ assert.Equal(t, pending, !alertRecord.GetDateTime("pending_since").Time().IsZero())
+}
+
+func waitForContainerAlert(d time.Duration) {
+ time.Sleep(d)
+ synctest.Wait()
+}
+
+func healthyContainer(name string) *container.Stats {
+ return &container.Stats{Name: name, Id: "abc123def456", Health: container.DockerHealthHealthy}
+}
+
+func unhealthyContainer(name string) *container.Stats {
+ return &container.Stats{Name: name, Id: "abc123def456", Health: container.DockerHealthUnhealthy}
+}
+
+func TestContainerHealthAlertTriggersAndResolves(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 1)
+ defer fixture.cleanup()
+
+ synctest.Test(t, func(t *testing.T) {
+ fixture.submit(t, []*container.Stats{unhealthyContainer("web")}, nil)
+
+ fixture.assertTriggered(t, true, "A one-minute alert should trigger on the first unhealthy update")
+ require.Equal(t, 1, fixture.hub.TestMailer.TotalSend(), "An email should have been sent")
+
+ msg := fixture.hub.TestMailer.LastMessage()
+ assert.Contains(t, msg.Subject, "web", "Subject should name the unhealthy container")
+ assert.Contains(t, strings.ToLower(msg.Subject), "unhealthy")
+
+ fixture.submit(t, []*container.Stats{unhealthyContainer("web")}, nil)
+ fixture.assertPending(t, false)
+
+ fixture.submitInvalid(t)
+ fixture.assertTriggered(t, true, "An invalid container snapshot should not resolve the alert")
+ assert.Equal(t, 1, fixture.hub.TestMailer.TotalSend(), "An invalid snapshot should not send a recovery")
+
+ fixture.submit(t, []*container.Stats{}, nil)
+ waitForContainerAlert(time.Second)
+
+ fixture.assertTriggered(t, false, "Alert should resolve once the container is healthy again")
+ assert.Equal(t, 2, fixture.hub.TestMailer.TotalSend(), "A second email should have been sent for the recovery")
+ assert.Contains(t, fixture.hub.TestMailer.LastMessage().Subject, " healthy")
+ })
+}
+
+func TestContainerHealthAlertInvalidSnapshotCancelsPending(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 5)
+ defer fixture.cleanup()
+
+ synctest.Test(t, func(t *testing.T) {
+ fixture.submit(t, []*container.Stats{unhealthyContainer("db")}, nil)
+ fixture.assertPending(t, true)
+ waitForContainerAlert(time.Minute)
+ fixture.submitInvalid(t)
+ fixture.assertPending(t, false)
+ waitForContainerAlert(10 * time.Minute)
+ fixture.submit(t, []*container.Stats{unhealthyContainer("db")}, nil)
+
+ fixture.assertTriggered(t, false, "Stale unhealthy data should not trigger an alert")
+ fixture.assertPending(t, true)
+ assert.Equal(t, 0, fixture.hub.TestMailer.TotalSend())
+ })
+}
+
+func TestContainerHealthAlertSystemDownCancelsPending(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 5)
+ defer fixture.cleanup()
+
+ // Use the hub's alert manager because the system-manager status hook invokes
+ // cancellation on that instance.
+ am := fixture.hub.GetAlertManager()
+ require.NoError(t, am.HandleContainerAlerts(
+ fixture.systemRecord,
+ &system.CombinedData{Containers: []*container.Stats{unhealthyContainer("db")}},
+ nil,
+ ))
+ fixture.assertPending(t, true)
+
+ fixture.systemRecord.Set("status", "down")
+ require.NoError(t, fixture.hub.Save(fixture.systemRecord))
+
+ fixture.assertPending(t, false)
+}
+
+func TestContainerHealthAlertResolvesBeforeMinDelayCancelsPending(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 5)
+ defer fixture.cleanup()
+
+ synctest.Test(t, func(t *testing.T) {
+ fixture.submit(t, []*container.Stats{unhealthyContainer("db")}, nil)
+ waitForContainerAlert(time.Minute)
+
+ fixture.assertTriggered(t, false, "Alert should not fire until the min delay elapses")
+ fixture.assertPending(t, true)
+ assert.Equal(t, 0, fixture.hub.TestMailer.TotalSend())
+
+ // container recovers before the 5 minute delay elapses
+ fixture.submit(t, []*container.Stats{healthyContainer("db")}, nil)
+ waitForContainerAlert(10 * time.Minute)
+ fixture.submit(t, []*container.Stats{healthyContainer("db")}, nil)
+
+ fixture.assertTriggered(t, false, "Alert should remain untriggered")
+ fixture.assertPending(t, false)
+ assert.Equal(t, 0, fixture.hub.TestMailer.TotalSend(), "No email should be sent for a container that recovered before the delay")
+ })
+}
+
+func TestContainerHealthAlertPreservesPendingDurationAcrossManagerRestart(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 2)
+ defer fixture.cleanup()
+
+ synctest.Test(t, func(t *testing.T) {
+ fixture.submit(t, []*container.Stats{unhealthyContainer("db")}, nil)
+ waitForContainerAlert(30 * time.Second)
+
+ restarted := alerts.NewTestAlertManagerWithoutWorker(fixture.hub)
+ waitForContainerAlert(91 * time.Second)
+ require.NoError(t, restarted.HandleContainerAlerts(
+ fixture.systemRecord,
+ &system.CombinedData{Containers: []*container.Stats{unhealthyContainer("db")}},
+ nil,
+ ))
+
+ fixture.assertTriggered(t, true, "Restart should preserve the original unhealthy start time")
+ fixture.assertPending(t, false)
+ assert.Equal(t, 1, fixture.hub.TestMailer.TotalSend())
+ })
+}
+
+func TestContainerHealthAlertClaimsPendingTimestampAtDatabasePrecision(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 1)
+ defer fixture.cleanup()
+
+ alertRecord, err := fixture.hub.FindRecordById("alerts", fixture.alertID)
+ require.NoError(t, err)
+ // PocketBase persists dates to milliseconds, while record update hooks can
+ // retain the original sub-millisecond value in the in-memory alert cache.
+ alertRecord.Set("pending_since", time.Now().UTC().Add(-2*time.Minute).Truncate(time.Millisecond).Add(123*time.Nanosecond))
+ require.NoError(t, fixture.hub.Save(alertRecord))
+
+ fixture.submit(t, []*container.Stats{unhealthyContainer("db")}, nil)
+
+ fixture.assertTriggered(t, true, "Equivalent persisted and cached timestamps should claim the alert")
+ fixture.assertPending(t, false)
+ assert.Equal(t, 1, fixture.hub.TestMailer.TotalSend())
+}
+
+func TestContainerHealthAlertRecoveryWhileFetchingLogsCancelsDelivery(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 1)
+ defer fixture.cleanup()
+
+ synctest.Test(t, func(t *testing.T) {
+ fetchLogs := func(containerID string) (string, error) {
+ fixture.submit(t, []*container.Stats{healthyContainer("api")}, nil)
+ return "FATAL stale failure", nil
+ }
+ fixture.submit(t, []*container.Stats{unhealthyContainer("api")}, fetchLogs)
+
+ fixture.assertTriggered(t, false, "Recovery should cancel delivery while logs are fetched")
+ fixture.assertPending(t, false)
+ assert.Equal(t, 0, fixture.hub.TestMailer.TotalSend())
+ })
+}
+
+func TestContainerHealthAlertIncludesLogExcerpt(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 1)
+ defer fixture.cleanup()
+
+ rawLogs := strings.Join([]string{
+ "2026-08-16T10:00:00Z booting",
+ "2026-08-16T10:00:01Z ERROR could not reach upstream",
+ "2026-08-16T10:00:02Z FATAL giving up after 3 retries",
+ }, "\n")
+ fetchLogs := func(containerID string) (string, error) {
+ assert.Equal(t, "abc123def456", containerID)
+ return rawLogs, nil
+ }
+
+ synctest.Test(t, func(t *testing.T) {
+ fixture.submit(t, []*container.Stats{unhealthyContainer("api")}, fetchLogs)
+
+ fixture.assertTriggered(t, true, "Alert should be triggered")
+ require.Equal(t, 1, fixture.hub.TestMailer.TotalSend())
+
+ body := fixture.hub.TestMailer.LastMessage().Text
+ assert.Contains(t, body, "could not reach upstream")
+ assert.Contains(t, body, "giving up after 3 retries")
+ assert.NotContains(t, body, "booting", "non error/fatal lines should be dropped when matches exist")
+ })
+}
+
+func TestContainerHealthAlertSkipsLogsOnFetchError(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 1)
+ defer fixture.cleanup()
+
+ fetchLogs := func(containerID string) (string, error) {
+ return "", fmt.Errorf("agent unreachable")
+ }
+
+ synctest.Test(t, func(t *testing.T) {
+ fixture.submit(t, []*container.Stats{unhealthyContainer("api")}, fetchLogs)
+
+ fixture.assertTriggered(t, true, "Alert should still be triggered even if logs can't be fetched")
+ require.Equal(t, 1, fixture.hub.TestMailer.TotalSend())
+ })
+}
+
+func TestContainerHealthAlertCapsLogFetchAttempts(t *testing.T) {
+ fixture := newContainerAlertTestFixture(t, 1)
+ defer fixture.cleanup()
+
+ containers := make([]*container.Stats, 100)
+ for i := range containers {
+ containers[i] = &container.Stats{
+ Name: fmt.Sprintf("container-%d", i),
+ Id: fmt.Sprintf("id-%d", i),
+ Health: container.DockerHealthUnhealthy,
+ }
+ }
+ attempts := 0
+ fetchLogs := func(containerID string) (string, error) {
+ attempts++
+ return "", fmt.Errorf("agent unreachable")
+ }
+
+ synctest.Test(t, func(t *testing.T) {
+ fixture.submit(t, containers, fetchLogs)
+
+ fixture.assertTriggered(t, true, "Alert should still fire when log retrieval fails")
+ assert.Equal(t, 2, attempts, "Log retrieval should attempt at most two containers")
+ require.Equal(t, 1, fixture.hub.TestMailer.TotalSend())
+ })
+}
+
+func TestBuildContainerLogExcerptPrefersErrorAndFatalLines(t *testing.T) {
+ raw := strings.Join([]string{
+ "2026-08-16T10:00:00Z starting up",
+ "2026-08-16T10:00:01Z listening on :8080",
+ "2026-08-16T10:00:02Z ERROR failed to connect to db",
+ "2026-08-16T10:00:03Z retrying connection",
+ "2026-08-16T10:00:04Z FATAL could not recover, exiting",
+ }, "\n")
+
+ excerpt := alerts.BuildContainerLogExcerpt(raw)
+ assert.Contains(t, excerpt, "failed to connect to db")
+ assert.Contains(t, excerpt, "could not recover, exiting")
+ assert.NotContains(t, excerpt, "starting up", "non-matching lines should be dropped when error/fatal lines exist")
+}
+
+func TestBuildContainerLogExcerptFallsBackToTailWhenNoMatches(t *testing.T) {
+ var lines []string
+ for i := range 20 {
+ lines = append(lines, fmt.Sprintf("line %d: all good here", i))
+ }
+ raw := strings.Join(lines, "\n")
+
+ excerpt := alerts.BuildContainerLogExcerpt(raw)
+ assert.Contains(t, excerpt, "line 19", "should keep the tail of the output")
+ assert.NotContains(t, excerpt, "line 0:", "should not keep the very start when falling back to a short tail")
+}
+
+func TestBuildContainerLogExcerptEmpty(t *testing.T) {
+ assert.Equal(t, "", alerts.BuildContainerLogExcerpt(" \n \n"))
+}
diff --git a/internal/alerts/alerts_system.go b/internal/alerts/alerts_system.go
index edba9454..d3801eac 100644
--- a/internal/alerts/alerts_system.go
+++ b/internal/alerts/alerts_system.go
@@ -47,7 +47,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
return nil
}
- alerts := am.alertsCache.GetAlertsExcludingNames(systemRecord.Id, "Status", alertNameSystemdFailed)
+ alerts := am.alertsCache.GetAlertsExcludingNames(systemRecord.Id, "Status", alertNameSystemdFailed, containerAlertName)
if len(alerts) == 0 {
return nil
}
diff --git a/internal/alerts/alerts_test_helpers.go b/internal/alerts/alerts_test_helpers.go
index 017f3edb..a7449b85 100644
--- a/internal/alerts/alerts_test_helpers.go
+++ b/internal/alerts/alerts_test_helpers.go
@@ -103,3 +103,8 @@ func (am *AlertManager) SetAlertTriggered(alert CachedAlertData, triggered bool)
func IsInternalURL(rawURL string) (bool, error) {
return isInternalURL(rawURL)
}
+
+// BuildContainerLogExcerpt exposes buildContainerLogExcerpt for testing.
+func BuildContainerLogExcerpt(raw string) string {
+ return buildContainerLogExcerpt(raw)
+}
diff --git a/internal/entities/system/system_test.go b/internal/entities/system/system_test.go
index f75fecd6..7a7f0049 100644
--- a/internal/entities/system/system_test.go
+++ b/internal/entities/system/system_test.go
@@ -6,6 +6,7 @@ import (
"testing"
"github.com/fxamacker/cbor/v2"
+ "github.com/henrygd/beszel/internal/entities/container"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
@@ -85,3 +86,34 @@ func TestCombinedDataSystemdUpdateMarkerTransport(t *testing.T) {
require.NoError(t, json.Unmarshal([]byte(`{"stats":{},"info":{},"container":[]}`), &legacy))
assert.False(t, legacy.SystemdServicesUpdated)
}
+
+func TestCombinedDataContainerValidityTransport(t *testing.T) {
+ validEmpty := CombinedData{Containers: []*container.Stats{}}
+
+ jsonData, err := json.Marshal(validEmpty)
+ require.NoError(t, err)
+ var decodedJSON CombinedData
+ require.NoError(t, json.Unmarshal(jsonData, &decodedJSON))
+ assert.NotNil(t, decodedJSON.Containers)
+ assert.Empty(t, decodedJSON.Containers)
+
+ jsonV2Data, err := jsonv2.Marshal(validEmpty)
+ require.NoError(t, err)
+ var decodedJSONV2 CombinedData
+ require.NoError(t, jsonv2.Unmarshal(jsonV2Data, &decodedJSONV2))
+ assert.NotNil(t, decodedJSONV2.Containers)
+ assert.Empty(t, decodedJSONV2.Containers)
+
+ cborData, err := cbor.Marshal(validEmpty)
+ require.NoError(t, err)
+ var decodedCBOR CombinedData
+ require.NoError(t, cbor.Unmarshal(cborData, &decodedCBOR))
+ assert.NotNil(t, decodedCBOR.Containers)
+ assert.Empty(t, decodedCBOR.Containers)
+
+ invalidData, err := cbor.Marshal(CombinedData{})
+ require.NoError(t, err)
+ var decodedInvalid CombinedData
+ require.NoError(t, cbor.Unmarshal(invalidData, &decodedInvalid))
+ assert.Nil(t, decodedInvalid.Containers)
+}
diff --git a/internal/hub/systems/system_manager.go b/internal/hub/systems/system_manager.go
index 8be62379..5c04ea00 100644
--- a/internal/hub/systems/system_manager.go
+++ b/internal/hub/systems/system_manager.go
@@ -58,7 +58,9 @@ type hubLike interface {
GetSSHKey(dataDir string) (ssh.Signer, error)
HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error
HandleStatusAlerts(status string, systemRecord *core.Record) error
+ HandleContainerAlerts(systemRecord *core.Record, data *system.CombinedData, fetchLogs func(containerID string) (string, error)) error
CancelPendingStatusAlerts(systemID string)
+ CancelPendingContainerAlerts(systemID string)
}
// NewSystemManager creates a new SystemManager instance with the provided hub.
@@ -189,7 +191,7 @@ func (sm *SystemManager) onRecordUpdate(e *core.RecordEvent) error {
// - paused: Closes SSH connection and deactivates alerts
// - pending: Starts monitoring (reuses WebSocket if available)
// - up: Triggers system alerts
-// - down: Triggers status change alerts
+// - down: Cancels pending container alerts and triggers status change alerts
func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
newStatus := e.Record.GetString("status")
prevStatus := pending
@@ -207,6 +209,7 @@ func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
}
_ = deactivateAlerts(e.App, e.Record.Id)
sm.hub.CancelPendingStatusAlerts(e.Record.Id)
+ sm.hub.CancelPendingContainerAlerts(e.Record.Id)
return e.Next()
case pending:
// Resume monitoring, preferring existing WebSocket connection
@@ -220,6 +223,10 @@ func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
}
_ = deactivateAlerts(e.App, e.Record.Id)
return e.Next()
+ case down:
+ // Docker state is unknown while the system is unreachable. Do not let a
+ // delayed container-health alert fire from the last received snapshot.
+ sm.hub.CancelPendingContainerAlerts(e.Record.Id)
}
// Handle systems not in manager
@@ -232,6 +239,9 @@ func (sm *SystemManager) onRecordAfterUpdateSuccess(e *core.RecordEvent) error {
if err := sm.hub.HandleSystemAlerts(e.Record, system.data); err != nil {
e.App.Logger().Error("Error handling system alerts", "err", err)
}
+ if err := sm.hub.HandleContainerAlerts(e.Record, system.data, system.FetchContainerLogsFromAgent); err != nil {
+ e.App.Logger().Error("Error handling container alerts", "err", err)
+ }
}
// Trigger status change alerts for up/down transitions
diff --git a/internal/hub/systems/system_smart_test.go b/internal/hub/systems/system_smart_test.go
index 7b47d770..aa98633d 100644
--- a/internal/hub/systems/system_smart_test.go
+++ b/internal/hub/systems/system_smart_test.go
@@ -29,7 +29,11 @@ func (stubHub) HandleSystemAlerts(systemRecord *core.Record, data *esystem.Combi
return nil
}
func (stubHub) HandleStatusAlerts(status string, systemRecord *core.Record) error { return nil }
-func (stubHub) CancelPendingStatusAlerts(systemID string) {}
+func (stubHub) HandleContainerAlerts(systemRecord *core.Record, data *esystem.CombinedData, fetchLogs func(containerID string) (string, error)) error {
+ return nil
+}
+func (stubHub) CancelPendingStatusAlerts(systemID string) {}
+func (stubHub) CancelPendingContainerAlerts(systemID string) {}
// newTestSystemWithHub creates a System backed by a real (temp) database, along
// with a matching "systems" record, for tests that need to exercise DB reads/writes.
diff --git a/internal/migrations/0_collections_snapshot_0_19_0_dev_1.go b/internal/migrations/0_collections_snapshot_0_19_0_dev_2.go
similarity index 99%
rename from internal/migrations/0_collections_snapshot_0_19_0_dev_1.go
rename to internal/migrations/0_collections_snapshot_0_19_0_dev_2.go
index b14d1cd8..54b4c7e2 100644
--- a/internal/migrations/0_collections_snapshot_0_19_0_dev_1.go
+++ b/internal/migrations/0_collections_snapshot_0_19_0_dev_2.go
@@ -79,7 +79,8 @@ func init() {
"LoadAvg1",
"LoadAvg5",
"LoadAvg15",
- "Battery"
+ "Battery",
+ "ContainerHealth"
]
},
{
@@ -115,6 +116,17 @@ func init() {
"system": false,
"type": "bool"
},
+ {
+ "hidden": true,
+ "id": "date1302749137",
+ "max": "",
+ "min": "",
+ "name": "pending_since",
+ "presentable": false,
+ "required": false,
+ "system": false,
+ "type": "date"
+ },
{
"hidden": false,
"id": "autodate2990389176",
diff --git a/internal/site/src/components/alerts/alerts-sheet.tsx b/internal/site/src/components/alerts/alerts-sheet.tsx
index 8e73c6fa..e5efeeb2 100644
--- a/internal/site/src/components/alerts/alerts-sheet.tsx
+++ b/internal/site/src/components/alerts/alerts-sheet.tsx
@@ -411,6 +411,7 @@ export function AlertContent({
)}
+ {checked && alertData.note && {alertData.note()}}
)}
diff --git a/internal/site/src/lib/alerts.ts b/internal/site/src/lib/alerts.ts
index b193bfb8..71ea0f88 100644
--- a/internal/site/src/lib/alerts.ts
+++ b/internal/site/src/lib/alerts.ts
@@ -1,5 +1,5 @@
import { t } from "@lingui/core/macro"
-import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerCrashIcon, ServerIcon } from "lucide-react"
+import { ContainerIcon, CpuIcon, HardDriveIcon, MemoryStickIcon, ServerCrashIcon, ServerIcon } from "lucide-react"
import type { RecordSubscription } from "pocketbase"
import { EthernetIcon, GpuIcon } from "@/components/ui/icons"
import { $alerts } from "@/lib/stores"
@@ -104,6 +104,16 @@ export const alertInfo: Record = {
start: 20,
invert: true,
},
+ ContainerHealth: {
+ name: () => t`Container Health`,
+ unit: "",
+ icon: ContainerIcon,
+ desc: () => t`Triggers when a Docker container's health check reports unhealthy`,
+ note: () =>
+ t`Notifications may include recent container log excerpts.`,
+ /** "for x minutes" is appended to desc when only one value */
+ singleDesc: () => `${t`Container`} ${t`Unhealthy`}`,
+ },
SystemdFailed: {
name: () => t`Failed Services`,
unit: "",
diff --git a/internal/site/src/locales/ar/ar.po b/internal/site/src/locales/ar/ar.po
index 98004fc9..ccc46bb7 100644
--- a/internal/site/src/locales/ar/ar.po
+++ b/internal/site/src/locales/ar/ar.po
@@ -424,6 +424,14 @@ msgstr "التعارضات"
msgid "Connection is down"
msgstr "الاتصال مقطوع"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "حاوية"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "صحة الحاوية"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "حاويات"
@@ -1749,6 +1757,10 @@ msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز متوسط التحميل لمدة 5 دقائق عتبة معينة"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "يتم التفعيل عندما يُبلّغ فحص سلامة حاوية Docker بأنها غير سليمة"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "يتم التفعيل عندما يتجاوز أي مستشعر عتبة معينة"
@@ -1795,6 +1807,10 @@ msgstr "يتم التفعيل عندما يتجاوز استخدام أي قرص
msgid "Type"
msgstr "النوع"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "غير سليمة"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "ملف الوحدة"
diff --git a/internal/site/src/locales/bg/bg.po b/internal/site/src/locales/bg/bg.po
index 90a8d94b..9fd743f6 100644
--- a/internal/site/src/locales/bg/bg.po
+++ b/internal/site/src/locales/bg/bg.po
@@ -424,6 +424,14 @@ msgstr "Конфликти"
msgid "Connection is down"
msgstr "Връзката е прекъсната"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Контейнер"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Здраве на контейнера"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Контейнери"
@@ -1749,6 +1757,10 @@ msgstr "Задейства се, когато употребата на паме
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Задейства се, когато употребата на паметта за 5 минута надвиши зададен праг"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Задейства се, когато проверката за състояние на Docker контейнер докладва нездравословно състояние"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Задейства се, когато някой даден сензор надвиши зададен праг"
@@ -1795,6 +1807,10 @@ msgstr "Задейства се, когато употребата на няко
msgid "Type"
msgstr "Тип"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Нездрав"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Файл на единица"
diff --git a/internal/site/src/locales/cs/cs.po b/internal/site/src/locales/cs/cs.po
index 0edfe5d3..5317d8cf 100644
--- a/internal/site/src/locales/cs/cs.po
+++ b/internal/site/src/locales/cs/cs.po
@@ -424,6 +424,14 @@ msgstr "Konflikty"
msgid "Connection is down"
msgstr "Připojení je nedostupné"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Kontejner"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Zdraví kontejneru"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Kontejnery"
@@ -1749,6 +1757,10 @@ msgstr "Spustí se, když využití paměti během 15 minut překročí prahovou
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Spustí se, když využití paměti během 5 minut překročí prahovou hodnotu"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Spouští se, když kontrola stavu (health check) kontejneru Dockeru nahlásí stav nezdravý"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Spustí se, když některý senzor překročí prahovou hodnotu"
@@ -1795,6 +1807,10 @@ msgstr "Spustí se, když využití disku překročí prahovou hodnotu"
msgid "Type"
msgstr "Typ"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Nezdravý"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Soubor jednotky"
diff --git a/internal/site/src/locales/da/da.po b/internal/site/src/locales/da/da.po
index df1333c3..c45c3e2d 100644
--- a/internal/site/src/locales/da/da.po
+++ b/internal/site/src/locales/da/da.po
@@ -424,6 +424,14 @@ msgstr "Konflikter"
msgid "Connection is down"
msgstr "Forbindelsen er nede"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Container"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Container-sundhed"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Containere"
@@ -1749,6 +1757,10 @@ msgstr "Udløser når 15 minut belastning gennemsnit overstiger en tærskel"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Udløser når 5 minut belastning gennemsnit overstiger en tærskel"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Udløser når en Docker-containers helbredstjek rapporterer usund"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Udløser når en sensor overstiger en tærskel"
@@ -1795,6 +1807,10 @@ msgstr "Udløser når brugen af en disk overstiger en tærskel"
msgid "Type"
msgstr "Type"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Usund"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Enhed fil"
diff --git a/internal/site/src/locales/de/de.po b/internal/site/src/locales/de/de.po
index 9b0570ea..26ba0037 100644
--- a/internal/site/src/locales/de/de.po
+++ b/internal/site/src/locales/de/de.po
@@ -424,6 +424,14 @@ msgstr "Konflikte"
msgid "Connection is down"
msgstr "Verbindung unterbrochen"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Container"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Container-Gesundheit"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Container"
@@ -1749,6 +1757,10 @@ msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 15 Minuten einen Schwel
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Löst aus, wenn der Lastdurchschnitt der letzten 5 Minuten einen Schwellenwert überschreitet"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Löst aus, wenn der Healthcheck eines Docker-Containers als ungesund gemeldet wird"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Löst aus, wenn ein Sensor einen Schwellenwert überschreitet"
@@ -1795,6 +1807,10 @@ msgstr "Löst aus, wenn die Nutzung einer Festplatte einen Schwellenwert übersc
msgid "Type"
msgstr "Typ"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Ungesund"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Unit-Datei"
diff --git a/internal/site/src/locales/en/en.po b/internal/site/src/locales/en/en.po
index ae0bfd81..58b51314 100644
--- a/internal/site/src/locales/en/en.po
+++ b/internal/site/src/locales/en/en.po
@@ -419,6 +419,14 @@ msgstr "Conflicts"
msgid "Connection is down"
msgstr "Connection is down"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Container"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Container Health"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Containers"
@@ -1744,6 +1752,10 @@ msgstr "Triggers when 15 minute load average exceeds a threshold"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Triggers when 5 minute load average exceeds a threshold"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Triggers when a Docker container's health check reports unhealthy"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Triggers when any sensor exceeds a threshold"
@@ -1790,6 +1802,10 @@ msgstr "Triggers when usage of any disk exceeds a threshold"
msgid "Type"
msgstr "Type"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Unhealthy"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Unit file"
diff --git a/internal/site/src/locales/es/es.po b/internal/site/src/locales/es/es.po
index 7a2f020a..4f66c932 100644
--- a/internal/site/src/locales/es/es.po
+++ b/internal/site/src/locales/es/es.po
@@ -424,6 +424,14 @@ msgstr "Conflictos"
msgid "Connection is down"
msgstr "La conexión está caída"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Contenedor"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Estado del contenedor"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Contenedores"
@@ -1749,6 +1757,10 @@ msgstr "Se activa cuando la carga media de 15 minutos supera un umbral"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Se activa cuando la carga media de 5 minutos supera un umbral"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Se activa cuando el healthcheck de un contenedor Docker indica un estado no saludable"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Se activa cuando cualquier sensor supera un umbral"
@@ -1795,6 +1807,10 @@ msgstr "Se activa cuando el uso de cualquier disco supera un umbral"
msgid "Type"
msgstr "Tipo"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "No saludable"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Archivo de unidad"
diff --git a/internal/site/src/locales/fa/fa.po b/internal/site/src/locales/fa/fa.po
index 61ad9ffe..081b2b20 100644
--- a/internal/site/src/locales/fa/fa.po
+++ b/internal/site/src/locales/fa/fa.po
@@ -424,6 +424,14 @@ msgstr "تعارضها"
msgid "Connection is down"
msgstr "اتصال قطع است"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "کانتینر"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "سلامت کانتینر"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "کانتینرها"
@@ -1749,6 +1757,10 @@ msgstr "هنگامی که میانگین بار ۱۵ دقیقهای از یک
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "هنگامی که میانگین بار ۵ دقیقهای از یک آستانه فراتر رود، فعال میشود"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "هنگامی که بررسی سلامت یک کانتینر Docker وضعیت ناسالم را گزارش میکند، فعال میشود"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "هنگامی که هر حسگری از یک آستانه فراتر رود، فعال میشود"
@@ -1795,6 +1807,10 @@ msgstr "هنگامی که استفاده از هر دیسکی از یک آستا
msgid "Type"
msgstr "نوع"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "ناسالم"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "فایل واحد"
diff --git a/internal/site/src/locales/fr/fr.po b/internal/site/src/locales/fr/fr.po
index 14bb109c..d40c3cc0 100644
--- a/internal/site/src/locales/fr/fr.po
+++ b/internal/site/src/locales/fr/fr.po
@@ -424,6 +424,14 @@ msgstr "Conflits"
msgid "Connection is down"
msgstr "Connexion interrompue"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Conteneur"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Santé du conteneur"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Conteneurs"
@@ -1749,6 +1757,10 @@ msgstr "Se déclenche lorsque la charge moyenne sur 15 minutes dépasse un seuil
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Se déclenche lorsque la charge moyenne sur 5 minutes dépasse un seuil"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Se déclenche lorsque le contrôle de santé (healthcheck) d'un conteneur Docker le signale comme non sain"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Déclenchement lorsque tout capteur dépasse un seuil"
@@ -1795,6 +1807,10 @@ msgstr "Déclenchement lorsque l'utilisation de tout disque dépasse un seuil"
msgid "Type"
msgstr "Type"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Non sain"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Fichier unité"
diff --git a/internal/site/src/locales/he/he.po b/internal/site/src/locales/he/he.po
index 39722223..ced20850 100644
--- a/internal/site/src/locales/he/he.po
+++ b/internal/site/src/locales/he/he.po
@@ -424,6 +424,14 @@ msgstr "התנגשויות"
msgid "Connection is down"
msgstr "החיבור נפל"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "קונטיינר"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "בריאות הקונטיינר"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "קונטיינרים"
@@ -1749,6 +1757,10 @@ msgstr "מופעל כאשר ממוצע העומס ל-15 דקות עולה על
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "מופעל כאשר ממוצע העומס ל-5 דקות עולה על סף"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "מופעל כאשר בדיקת התקינות (health check) של קונטיינר Docker מדווחת שהוא לא תקין"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "מופעל כאשר כל חיישן עולה על סף"
@@ -1795,6 +1807,10 @@ msgstr "מופעל כאשר שימוש בכל דיסק עולה על סף"
msgid "Type"
msgstr "סוג"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "לא תקין"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "קובץ יחידה"
diff --git a/internal/site/src/locales/hr/hr.po b/internal/site/src/locales/hr/hr.po
index 46ece493..5bf8c29e 100644
--- a/internal/site/src/locales/hr/hr.po
+++ b/internal/site/src/locales/hr/hr.po
@@ -424,6 +424,14 @@ msgstr "Sukobi"
msgid "Connection is down"
msgstr "Veza je pala"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Kontejner"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Zdravlje kontejnera"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Kontejneri"
@@ -1749,6 +1757,10 @@ msgstr "Pokreće se kada prosječna opterećenost sustava unutar 15 minuta prije
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Pokreće se kada prosječna opterećenost sustava unutar 5 minuta prijeđe prag"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Pokreće se kada provjera zdravlja Docker kontejnera prijavi da je nezdrav"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Pokreće se kada bilo koji senzor prijeđe prag"
@@ -1795,6 +1807,10 @@ msgstr "Pokreće se kada iskorištenost bilo kojeg diska premaši prag"
msgid "Type"
msgstr "Vrsta"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Nezdrav"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Datoteka jedinice"
diff --git a/internal/site/src/locales/hu/hu.po b/internal/site/src/locales/hu/hu.po
index 00a537bf..defae92d 100644
--- a/internal/site/src/locales/hu/hu.po
+++ b/internal/site/src/locales/hu/hu.po
@@ -424,6 +424,14 @@ msgstr "Konfliktusok"
msgid "Connection is down"
msgstr "Kapcsolat megszakadt"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Konténer"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Konténer állapota"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Konténerek"
@@ -1749,6 +1757,10 @@ msgstr "Riaszt, ha a 15 perces terhelési átlag túllép egy küszöbértéket"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Riaszt, ha az 5 perces terhelési átlag túllép egy küszöbértéket"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Riaszt, amikor egy Docker konténer health check ellenőrzése nem egészséges állapotot jelez"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Riaszt, ha bármelyik hőmérséklet érzékelő túllép egy küszöbértéket"
@@ -1795,6 +1807,10 @@ msgstr "Riaszt, ha a lemezhasználat túllép egy küszöbértéket"
msgid "Type"
msgstr "Típus"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Nem egészséges"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Egység fájl"
diff --git a/internal/site/src/locales/id/id.po b/internal/site/src/locales/id/id.po
index b38e30a0..320ff05e 100644
--- a/internal/site/src/locales/id/id.po
+++ b/internal/site/src/locales/id/id.po
@@ -424,6 +424,14 @@ msgstr "Konflik"
msgid "Connection is down"
msgstr "Koneksi terputus"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Kontainer"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Kesehatan Kontainer"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Kontainer"
@@ -1749,6 +1757,10 @@ msgstr "Dipicu ketika rata-rata beban 15 menit melebihi ambang batas"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Dipicu ketika rata-rata beban 5 menit melebihi ambang batas"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Dipicu ketika pemeriksaan kesehatan (health check) kontainer Docker melaporkan kondisi tidak sehat"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Dipicu ketika sensor apa pun melebihi ambang batas"
@@ -1795,6 +1807,10 @@ msgstr "Dipicu ketika penggunaan disk apa pun melebihi ambang batas"
msgid "Type"
msgstr "Tipe"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Tidak sehat"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "File unit"
diff --git a/internal/site/src/locales/it/it.po b/internal/site/src/locales/it/it.po
index 3b61d4b5..408c6a64 100644
--- a/internal/site/src/locales/it/it.po
+++ b/internal/site/src/locales/it/it.po
@@ -424,6 +424,14 @@ msgstr "Conflitti"
msgid "Connection is down"
msgstr "La connessione è interrotta"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Container"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Stato del container"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Container"
@@ -1749,6 +1757,10 @@ msgstr "Si attiva quando la media di carico di 15 minuti supera una soglia"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Si attiva quando la media di carico di 5 minuti supera una soglia"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Si attiva quando il controllo di integrità (health check) di un container Docker restituisce uno stato non sano"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Attiva quando un sensore supera una soglia"
@@ -1795,6 +1807,10 @@ msgstr "Attiva quando l'utilizzo di un disco supera una soglia"
msgid "Type"
msgstr "Tipo"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Non sano"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "File unit"
diff --git a/internal/site/src/locales/ja/ja.po b/internal/site/src/locales/ja/ja.po
index 148506a8..c319ad8e 100644
--- a/internal/site/src/locales/ja/ja.po
+++ b/internal/site/src/locales/ja/ja.po
@@ -424,6 +424,14 @@ msgstr "競合"
msgid "Connection is down"
msgstr "接続が切断されました"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "コンテナ"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "コンテナのヘルス"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "コンテナ"
@@ -1749,6 +1757,10 @@ msgstr "15分間の負荷平均がしきい値を超えたときにトリガー
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "5分間の負荷平均がしきい値を超えたときにトリガーされます"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Dockerコンテナのヘルスチェックが異常を報告したときにトリガーされます"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "センサーがしきい値を超えたときにトリガーされます"
@@ -1795,6 +1807,10 @@ msgstr "ディスクの使用量がしきい値を超えたときにトリガー
msgid "Type"
msgstr "タイプ"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "異常"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "ユニットファイル"
diff --git a/internal/site/src/locales/ko/ko.po b/internal/site/src/locales/ko/ko.po
index 26fdf8f1..32e9bae5 100644
--- a/internal/site/src/locales/ko/ko.po
+++ b/internal/site/src/locales/ko/ko.po
@@ -424,6 +424,14 @@ msgstr "충돌"
msgid "Connection is down"
msgstr "연결이 끊겼습니다"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "컨테이너"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "컨테이너 상태"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "컨테이너"
@@ -1749,6 +1757,10 @@ msgstr "15분 부하 평균이 임계값을 초과하면 트리거됩니다."
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "5분 부하 평균이 임계값을 초과하면 트리거됩니다."
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Docker 컨테이너의 상태 확인(health check)에서 비정상 상태가 보고되면 트리거됩니다"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "센서가 임계값을 초과할 때 트리거됩니다."
@@ -1795,6 +1807,10 @@ msgstr "디스크 사용량이 임계값을 초과할 때 트리거됩니다."
msgid "Type"
msgstr "유형"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "비정상"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Unit 파일"
diff --git a/internal/site/src/locales/nl/nl.po b/internal/site/src/locales/nl/nl.po
index b57b1489..ddcb29ca 100644
--- a/internal/site/src/locales/nl/nl.po
+++ b/internal/site/src/locales/nl/nl.po
@@ -424,6 +424,14 @@ msgstr "Conflicten"
msgid "Connection is down"
msgstr "Verbinding is niet actief"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Container"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Containergezondheid"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr ""
@@ -1749,6 +1757,10 @@ msgstr "Triggert wanneer de 15 minuten gemiddelde belasting een drempelwaarde ov
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Triggert wanneer de 5 minuten gemiddelde belasting een drempelwaarde overschrijdt"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Triggert wanneer de health check van een Docker-container ongezond rapporteert"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Triggert wanneer een sensor een drempelwaarde overschrijdt"
@@ -1795,6 +1807,10 @@ msgstr "Triggert wanneer het gebruik van een schijf een drempelwaarde overschrij
msgid "Type"
msgstr "Type"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Ongezond"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Unit-bestand"
diff --git a/internal/site/src/locales/no/no.po b/internal/site/src/locales/no/no.po
index 58f7dfaf..d94cb6e8 100644
--- a/internal/site/src/locales/no/no.po
+++ b/internal/site/src/locales/no/no.po
@@ -424,6 +424,14 @@ msgstr "Konflikter"
msgid "Connection is down"
msgstr "Tilkoblingen er nede"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Container"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Containerhelse"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Containere"
@@ -1749,6 +1757,10 @@ msgstr "Slår inn når gjennomsnittsbelastningen over 15 minutter overstiger en
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Slår inn når gjennomsnittsbelastningen over 5 minutter overstiger en grenseverdi"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Slår inn når helsesjekken til en Docker-container rapporterer usunn"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Slår inn når hvilken som helst sensor overstiger en grenseverdi"
@@ -1795,6 +1807,10 @@ msgstr "Slår inn når forbruk av hvilken som helst disk overstiger en grensever
msgid "Type"
msgstr "Type"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Usunn"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Enhetsfil"
diff --git a/internal/site/src/locales/pl/pl.po b/internal/site/src/locales/pl/pl.po
index 1b28be14..224c86a4 100644
--- a/internal/site/src/locales/pl/pl.po
+++ b/internal/site/src/locales/pl/pl.po
@@ -424,6 +424,14 @@ msgstr "Konflikty"
msgid "Connection is down"
msgstr "Brak połączenia"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Kontener"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Kondycja kontenera"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Kontenery"
@@ -1749,6 +1757,10 @@ msgstr "Uruchamia się, gdy 15-minutowe średnie obciążenie systemu przekroczy
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Uruchamia się, gdy 5-minutowe średnie obciążenie systemu przekroczy ustawiony próg"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Wyzwalane, gdy kontrola kondycji (health check) kontenera Docker zgłasza stan niesprawny"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Wyzwalane, gdy jakikolwiek czujnik przekroczy ustalony próg."
@@ -1795,6 +1807,10 @@ msgstr "Wyzwalane, gdy wykorzystanie któregokolwiek dysku przekroczy ustalony p
msgid "Type"
msgstr "Typ"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Niesprawny"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Plik jednostki"
diff --git a/internal/site/src/locales/pt/pt.po b/internal/site/src/locales/pt/pt.po
index 14dd452f..d1c37931 100644
--- a/internal/site/src/locales/pt/pt.po
+++ b/internal/site/src/locales/pt/pt.po
@@ -424,6 +424,14 @@ msgstr "Conflitos"
msgid "Connection is down"
msgstr "A conexão está inativa"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Contentor"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Saúde do contentor"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Contentores"
@@ -1749,6 +1757,10 @@ msgstr "Dispara quando a média de carga de 15 minutos excede um limite"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Dispara quando a média de carga de 5 minutos excede um limite"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Dispara quando a verificação de saúde (health check) de um contentor Docker reporta um estado não saudável"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Dispara quando qualquer sensor excede um limite"
@@ -1795,6 +1807,10 @@ msgstr "Dispara quando o uso de qualquer disco excede um limite"
msgid "Type"
msgstr "Tipo"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Não saudável"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Arquivo de unidade"
diff --git a/internal/site/src/locales/ru/ru.po b/internal/site/src/locales/ru/ru.po
index ca017311..a8168acb 100644
--- a/internal/site/src/locales/ru/ru.po
+++ b/internal/site/src/locales/ru/ru.po
@@ -424,6 +424,14 @@ msgstr "Конфликты"
msgid "Connection is down"
msgstr "Нет соединения"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Контейнер"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Здоровье контейнера"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Контейнеры"
@@ -1749,6 +1757,10 @@ msgstr "Срабатывает, когда средняя загрузка за
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Срабатывает, когда средняя загрузка за 5 минут превышает порог"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Срабатывает, когда проверка состояния (health check) Docker-контейнера сообщает статус нездоровый"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Срабатывает, когда любой датчик превышает порог"
@@ -1795,6 +1807,10 @@ msgstr "Срабатывает, когда нагрузка на любой из
msgid "Type"
msgstr "Тип"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Нездоровый"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Файл единиц измерения"
diff --git a/internal/site/src/locales/sl/sl.po b/internal/site/src/locales/sl/sl.po
index 653c3cf6..0d09460e 100644
--- a/internal/site/src/locales/sl/sl.po
+++ b/internal/site/src/locales/sl/sl.po
@@ -424,6 +424,14 @@ msgstr "Konflikti"
msgid "Connection is down"
msgstr "Povezava je prekinjena"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Vsebnik"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Zdravje vsebnika"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Vsebniki"
@@ -1749,6 +1757,10 @@ msgstr "Sproži se, ko 15-minutna povprečna obremenitev preseže prag"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Sproži se, ko 5-minutna povprečna obremenitev preseže prag"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Sproži se, ko preverjanje zdravja vsebnika Docker javi stanje nezdravo"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Sproži se, ko kateri koli senzor preseže prag"
@@ -1795,6 +1807,10 @@ msgstr "Sproži se, ko uporaba katerega koli diska preseže prag"
msgid "Type"
msgstr "Vrsta"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Nezdrav"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Datoteka enote"
diff --git a/internal/site/src/locales/sr/sr.po b/internal/site/src/locales/sr/sr.po
index fc96a77c..aa7312e0 100644
--- a/internal/site/src/locales/sr/sr.po
+++ b/internal/site/src/locales/sr/sr.po
@@ -424,6 +424,14 @@ msgstr "Конфликти"
msgid "Connection is down"
msgstr "Веза је прекинута"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Контејнер"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Здравље контејнера"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Контејнери"
@@ -1749,6 +1757,10 @@ msgstr "Окида се када просечно оптерећење од 15
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Окида се када просечно оптерећење од 5 минута премаши праг"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Окида се када провера здравља Docker контејнера пријави да је нездрав"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Окида се када било који сензор премаши праг"
@@ -1795,6 +1807,10 @@ msgstr "Окида се када употреба било ког диска п
msgid "Type"
msgstr "Тип"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Нездрав"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Јединична датотека"
diff --git a/internal/site/src/locales/sv/sv.po b/internal/site/src/locales/sv/sv.po
index 373366e5..1f511db1 100644
--- a/internal/site/src/locales/sv/sv.po
+++ b/internal/site/src/locales/sv/sv.po
@@ -424,6 +424,14 @@ msgstr "Konflikter"
msgid "Connection is down"
msgstr "Ej ansluten"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Container"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Containerhälsa"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Containrar"
@@ -1749,6 +1757,10 @@ msgstr "Utlöses när 15-minuters genomsnittlig belastning överskrider ett trö
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Utlöses när 5-minuters genomsnittlig belastning överskrider ett tröskelvärde"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Utlöses när en Docker-containers hälsokontroll rapporterar osund"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Utlöses när någon sensor överskrider ett tröskelvärde"
@@ -1795,6 +1807,10 @@ msgstr "Utlöses när användningen av någon disk överskrider ett tröskelvär
msgid "Type"
msgstr "Typ"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Osund"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Unit-fil"
diff --git a/internal/site/src/locales/tr/tr.po b/internal/site/src/locales/tr/tr.po
index f5314a5b..05587829 100644
--- a/internal/site/src/locales/tr/tr.po
+++ b/internal/site/src/locales/tr/tr.po
@@ -424,6 +424,14 @@ msgstr "Çakışmalar"
msgid "Connection is down"
msgstr "Bağlantı kesildi"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Konteyner"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Konteyner Sağlığı"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Konteynerler"
@@ -1749,6 +1757,10 @@ msgstr "15 dakikalık yük ortalaması bir eşiği aştığında tetiklenir"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "5 dakikalık yük ortalaması bir eşiği aştığında tetiklenir"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Bir Docker konteynerinin sağlık kontrolü sağlıksız durumunu bildirdiğinde tetiklenir"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Herhangi bir sensör bir eşiği aştığında tetiklenir"
@@ -1795,6 +1807,10 @@ msgstr "Herhangi bir diskin kullanımı bir eşiği aştığında tetiklenir"
msgid "Type"
msgstr "Tür"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Sağlıksız"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Birim dosyası"
diff --git a/internal/site/src/locales/uk/uk.po b/internal/site/src/locales/uk/uk.po
index adf72c0d..433752a2 100644
--- a/internal/site/src/locales/uk/uk.po
+++ b/internal/site/src/locales/uk/uk.po
@@ -424,6 +424,14 @@ msgstr "Конфлікти"
msgid "Connection is down"
msgstr "З’єднання розірвано"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Контейнер"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Здоров'я контейнера"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Контейнери"
@@ -1749,6 +1757,10 @@ msgstr "Спрацьовує, коли середнє навантаження
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Спрацьовує, коли середнє навантаження за 5 хвилин перевищує поріг"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Спрацьовує, коли перевірка стану (health check) Docker-контейнера повідомляє про нездоровий стан"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Спрацьовує, коли будь-який датчик перевищує поріг"
@@ -1795,6 +1807,10 @@ msgstr "Спрацьовує, коли використання будь-яко
msgid "Type"
msgstr "Тип"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Нездоровий"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Файл юніта"
diff --git a/internal/site/src/locales/vi/vi.po b/internal/site/src/locales/vi/vi.po
index e61df68f..9f9d62be 100644
--- a/internal/site/src/locales/vi/vi.po
+++ b/internal/site/src/locales/vi/vi.po
@@ -424,6 +424,14 @@ msgstr "Xung đột"
msgid "Connection is down"
msgstr "Mất kết nối"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "Container"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "Tình trạng sức khỏe container"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "Container"
@@ -1749,6 +1757,10 @@ msgstr "Kích hoạt khi tải trung bình 15 phút vượt quá ngưỡng"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "Kích hoạt khi tải trung bình 5 phút vượt quá ngưỡng"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "Kích hoạt khi kiểm tra sức khỏe (health check) của container Docker báo cáo trạng thái không khỏe mạnh"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "Kích hoạt khi bất kỳ cảm biến nào vượt quá ngưỡng"
@@ -1795,6 +1807,10 @@ msgstr "Kích hoạt khi sử dụng bất kỳ đĩa nào vượt quá ngưỡn
msgid "Type"
msgstr "Loại"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "Không khỏe mạnh"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "Tệp đơn vị"
diff --git a/internal/site/src/locales/zh-CN/zh-CN.po b/internal/site/src/locales/zh-CN/zh-CN.po
index 5f7bf765..f961f159 100644
--- a/internal/site/src/locales/zh-CN/zh-CN.po
+++ b/internal/site/src/locales/zh-CN/zh-CN.po
@@ -424,6 +424,14 @@ msgstr "冲突"
msgid "Connection is down"
msgstr "连接已断开"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "容器"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "容器健康"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "容器"
@@ -1749,6 +1757,10 @@ msgstr "当 15 分钟负载平均值超过阈值时触发"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "当 5 分钟内的平均负载超过阈值时触发"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "当 Docker 容器的健康检查报告不健康状态时触发"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "当任何传感器超过阈值时触发"
@@ -1795,6 +1807,10 @@ msgstr "当任何磁盘的使用率超过阈值时触发"
msgid "Type"
msgstr "类型"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "不健康"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "单元文件"
diff --git a/internal/site/src/locales/zh-HK/zh-HK.po b/internal/site/src/locales/zh-HK/zh-HK.po
index bc966488..4f0a190f 100644
--- a/internal/site/src/locales/zh-HK/zh-HK.po
+++ b/internal/site/src/locales/zh-HK/zh-HK.po
@@ -424,6 +424,14 @@ msgstr "衝突"
msgid "Connection is down"
msgstr "連線中斷"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "容器"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "容器健康狀態"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "容器"
@@ -1749,6 +1757,10 @@ msgstr "當 15 分鐘平均負載超過閾值時觸發"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "當 5 分鐘平均負載超過閾值時觸發"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "當 Docker 容器的健康檢查回報不健康狀態時觸發"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "當任何傳感器超過閾值時觸發"
@@ -1795,6 +1807,10 @@ msgstr "當任何磁碟的使用超過閾值時觸發"
msgid "Type"
msgstr "類型"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "不健康"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "單元檔案"
diff --git a/internal/site/src/locales/zh/zh.po b/internal/site/src/locales/zh/zh.po
index a1e7749d..31a11801 100644
--- a/internal/site/src/locales/zh/zh.po
+++ b/internal/site/src/locales/zh/zh.po
@@ -424,6 +424,14 @@ msgstr "衝突"
msgid "Connection is down"
msgstr "連線中斷"
+#: src/lib/alerts.ts
+msgid "Container"
+msgstr "容器"
+
+#: src/lib/alerts.ts
+msgid "Container Health"
+msgstr "容器健康狀態"
+
#: src/components/routes/system.tsx
msgid "Containers"
msgstr "容器"
@@ -1749,6 +1757,10 @@ msgstr "當 15 分鐘平均負載超過閾值時觸發"
msgid "Triggers when 5 minute load average exceeds a threshold"
msgstr "當 5 分鐘平均負載超過閾值時觸發"
+#: src/lib/alerts.ts
+msgid "Triggers when a Docker container's health check reports unhealthy"
+msgstr "當 Docker 容器的健康檢查回報不健康狀態時觸發"
+
#: src/lib/alerts.ts
msgid "Triggers when any sensor exceeds a threshold"
msgstr "當任何感應器超過閾值時觸發"
@@ -1795,6 +1807,10 @@ msgstr "當任何磁碟使用率超過閾值時觸發"
msgid "Type"
msgstr "類型"
+#: src/lib/alerts.ts
+msgid "Unhealthy"
+msgstr "不健康"
+
#: src/components/systemd-table/systemd-table.tsx
msgid "Unit file"
msgstr "單元檔案"
diff --git a/internal/site/src/types.d.ts b/internal/site/src/types.d.ts
index 60d38266..6a001787 100644
--- a/internal/site/src/types.d.ts
+++ b/internal/site/src/types.d.ts
@@ -400,6 +400,8 @@ export interface AlertInfo {
noDuration?: boolean
/** Description shown instead of numeric threshold and duration values */
triggeredDesc?: () => string
+ /** Additional information that remains visible while the alert is enabled */
+ note?: () => string
invert?: boolean
}