mirror of
https://github.com/henrygd/beszel.git
synced 2025-12-17 02:36:17 +01:00
add low battery alert (#1507)
This commit is contained in:
@@ -1053,53 +1053,6 @@ func TestDecodeDockerLogStreamMemoryProtection(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestAllocateBuffer(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
currentCap int
|
|
||||||
needed int
|
|
||||||
expectedCap int
|
|
||||||
shouldRealloc bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "buffer has enough capacity",
|
|
||||||
currentCap: 1024,
|
|
||||||
needed: 512,
|
|
||||||
expectedCap: 1024,
|
|
||||||
shouldRealloc: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "buffer needs reallocation",
|
|
||||||
currentCap: 512,
|
|
||||||
needed: 1024,
|
|
||||||
expectedCap: 1024,
|
|
||||||
shouldRealloc: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "buffer needs exact size",
|
|
||||||
currentCap: 1024,
|
|
||||||
needed: 1024,
|
|
||||||
expectedCap: 1024,
|
|
||||||
shouldRealloc: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
current := make([]byte, 0, tt.currentCap)
|
|
||||||
result := allocateBuffer(current, tt.needed)
|
|
||||||
|
|
||||||
assert.Equal(t, tt.needed, len(result))
|
|
||||||
assert.GreaterOrEqual(t, cap(result), tt.expectedCap)
|
|
||||||
|
|
||||||
if tt.shouldRealloc {
|
|
||||||
// If reallocation was needed, capacity should be at least the needed size
|
|
||||||
assert.GreaterOrEqual(t, cap(result), tt.needed)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestShouldExcludeContainer(t *testing.T) {
|
func TestShouldExcludeContainer(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -1259,4 +1212,3 @@ func TestAnsiEscapePattern(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,6 +49,7 @@ type SystemAlertStats struct {
|
|||||||
GPU map[string]SystemAlertGPUData `json:"g"`
|
GPU map[string]SystemAlertGPUData `json:"g"`
|
||||||
Temperatures map[string]float32 `json:"t"`
|
Temperatures map[string]float32 `json:"t"`
|
||||||
LoadAvg [3]float64 `json:"la"`
|
LoadAvg [3]float64 `json:"la"`
|
||||||
|
Battery [2]uint8 `json:"bat"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type SystemAlertGPUData struct {
|
type SystemAlertGPUData struct {
|
||||||
|
|||||||
387
internal/alerts/alerts_battery_test.go
Normal file
387
internal/alerts/alerts_battery_test.go
Normal file
@@ -0,0 +1,387 @@
|
|||||||
|
//go:build testing
|
||||||
|
// +build testing
|
||||||
|
|
||||||
|
package alerts_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"testing"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/henrygd/beszel/internal/entities/system"
|
||||||
|
beszelTests "github.com/henrygd/beszel/internal/tests"
|
||||||
|
|
||||||
|
"github.com/pocketbase/dbx"
|
||||||
|
"github.com/pocketbase/pocketbase/tools/types"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
)
|
||||||
|
|
||||||
|
// TestBatteryAlertLogic tests that battery alerts trigger when value drops BELOW threshold
|
||||||
|
// (opposite of other alerts like CPU, Memory, etc. which trigger when exceeding threshold)
|
||||||
|
func TestBatteryAlertLogic(t *testing.T) {
|
||||||
|
hub, user := beszelTests.GetHubWithUser(t)
|
||||||
|
defer hub.Cleanup()
|
||||||
|
|
||||||
|
// Create a system
|
||||||
|
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
|
||||||
|
require.NoError(t, err)
|
||||||
|
systemRecord := systems[0]
|
||||||
|
|
||||||
|
// Create a battery alert with threshold of 20% and min of 1 minute (immediate trigger)
|
||||||
|
batteryAlert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
|
||||||
|
"name": "Battery",
|
||||||
|
"system": systemRecord.Id,
|
||||||
|
"user": user.Id,
|
||||||
|
"value": 20, // threshold: 20%
|
||||||
|
"min": 1, // 1 minute (immediate trigger for testing)
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify alert is not triggered initially
|
||||||
|
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should not be triggered initially")
|
||||||
|
|
||||||
|
// Create system stats with battery at 50% (above threshold - should NOT trigger)
|
||||||
|
statsHigh := system.Stats{
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
Battery: [2]uint8{50, 1}, // 50% battery, discharging
|
||||||
|
}
|
||||||
|
statsHighJSON, _ := json.Marshal(statsHigh)
|
||||||
|
_, err = beszelTests.CreateRecord(hub, "system_stats", map[string]any{
|
||||||
|
"system": systemRecord.Id,
|
||||||
|
"type": "1m",
|
||||||
|
"stats": string(statsHighJSON),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create CombinedData for the alert handler
|
||||||
|
combinedDataHigh := &system.CombinedData{
|
||||||
|
Stats: statsHigh,
|
||||||
|
Info: system.Info{
|
||||||
|
Hostname: "test-host",
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate system update time
|
||||||
|
systemRecord.Set("updated", time.Now().UTC())
|
||||||
|
err = hub.SaveNoValidate(systemRecord)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Handle system alerts with high battery
|
||||||
|
am := hub.GetAlertManager()
|
||||||
|
err = am.HandleSystemAlerts(systemRecord, combinedDataHigh)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify alert is still NOT triggered (battery 50% is above threshold 20%)
|
||||||
|
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should NOT be triggered when battery (50%%) is above threshold (20%%)")
|
||||||
|
|
||||||
|
// Now create stats with battery at 15% (below threshold - should trigger)
|
||||||
|
statsLow := system.Stats{
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
Battery: [2]uint8{15, 1}, // 15% battery, discharging
|
||||||
|
}
|
||||||
|
statsLowJSON, _ := json.Marshal(statsLow)
|
||||||
|
_, err = beszelTests.CreateRecord(hub, "system_stats", map[string]any{
|
||||||
|
"system": systemRecord.Id,
|
||||||
|
"type": "1m",
|
||||||
|
"stats": string(statsLowJSON),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
combinedDataLow := &system.CombinedData{
|
||||||
|
Stats: statsLow,
|
||||||
|
Info: system.Info{
|
||||||
|
Hostname: "test-host",
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update system timestamp
|
||||||
|
systemRecord.Set("updated", time.Now().UTC())
|
||||||
|
err = hub.SaveNoValidate(systemRecord)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Handle system alerts with low battery
|
||||||
|
err = am.HandleSystemAlerts(systemRecord, combinedDataLow)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for the alert to be processed
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify alert IS triggered (battery 15% is below threshold 20%)
|
||||||
|
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, batteryAlert.GetBool("triggered"), "Alert SHOULD be triggered when battery (15%%) drops below threshold (20%%)")
|
||||||
|
|
||||||
|
// Now test resolution: battery goes back above threshold
|
||||||
|
statsRecovered := system.Stats{
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
Battery: [2]uint8{25, 1}, // 25% battery, discharging
|
||||||
|
}
|
||||||
|
statsRecoveredJSON, _ := json.Marshal(statsRecovered)
|
||||||
|
_, err = beszelTests.CreateRecord(hub, "system_stats", map[string]any{
|
||||||
|
"system": systemRecord.Id,
|
||||||
|
"type": "1m",
|
||||||
|
"stats": string(statsRecoveredJSON),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
combinedDataRecovered := &system.CombinedData{
|
||||||
|
Stats: statsRecovered,
|
||||||
|
Info: system.Info{
|
||||||
|
Hostname: "test-host",
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update system timestamp
|
||||||
|
systemRecord.Set("updated", time.Now().UTC())
|
||||||
|
err = hub.SaveNoValidate(systemRecord)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Handle system alerts with recovered battery
|
||||||
|
err = am.HandleSystemAlerts(systemRecord, combinedDataRecovered)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for the alert to be processed
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify alert is now resolved (battery 25% is above threshold 20%)
|
||||||
|
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should be resolved when battery (25%%) goes above threshold (20%%)")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBatteryAlertNoBattery verifies that systems without battery data don't trigger alerts
|
||||||
|
func TestBatteryAlertNoBattery(t *testing.T) {
|
||||||
|
hub, user := beszelTests.GetHubWithUser(t)
|
||||||
|
defer hub.Cleanup()
|
||||||
|
|
||||||
|
// Create a system
|
||||||
|
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
|
||||||
|
require.NoError(t, err)
|
||||||
|
systemRecord := systems[0]
|
||||||
|
|
||||||
|
// Create a battery alert
|
||||||
|
batteryAlert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
|
||||||
|
"name": "Battery",
|
||||||
|
"system": systemRecord.Id,
|
||||||
|
"user": user.Id,
|
||||||
|
"value": 20,
|
||||||
|
"min": 1,
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Create stats with NO battery data (Battery[0] = 0)
|
||||||
|
statsNoBattery := system.Stats{
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
Battery: [2]uint8{0, 0}, // No battery
|
||||||
|
}
|
||||||
|
|
||||||
|
combinedData := &system.CombinedData{
|
||||||
|
Stats: statsNoBattery,
|
||||||
|
Info: system.Info{
|
||||||
|
Hostname: "test-host",
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Simulate system update time
|
||||||
|
systemRecord.Set("updated", time.Now().UTC())
|
||||||
|
err = hub.SaveNoValidate(systemRecord)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Handle system alerts
|
||||||
|
am := hub.GetAlertManager()
|
||||||
|
err = am.HandleSystemAlerts(systemRecord, combinedData)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait a moment for processing
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify alert is NOT triggered (no battery data should skip the alert)
|
||||||
|
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should NOT be triggered when system has no battery")
|
||||||
|
}
|
||||||
|
|
||||||
|
// TestBatteryAlertAveragedSamples tests battery alerts with min > 1 (averaging multiple samples)
|
||||||
|
// This ensures the inverted threshold logic works correctly across averaged time windows
|
||||||
|
func TestBatteryAlertAveragedSamples(t *testing.T) {
|
||||||
|
hub, user := beszelTests.GetHubWithUser(t)
|
||||||
|
defer hub.Cleanup()
|
||||||
|
|
||||||
|
// Create a system
|
||||||
|
systems, err := beszelTests.CreateSystems(hub, 1, user.Id, "up")
|
||||||
|
require.NoError(t, err)
|
||||||
|
systemRecord := systems[0]
|
||||||
|
|
||||||
|
// Create a battery alert with threshold of 25% and min of 2 minutes (requires averaging)
|
||||||
|
batteryAlert, err := beszelTests.CreateRecord(hub, "alerts", map[string]any{
|
||||||
|
"name": "Battery",
|
||||||
|
"system": systemRecord.Id,
|
||||||
|
"user": user.Id,
|
||||||
|
"value": 25, // threshold: 25%
|
||||||
|
"min": 2, // 2 minutes - requires averaging
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Verify alert is not triggered initially
|
||||||
|
assert.False(t, batteryAlert.GetBool("triggered"), "Alert should not be triggered initially")
|
||||||
|
|
||||||
|
am := hub.GetAlertManager()
|
||||||
|
now := time.Now().UTC()
|
||||||
|
|
||||||
|
// Create system_stats records with low battery (below threshold)
|
||||||
|
// The alert has min=2 minutes, so alert.time = now - 2 minutes
|
||||||
|
// For the alert to be valid, alert.time must be AFTER the oldest record's created time
|
||||||
|
// So we need records older than (now - 2 min), plus records within the window
|
||||||
|
// Records at: now-3min (oldest, before window), now-90s, now-60s, now-30s
|
||||||
|
recordTimes := []time.Duration{
|
||||||
|
-180 * time.Second, // 3 min ago - this makes the oldest record before alert.time
|
||||||
|
-90 * time.Second,
|
||||||
|
-60 * time.Second,
|
||||||
|
-30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, offset := range recordTimes {
|
||||||
|
statsLow := system.Stats{
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
Battery: [2]uint8{15, 1}, // 15% battery (below 25% threshold)
|
||||||
|
}
|
||||||
|
statsLowJSON, _ := json.Marshal(statsLow)
|
||||||
|
|
||||||
|
recordTime := now.Add(offset)
|
||||||
|
record, err := beszelTests.CreateRecord(hub, "system_stats", map[string]any{
|
||||||
|
"system": systemRecord.Id,
|
||||||
|
"type": "1m",
|
||||||
|
"stats": string(statsLowJSON),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
// Update created time to simulate historical records - use SetRaw with formatted string
|
||||||
|
record.SetRaw("created", recordTime.Format(types.DefaultDateLayout))
|
||||||
|
err = hub.SaveNoValidate(record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create combined data with low battery
|
||||||
|
combinedDataLow := &system.CombinedData{
|
||||||
|
Stats: system.Stats{
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
Battery: [2]uint8{15, 1},
|
||||||
|
},
|
||||||
|
Info: system.Info{
|
||||||
|
Hostname: "test-host",
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update system timestamp
|
||||||
|
systemRecord.Set("updated", now)
|
||||||
|
err = hub.SaveNoValidate(systemRecord)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Handle system alerts - should trigger because average battery is below threshold
|
||||||
|
err = am.HandleSystemAlerts(systemRecord, combinedDataLow)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for alert processing
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify alert IS triggered (average battery 15% is below threshold 25%)
|
||||||
|
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.True(t, batteryAlert.GetBool("triggered"),
|
||||||
|
"Alert SHOULD be triggered when average battery (15%%) is below threshold (25%%) over min period")
|
||||||
|
|
||||||
|
// Now add records with high battery to test resolution
|
||||||
|
// Use a new time window 2 minutes later
|
||||||
|
newNow := now.Add(2 * time.Minute)
|
||||||
|
// Records need to span before the alert time window (newNow - 2 min)
|
||||||
|
recordTimesHigh := []time.Duration{
|
||||||
|
-180 * time.Second, // 3 min before newNow - makes oldest record before alert.time
|
||||||
|
-90 * time.Second,
|
||||||
|
-60 * time.Second,
|
||||||
|
-30 * time.Second,
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, offset := range recordTimesHigh {
|
||||||
|
statsHigh := system.Stats{
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
Battery: [2]uint8{50, 1}, // 50% battery (above 25% threshold)
|
||||||
|
}
|
||||||
|
statsHighJSON, _ := json.Marshal(statsHigh)
|
||||||
|
|
||||||
|
recordTime := newNow.Add(offset)
|
||||||
|
record, err := beszelTests.CreateRecord(hub, "system_stats", map[string]any{
|
||||||
|
"system": systemRecord.Id,
|
||||||
|
"type": "1m",
|
||||||
|
"stats": string(statsHighJSON),
|
||||||
|
})
|
||||||
|
require.NoError(t, err)
|
||||||
|
record.SetRaw("created", recordTime.Format(types.DefaultDateLayout))
|
||||||
|
err = hub.SaveNoValidate(record)
|
||||||
|
require.NoError(t, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Create combined data with high battery
|
||||||
|
combinedDataHigh := &system.CombinedData{
|
||||||
|
Stats: system.Stats{
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
Battery: [2]uint8{50, 1},
|
||||||
|
},
|
||||||
|
Info: system.Info{
|
||||||
|
Hostname: "test-host",
|
||||||
|
Cpu: 10,
|
||||||
|
MemPct: 30,
|
||||||
|
DiskPct: 40,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update system timestamp to the new time window
|
||||||
|
systemRecord.Set("updated", newNow)
|
||||||
|
err = hub.SaveNoValidate(systemRecord)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Handle system alerts - should resolve because average battery is now above threshold
|
||||||
|
err = am.HandleSystemAlerts(systemRecord, combinedDataHigh)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
// Wait for alert processing
|
||||||
|
time.Sleep(20 * time.Millisecond)
|
||||||
|
|
||||||
|
// Verify alert is resolved (average battery 50% is above threshold 25%)
|
||||||
|
batteryAlert, err = hub.FindFirstRecordByFilter("alerts", "id={:id}", dbx.Params{"id": batteryAlert.Id})
|
||||||
|
require.NoError(t, err)
|
||||||
|
assert.False(t, batteryAlert.GetBool("triggered"),
|
||||||
|
"Alert should be resolved when average battery (50%%) is above threshold (25%%) over min period")
|
||||||
|
}
|
||||||
@@ -66,18 +66,31 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
unit = ""
|
unit = ""
|
||||||
case "GPU":
|
case "GPU":
|
||||||
val = data.Info.GpuPct
|
val = data.Info.GpuPct
|
||||||
|
case "Battery":
|
||||||
|
if data.Stats.Battery[0] == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
val = float64(data.Stats.Battery[0])
|
||||||
}
|
}
|
||||||
|
|
||||||
triggered := alertRecord.GetBool("triggered")
|
triggered := alertRecord.GetBool("triggered")
|
||||||
threshold := alertRecord.GetFloat("value")
|
threshold := alertRecord.GetFloat("value")
|
||||||
|
|
||||||
|
// Battery alert has inverted logic: trigger when value is BELOW threshold
|
||||||
|
lowAlert := isLowAlert(name)
|
||||||
|
|
||||||
// CONTINUE
|
// CONTINUE
|
||||||
// IF alert is not triggered and curValue is less than threshold
|
// For normal alerts: IF not triggered and curValue <= threshold, OR triggered and curValue > threshold
|
||||||
// OR alert is triggered and curValue is greater than threshold
|
// For low alerts (Battery): IF not triggered and curValue >= threshold, OR triggered and curValue < threshold
|
||||||
if (!triggered && val <= threshold) || (triggered && val > threshold) {
|
if lowAlert {
|
||||||
// log.Printf("Skipping alert %s: val %f | threshold %f | triggered %v\n", name, val, threshold, triggered)
|
if (!triggered && val >= threshold) || (triggered && val < threshold) {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
if (!triggered && val <= threshold) || (triggered && val > threshold) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
min := max(1, cast.ToUint8(alertRecord.Get("min")))
|
min := max(1, cast.ToUint8(alertRecord.Get("min")))
|
||||||
|
|
||||||
@@ -94,7 +107,11 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
|
|
||||||
// send alert immediately if min is 1 - no need to sum up values.
|
// send alert immediately if min is 1 - no need to sum up values.
|
||||||
if min == 1 {
|
if min == 1 {
|
||||||
|
if lowAlert {
|
||||||
|
alert.triggered = val < threshold
|
||||||
|
} else {
|
||||||
alert.triggered = val > threshold
|
alert.triggered = val > threshold
|
||||||
|
}
|
||||||
go am.sendSystemAlert(alert)
|
go am.sendSystemAlert(alert)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -219,6 +236,8 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
alert.val += maxUsage
|
alert.val += maxUsage
|
||||||
|
case "Battery":
|
||||||
|
alert.val += float64(stats.Battery[0])
|
||||||
default:
|
default:
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -256,6 +275,17 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
// log.Printf("%s: val %f | count %d | min-count %f | threshold %f\n", alert.name, alert.val, alert.count, minCount, alert.threshold)
|
// log.Printf("%s: val %f | count %d | min-count %f | threshold %f\n", alert.name, alert.val, alert.count, minCount, alert.threshold)
|
||||||
// pass through alert if count is greater than or equal to minCount
|
// pass through alert if count is greater than or equal to minCount
|
||||||
if float32(alert.count) >= minCount {
|
if float32(alert.count) >= minCount {
|
||||||
|
// Battery alert has inverted logic: trigger when value is BELOW threshold
|
||||||
|
lowAlert := isLowAlert(alert.name)
|
||||||
|
if lowAlert {
|
||||||
|
if !alert.triggered && alert.val < alert.threshold {
|
||||||
|
alert.triggered = true
|
||||||
|
go am.sendSystemAlert(alert)
|
||||||
|
} else if alert.triggered && alert.val >= alert.threshold {
|
||||||
|
alert.triggered = false
|
||||||
|
go am.sendSystemAlert(alert)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
if !alert.triggered && alert.val > alert.threshold {
|
if !alert.triggered && alert.val > alert.threshold {
|
||||||
alert.triggered = true
|
alert.triggered = true
|
||||||
go am.sendSystemAlert(alert)
|
go am.sendSystemAlert(alert)
|
||||||
@@ -265,6 +295,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,11 +319,20 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var subject string
|
var subject string
|
||||||
|
lowAlert := isLowAlert(alert.name)
|
||||||
if alert.triggered {
|
if alert.triggered {
|
||||||
|
if lowAlert {
|
||||||
|
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
|
||||||
|
} else {
|
||||||
|
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if lowAlert {
|
||||||
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
|
subject = fmt.Sprintf("%s %s above threshold", systemName, titleAlertName)
|
||||||
} else {
|
} else {
|
||||||
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
|
subject = fmt.Sprintf("%s %s below threshold", systemName, titleAlertName)
|
||||||
}
|
}
|
||||||
|
}
|
||||||
minutesLabel := "minute"
|
minutesLabel := "minute"
|
||||||
if alert.min > 1 {
|
if alert.min > 1 {
|
||||||
minutesLabel += "s"
|
minutesLabel += "s"
|
||||||
@@ -316,3 +356,7 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
|
|||||||
LinkText: "View " + systemName,
|
LinkText: "View " + systemName,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func isLowAlert(name string) bool {
|
||||||
|
return name == "Battery"
|
||||||
|
}
|
||||||
|
|||||||
@@ -78,7 +78,8 @@ func init() {
|
|||||||
"GPU",
|
"GPU",
|
||||||
"LoadAvg1",
|
"LoadAvg1",
|
||||||
"LoadAvg5",
|
"LoadAvg5",
|
||||||
"LoadAvg15"
|
"LoadAvg15",
|
||||||
|
"Battery"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -245,6 +245,15 @@ export function AlertContent({
|
|||||||
{!singleDescription && (
|
{!singleDescription && (
|
||||||
<div>
|
<div>
|
||||||
<p id={`v${name}`} className="text-sm block h-8">
|
<p id={`v${name}`} className="text-sm block h-8">
|
||||||
|
{alertKey === "Battery" ? (
|
||||||
|
<Trans>
|
||||||
|
Average drops below{" "}
|
||||||
|
<strong className="text-foreground">
|
||||||
|
{value}
|
||||||
|
{alertData.unit}
|
||||||
|
</strong>
|
||||||
|
</Trans>
|
||||||
|
) : (
|
||||||
<Trans>
|
<Trans>
|
||||||
Average exceeds{" "}
|
Average exceeds{" "}
|
||||||
<strong className="text-foreground">
|
<strong className="text-foreground">
|
||||||
@@ -252,6 +261,7 @@ export function AlertContent({
|
|||||||
{alertData.unit}
|
{alertData.unit}
|
||||||
</strong>
|
</strong>
|
||||||
</Trans>
|
</Trans>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
<div className="flex gap-3">
|
<div className="flex gap-3">
|
||||||
<Slider
|
<Slider
|
||||||
|
|||||||
@@ -139,3 +139,11 @@ export function WebSocketIcon(props: SVGProps<SVGSVGElement>) {
|
|||||||
</svg>
|
</svg>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function BatteryIcon(props: SVGProps<SVGSVGElement>) {
|
||||||
|
return (
|
||||||
|
<svg viewBox="0 0 256 256" {...props} fill="currentColor">
|
||||||
|
<path d="M176,32H80A24,24,0,0,0,56,56V224a24,24,0,0,0,24,24h96a24,24,0,0,0,24-24V56A24,24,0,0,0,176,32Zm8,192a8,8,0,0,1-8,8H80a8,8,0,0,1-8-8V56a8,8,0,0,1,8-8h96a8,8,0,0,1,8,8Zm-16-24a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,200ZM88,8a8,8,0,0,1,8-8h64a8,8,0,0,1,0,16H96A8,8,0,0,1,88,8Zm80,152a8,8,0,0,1-8,8H96a8,8,0,0,1,0-16h64A8,8,0,0,1,168,160Z"></path>
|
||||||
|
</svg>
|
||||||
|
)
|
||||||
|
}
|
||||||
@@ -1,10 +1,11 @@
|
|||||||
import { t } from "@lingui/core/macro"
|
import { t } from "@lingui/core/macro"
|
||||||
import { CpuIcon, HardDriveIcon, HourglassIcon, MemoryStickIcon, ServerIcon, ThermometerIcon } from "lucide-react"
|
import { CpuIcon, HardDriveIcon, MemoryStickIcon, ServerIcon } from "lucide-react"
|
||||||
import type { RecordSubscription } from "pocketbase"
|
import type { RecordSubscription } from "pocketbase"
|
||||||
import { EthernetIcon, GpuIcon } from "@/components/ui/icons"
|
import { EthernetIcon, GpuIcon } from "@/components/ui/icons"
|
||||||
import { $alerts } from "@/lib/stores"
|
import { $alerts } from "@/lib/stores"
|
||||||
import type { AlertInfo, AlertRecord } from "@/types"
|
import type { AlertInfo, AlertRecord } from "@/types"
|
||||||
import { pb } from "./api"
|
import { pb } from "./api"
|
||||||
|
import { ThermometerIcon, BatteryIcon, HourglassIcon } from "@/components/ui/icons"
|
||||||
|
|
||||||
/** Alert info for each alert type */
|
/** Alert info for each alert type */
|
||||||
export const alertInfo: Record<string, AlertInfo> = {
|
export const alertInfo: Record<string, AlertInfo> = {
|
||||||
@@ -83,6 +84,13 @@ export const alertInfo: Record<string, AlertInfo> = {
|
|||||||
step: 0.1,
|
step: 0.1,
|
||||||
desc: () => t`Triggers when 15 minute load average exceeds a threshold`,
|
desc: () => t`Triggers when 15 minute load average exceeds a threshold`,
|
||||||
},
|
},
|
||||||
|
Battery: {
|
||||||
|
name: () => t`Battery`,
|
||||||
|
unit: "%",
|
||||||
|
icon: BatteryIcon,
|
||||||
|
desc: () => t`Triggers when battery charge drops below a threshold`,
|
||||||
|
start: 20,
|
||||||
|
},
|
||||||
} as const
|
} as const
|
||||||
|
|
||||||
/** Helper to manage user alerts */
|
/** Helper to manage user alerts */
|
||||||
|
|||||||
Reference in New Issue
Block a user