From 5969d368564660b13c7a865b21cc12a25c12a634 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Mleczko?= Date: Wed, 2 Sep 2026 18:46:23 +0200 Subject: [PATCH] feat(alerts): add container health alerts with log excerpt on notifications (#2225) Add a new "ContainerHealth" alert type that fires when a Docker container's health check reports unhealthy, and resolves when it recovers. This mirrors the existing Status (up/down) alert pattern: an alert can be armed per system and honors the "min minutes" delay before firing. When the alert fires, the notification (email and any configured webhook, including Discord via shoutrrr) includes a log excerpt fetched live from the agent for up to 2 of the unhealthy containers, prioritizing lines containing "error" or "fatal" (falling back to the log tail if none match), capped to keep the message well under Discord's size limit. --------- Co-authored-by: hank --- agent/docker_test.go | 2 + internal/alerts/alerts.go | 8 +- internal/alerts/alerts_cache.go | 18 +- internal/alerts/alerts_container.go | 318 ++++++++++++++++ internal/alerts/alerts_container_test.go | 349 ++++++++++++++++++ internal/alerts/alerts_system.go | 2 +- internal/alerts/alerts_test_helpers.go | 5 + internal/entities/system/system_test.go | 32 ++ internal/hub/systems/system_manager.go | 12 +- internal/hub/systems/system_smart_test.go | 6 +- ...=> 0_collections_snapshot_0_19_0_dev_2.go} | 14 +- .../src/components/alerts/alerts-sheet.tsx | 1 + internal/site/src/lib/alerts.ts | 12 +- internal/site/src/locales/ar/ar.po | 16 + internal/site/src/locales/bg/bg.po | 16 + internal/site/src/locales/cs/cs.po | 16 + internal/site/src/locales/da/da.po | 16 + internal/site/src/locales/de/de.po | 16 + internal/site/src/locales/en/en.po | 16 + internal/site/src/locales/es/es.po | 16 + internal/site/src/locales/fa/fa.po | 16 + internal/site/src/locales/fr/fr.po | 16 + internal/site/src/locales/he/he.po | 16 + internal/site/src/locales/hr/hr.po | 16 + internal/site/src/locales/hu/hu.po | 16 + internal/site/src/locales/id/id.po | 16 + internal/site/src/locales/it/it.po | 16 + internal/site/src/locales/ja/ja.po | 16 + internal/site/src/locales/ko/ko.po | 16 + internal/site/src/locales/nl/nl.po | 16 + internal/site/src/locales/no/no.po | 16 + internal/site/src/locales/pl/pl.po | 16 + internal/site/src/locales/pt/pt.po | 16 + internal/site/src/locales/ru/ru.po | 16 + internal/site/src/locales/sl/sl.po | 16 + internal/site/src/locales/sr/sr.po | 16 + internal/site/src/locales/sv/sv.po | 16 + internal/site/src/locales/tr/tr.po | 16 + internal/site/src/locales/uk/uk.po | 16 + internal/site/src/locales/vi/vi.po | 16 + internal/site/src/locales/zh-CN/zh-CN.po | 16 + internal/site/src/locales/zh-HK/zh-HK.po | 16 + internal/site/src/locales/zh/zh.po | 16 + internal/site/src/types.d.ts | 2 + 44 files changed, 1245 insertions(+), 16 deletions(-) create mode 100644 internal/alerts/alerts_container.go create mode 100644 internal/alerts/alerts_container_test.go rename internal/migrations/{0_collections_snapshot_0_19_0_dev_1.go => 0_collections_snapshot_0_19_0_dev_2.go} (99%) 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 }