diff --git a/agent/network.go b/agent/network.go
index 5de78a1f..933ecc5e 100644
--- a/agent/network.go
+++ b/agent/network.go
@@ -213,10 +213,8 @@ func (a *Agent) applyNetworkTotals(
totalBytesSent, totalBytesRecv uint64,
bytesSentPerSecond, bytesRecvPerSecond uint64,
) {
- networkSentPs := utils.BytesToMegabytes(float64(bytesSentPerSecond))
- networkRecvPs := utils.BytesToMegabytes(float64(bytesRecvPerSecond))
- if networkSentPs > 10_000 || networkRecvPs > 10_000 {
- slog.Warn("Invalid net stats. Resetting.", "sent", networkSentPs, "recv", networkRecvPs)
+ if bytesSentPerSecond > 10_000_000_000 || bytesRecvPerSecond > 10_000_000_000 {
+ slog.Warn("Invalid net stats. Resetting.", "sent", bytesSentPerSecond, "recv", bytesRecvPerSecond)
for _, v := range netIO {
if _, exists := a.netInterfaces[v.Name]; !exists {
continue
@@ -226,14 +224,10 @@ func (a *Agent) applyNetworkTotals(
a.initializeNetIoStats()
delete(a.netIoStats, cacheTimeMs)
delete(a.netInterfaceDeltaTrackers, cacheTimeMs)
- systemStats.NetworkSent = 0
- systemStats.NetworkRecv = 0
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = 0, 0
return
}
- systemStats.NetworkSent = networkSentPs
- systemStats.NetworkRecv = networkRecvPs
systemStats.Bandwidth[0], systemStats.Bandwidth[1] = bytesSentPerSecond, bytesRecvPerSecond
nis.BytesSent = totalBytesSent
nis.BytesRecv = totalBytesRecv
diff --git a/agent/network_test.go b/agent/network_test.go
index 3f32715b..6f7ecb40 100644
--- a/agent/network_test.go
+++ b/agent/network_test.go
@@ -416,8 +416,6 @@ func TestApplyNetworkTotals(t *testing.T) {
totalBytesSent uint64
totalBytesRecv uint64
expectReset bool
- expectedNetworkSent float64
- expectedNetworkRecv float64
expectedBandwidthSent uint64
expectedBandwidthRecv uint64
}{
@@ -428,8 +426,6 @@ func TestApplyNetworkTotals(t *testing.T) {
totalBytesSent: 10000000,
totalBytesRecv: 20000000,
expectReset: false,
- expectedNetworkSent: 0.95, // ~1 MB/s rounded to 2 decimals
- expectedNetworkRecv: 1.91, // ~2 MB/s rounded to 2 decimals
expectedBandwidthSent: 1000000,
expectedBandwidthRecv: 2000000,
},
@@ -457,18 +453,6 @@ func TestApplyNetworkTotals(t *testing.T) {
totalBytesRecv: 20000000,
expectReset: true,
},
- {
- name: "Valid network stats - at threshold boundary",
- bytesSentPerSecond: 10485750000, // ~9999.99 MB/s (rounds to 9999.99)
- bytesRecvPerSecond: 10485750000, // ~9999.99 MB/s (rounds to 9999.99)
- totalBytesSent: 10000000,
- totalBytesRecv: 20000000,
- expectReset: false,
- expectedNetworkSent: 9999.99,
- expectedNetworkRecv: 9999.99,
- expectedBandwidthSent: 10485750000,
- expectedBandwidthRecv: 10485750000,
- },
{
name: "Zero values",
bytesSentPerSecond: 0,
@@ -476,8 +460,6 @@ func TestApplyNetworkTotals(t *testing.T) {
totalBytesSent: 0,
totalBytesRecv: 0,
expectReset: false,
- expectedNetworkSent: 0.0,
- expectedNetworkRecv: 0.0,
expectedBandwidthSent: 0,
expectedBandwidthRecv: 0,
},
@@ -514,14 +496,10 @@ func TestApplyNetworkTotals(t *testing.T) {
// Should have reset network tracking state - maps cleared and stats zeroed
assert.NotContains(t, a.netIoStats, cacheTimeMs, "cache entry should be cleared after reset")
assert.NotContains(t, a.netInterfaceDeltaTrackers, cacheTimeMs, "tracker should be cleared on reset")
- assert.Zero(t, systemStats.NetworkSent)
- assert.Zero(t, systemStats.NetworkRecv)
assert.Zero(t, systemStats.Bandwidth[0])
assert.Zero(t, systemStats.Bandwidth[1])
} else {
// Should have applied stats
- assert.Equal(t, tt.expectedNetworkSent, systemStats.NetworkSent)
- assert.Equal(t, tt.expectedNetworkRecv, systemStats.NetworkRecv)
assert.Equal(t, tt.expectedBandwidthSent, systemStats.Bandwidth[0])
assert.Equal(t, tt.expectedBandwidthRecv, systemStats.Bandwidth[1])
diff --git a/internal/alerts/alerts.go b/internal/alerts/alerts.go
index da3ad784..ab427fc3 100644
--- a/internal/alerts/alerts.go
+++ b/internal/alerts/alerts.go
@@ -45,17 +45,17 @@ type SystemAlertFsStats struct {
DiskUsed float64 `json:"du"`
}
+// Values pulled from system_stats.stats that are relevant to alerts.
type SystemAlertStats struct {
- Cpu float64 `json:"cpu"`
- Mem float64 `json:"mp"`
- Disk float64 `json:"dp"`
- NetSent float64 `json:"ns"`
- NetRecv float64 `json:"nr"`
- GPU map[string]SystemAlertGPUData `json:"g"`
- Temperatures map[string]float32 `json:"t"`
- LoadAvg [3]float64 `json:"la"`
- Battery [2]uint8 `json:"bat"`
- ExtraFs map[string]SystemAlertFsStats `json:"efs"`
+ Cpu float64 `json:"cpu"`
+ Mem float64 `json:"mp"`
+ Disk float64 `json:"dp"`
+ Bandwidth [2]uint64 `json:"b"`
+ GPU map[string]SystemAlertGPUData `json:"g"`
+ Temperatures map[string]float32 `json:"t"`
+ LoadAvg [3]float64 `json:"la"`
+ Battery [2]uint8 `json:"bat"`
+ ExtraFs map[string]SystemAlertFsStats `json:"efs"`
}
type SystemAlertGPUData struct {
@@ -265,13 +265,14 @@ func (am *AlertManager) SendShoutrrrAlert(notificationUrl, title, message, link,
}
// Add link
- if scheme == "ntfy" {
+ switch scheme {
+ case "ntfy":
queryParams.Add("Actions", fmt.Sprintf("view, %s, %s", linkText, link))
- } else if scheme == "lark" {
+ case "lark":
queryParams.Add("link", link)
- } else if scheme == "bark" {
+ case "bark":
queryParams.Add("url", link)
- } else {
+ default:
message += "\n\n" + link
}
diff --git a/internal/alerts/alerts_system.go b/internal/alerts/alerts_system.go
index df48a944..a1a8ec5c 100644
--- a/internal/alerts/alerts_system.go
+++ b/internal/alerts/alerts_system.go
@@ -11,7 +11,6 @@ import (
"github.com/pocketbase/dbx"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/types"
- "github.com/spf13/cast"
)
func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *system.CombinedData) error {
@@ -92,7 +91,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
}
}
- min := max(1, cast.ToUint8(alertRecord.Get("min")))
+ min := max(1, uint8(alertRecord.GetInt("min")))
alert := SystemAlertData{
systemRecord: systemRecord,
@@ -192,7 +191,7 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
case "Memory":
alert.val += stats.Mem
case "Bandwidth":
- alert.val += stats.NetSent + stats.NetRecv
+ alert.val += float64(stats.Bandwidth[0]+stats.Bandwidth[1]) / (1024 * 1024)
case "Disk":
if alert.mapSums == nil {
alert.mapSums = make(map[string]float32, len(stats.ExtraFs)+1)
diff --git a/internal/entities/system/system.go b/internal/entities/system/system.go
index 2a5f50f6..6ac601d0 100644
--- a/internal/entities/system/system.go
+++ b/internal/entities/system/system.go
@@ -12,8 +12,9 @@ import (
type Stats struct {
Cpu float64 `json:"cpu" cbor:"0,keyasint"`
- MaxCpu float64 `json:"cpum,omitempty" cbor:"1,keyasint,omitempty"`
+ MaxCpu float64 `json:"cpum,omitempty" cbor:"-"`
Mem float64 `json:"m" cbor:"2,keyasint"`
+ MaxMem float64 `json:"mm,omitempty" cbor:"-"`
MemUsed float64 `json:"mu" cbor:"3,keyasint"`
MemPct float64 `json:"mp" cbor:"4,keyasint"`
MemBuffCache float64 `json:"mb" cbor:"5,keyasint"`
@@ -23,26 +24,25 @@ type Stats struct {
DiskTotal float64 `json:"d" cbor:"9,keyasint"`
DiskUsed float64 `json:"du" cbor:"10,keyasint"`
DiskPct float64 `json:"dp" cbor:"11,keyasint"`
- DiskReadPs float64 `json:"dr" cbor:"12,keyasint"`
- DiskWritePs float64 `json:"dw" cbor:"13,keyasint"`
- MaxDiskReadPs float64 `json:"drm,omitempty" cbor:"14,keyasint,omitempty"`
- MaxDiskWritePs float64 `json:"dwm,omitempty" cbor:"15,keyasint,omitempty"`
+ DiskReadPs float64 `json:"dr,omitzero" cbor:"12,keyasint,omitzero"`
+ DiskWritePs float64 `json:"dw,omitzero" cbor:"13,keyasint,omitzero"`
+ MaxDiskReadPs float64 `json:"drm,omitempty" cbor:"-"`
+ MaxDiskWritePs float64 `json:"dwm,omitempty" cbor:"-"`
NetworkSent float64 `json:"ns,omitzero" cbor:"16,keyasint,omitzero"`
NetworkRecv float64 `json:"nr,omitzero" cbor:"17,keyasint,omitzero"`
- MaxNetworkSent float64 `json:"nsm,omitempty" cbor:"18,keyasint,omitempty"`
- MaxNetworkRecv float64 `json:"nrm,omitempty" cbor:"19,keyasint,omitempty"`
+ MaxNetworkSent float64 `json:"nsm,omitempty" cbor:"-"`
+ MaxNetworkRecv float64 `json:"nrm,omitempty" cbor:"-"`
Temperatures map[string]float64 `json:"t,omitempty" cbor:"20,keyasint,omitempty"`
ExtraFs map[string]*FsStats `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
GPUData map[string]GPUData `json:"g,omitempty" cbor:"22,keyasint,omitempty"`
- LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty"`
- LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty"`
- LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"`
- Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
- MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"27,keyasint,omitzero"` // [sent bytes, recv bytes]
+ // LoadAvg1 float64 `json:"l1,omitempty" cbor:"23,keyasint,omitempty"`
+ // LoadAvg5 float64 `json:"l5,omitempty" cbor:"24,keyasint,omitempty"`
+ // LoadAvg15 float64 `json:"l15,omitempty" cbor:"25,keyasint,omitempty"`
+ Bandwidth [2]uint64 `json:"b,omitzero" cbor:"26,keyasint,omitzero"` // [sent bytes, recv bytes]
+ MaxBandwidth [2]uint64 `json:"bm,omitzero" cbor:"-"` // [sent bytes, recv bytes]
// TODO: remove other load fields in future release in favor of load avg array
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
- Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
- MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
+ Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
NetworkInterfaces map[string][4]uint64 `json:"ni,omitempty" cbor:"31,keyasint,omitempty"` // [upload bytes, download bytes, total upload, total download]
DiskIO [2]uint64 `json:"dio,omitzero" cbor:"32,keyasint,omitzero"` // [read bytes, write bytes]
MaxDiskIO [2]uint64 `json:"diom,omitzero" cbor:"-"` // [max read bytes, max write bytes]
@@ -90,8 +90,8 @@ type FsStats struct {
TotalWrite uint64 `json:"-"`
DiskReadPs float64 `json:"r" cbor:"2,keyasint"`
DiskWritePs float64 `json:"w" cbor:"3,keyasint"`
- MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"4,keyasint,omitempty"`
- MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"5,keyasint,omitempty"`
+ MaxDiskReadPS float64 `json:"rm,omitempty" cbor:"-"`
+ MaxDiskWritePS float64 `json:"wm,omitempty" cbor:"-"`
// TODO: remove DiskReadPs and DiskWritePs in future release in favor of DiskReadBytes and DiskWriteBytes
DiskReadBytes uint64 `json:"rb" cbor:"6,keyasint,omitempty"`
DiskWriteBytes uint64 `json:"wb" cbor:"7,keyasint,omitempty"`
@@ -129,23 +129,23 @@ type Info struct {
KernelVersion string `json:"k,omitempty" cbor:"1,keyasint,omitempty"` // deprecated - moved to Details struct
Cores int `json:"c,omitzero" cbor:"2,keyasint,omitzero"` // deprecated - moved to Details struct
// Threads is needed in Info struct to calculate load average thresholds
- Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
- CpuModel string `json:"m,omitempty" cbor:"4,keyasint,omitempty"` // deprecated - moved to Details struct
- Uptime uint64 `json:"u" cbor:"5,keyasint"`
- Cpu float64 `json:"cpu" cbor:"6,keyasint"`
- MemPct float64 `json:"mp" cbor:"7,keyasint"`
- DiskPct float64 `json:"dp" cbor:"8,keyasint"`
- Bandwidth float64 `json:"b" cbor:"9,keyasint"`
- AgentVersion string `json:"v" cbor:"10,keyasint"`
- Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` // deprecated - moved to Details struct
- GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
- DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
- Os Os `json:"os,omitempty" cbor:"14,keyasint,omitempty"` // deprecated - moved to Details struct
- LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` // deprecated - use `la` array instead
- LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` // deprecated - use `la` array instead
- LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` // deprecated - use `la` array instead
- BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
+ Threads int `json:"t,omitempty" cbor:"3,keyasint,omitempty"`
+ CpuModel string `json:"m,omitempty" cbor:"4,keyasint,omitempty"` // deprecated - moved to Details struct
+ Uptime uint64 `json:"u" cbor:"5,keyasint"`
+ Cpu float64 `json:"cpu" cbor:"6,keyasint"`
+ MemPct float64 `json:"mp" cbor:"7,keyasint"`
+ DiskPct float64 `json:"dp" cbor:"8,keyasint"`
+ Bandwidth float64 `json:"b,omitzero" cbor:"9,keyasint"` // deprecated in favor of BandwidthBytes
+ AgentVersion string `json:"v" cbor:"10,keyasint"`
+ Podman bool `json:"p,omitempty" cbor:"11,keyasint,omitempty"` // deprecated - moved to Details struct
+ GpuPct float64 `json:"g,omitempty" cbor:"12,keyasint,omitempty"`
+ DashboardTemp float64 `json:"dt,omitempty" cbor:"13,keyasint,omitempty"`
+ Os Os `json:"os,omitempty" cbor:"14,keyasint,omitempty"` // deprecated - moved to Details struct
+ // LoadAvg1 float64 `json:"l1,omitempty" cbor:"15,keyasint,omitempty"` // deprecated - use `la` array instead
+ // LoadAvg5 float64 `json:"l5,omitempty" cbor:"16,keyasint,omitempty"` // deprecated - use `la` array instead
+ // LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"` // deprecated - use `la` array instead
+ BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
diff --git a/internal/hub/systems/system.go b/internal/hub/systems/system.go
index 31e2c11e..1da3706e 100644
--- a/internal/hub/systems/system.go
+++ b/internal/hub/systems/system.go
@@ -133,6 +133,9 @@ func (sys *System) update() error {
return err
}
+ // ensure deprecated fields from older agents are migrated to current fields
+ migrateDeprecatedFields(data, !sys.detailsFetched.Load())
+
// create system records
_, err = sys.createRecords(data)
@@ -702,3 +705,50 @@ func getJitter() <-chan time.Time {
msDelay := (interval * minPercent / 100) + rand.Intn(interval*jitterRange/100)
return time.After(time.Duration(msDelay) * time.Millisecond)
}
+
+// migrateDeprecatedFields moves values from deprecated fields to their new locations if the new
+// fields are not already populated. Deprecated fields and refs may be removed at least 30 days
+// and one minor version release after the release that includes the migration.
+//
+// This is run when processing incoming system data from agents, which may be on older versions.
+func migrateDeprecatedFields(cd *system.CombinedData, createDetails bool) {
+ // migration added 0.19.0
+ if cd.Stats.Bandwidth[0] == 0 && cd.Stats.Bandwidth[1] == 0 {
+ cd.Stats.Bandwidth[0] = uint64(cd.Stats.NetworkSent * 1024 * 1024)
+ cd.Stats.Bandwidth[1] = uint64(cd.Stats.NetworkRecv * 1024 * 1024)
+ cd.Stats.NetworkSent, cd.Stats.NetworkRecv = 0, 0
+ }
+ // migration added 0.19.0
+ if cd.Info.BandwidthBytes == 0 {
+ cd.Info.BandwidthBytes = uint64(cd.Info.Bandwidth * 1024 * 1024)
+ cd.Info.Bandwidth = 0
+ }
+ // migration added 0.19.0
+ if cd.Stats.DiskIO[0] == 0 && cd.Stats.DiskIO[1] == 0 {
+ cd.Stats.DiskIO[0] = uint64(cd.Stats.DiskReadPs * 1024 * 1024)
+ cd.Stats.DiskIO[1] = uint64(cd.Stats.DiskWritePs * 1024 * 1024)
+ cd.Stats.DiskReadPs, cd.Stats.DiskWritePs = 0, 0
+ }
+ // migration added 0.19.0 - Move deprecated Info fields to Details struct
+ if cd.Details == nil && cd.Info.Hostname != "" {
+ if createDetails {
+ cd.Details = &system.Details{
+ Hostname: cd.Info.Hostname,
+ Kernel: cd.Info.KernelVersion,
+ Cores: cd.Info.Cores,
+ Threads: cd.Info.Threads,
+ CpuModel: cd.Info.CpuModel,
+ Podman: cd.Info.Podman,
+ Os: cd.Info.Os,
+ MemoryTotal: uint64(cd.Stats.Mem * 1024 * 1024 * 1024),
+ }
+ }
+ // zero the deprecated fields to prevent saving them in systems.info DB json payload
+ cd.Info.Hostname = ""
+ cd.Info.KernelVersion = ""
+ cd.Info.Cores = 0
+ cd.Info.CpuModel = ""
+ cd.Info.Podman = false
+ cd.Info.Os = 0
+ }
+}
diff --git a/internal/hub/systems/system_test.go b/internal/hub/systems/system_test.go
new file mode 100644
index 00000000..4fbff5b4
--- /dev/null
+++ b/internal/hub/systems/system_test.go
@@ -0,0 +1,159 @@
+//go:build testing
+
+package systems
+
+import (
+ "testing"
+
+ "github.com/henrygd/beszel/internal/entities/system"
+)
+
+func TestCombinedData_MigrateDeprecatedFields(t *testing.T) {
+ t.Run("Migrate NetworkSent and NetworkRecv to Bandwidth", func(t *testing.T) {
+ cd := &system.CombinedData{
+ Stats: system.Stats{
+ NetworkSent: 1.5, // 1.5 MB
+ NetworkRecv: 2.5, // 2.5 MB
+ },
+ }
+ migrateDeprecatedFields(cd, true)
+
+ expectedSent := uint64(1.5 * 1024 * 1024)
+ expectedRecv := uint64(2.5 * 1024 * 1024)
+
+ if cd.Stats.Bandwidth[0] != expectedSent {
+ t.Errorf("expected Bandwidth[0] %d, got %d", expectedSent, cd.Stats.Bandwidth[0])
+ }
+ if cd.Stats.Bandwidth[1] != expectedRecv {
+ t.Errorf("expected Bandwidth[1] %d, got %d", expectedRecv, cd.Stats.Bandwidth[1])
+ }
+ if cd.Stats.NetworkSent != 0 || cd.Stats.NetworkRecv != 0 {
+ t.Errorf("expected NetworkSent and NetworkRecv to be reset, got %f, %f", cd.Stats.NetworkSent, cd.Stats.NetworkRecv)
+ }
+ })
+
+ t.Run("Migrate Info.Bandwidth to Info.BandwidthBytes", func(t *testing.T) {
+ cd := &system.CombinedData{
+ Info: system.Info{
+ Bandwidth: 10.0, // 10 MB
+ },
+ }
+ migrateDeprecatedFields(cd, true)
+
+ expected := uint64(10 * 1024 * 1024)
+ if cd.Info.BandwidthBytes != expected {
+ t.Errorf("expected BandwidthBytes %d, got %d", expected, cd.Info.BandwidthBytes)
+ }
+ if cd.Info.Bandwidth != 0 {
+ t.Errorf("expected Info.Bandwidth to be reset, got %f", cd.Info.Bandwidth)
+ }
+ })
+
+ t.Run("Migrate DiskReadPs and DiskWritePs to DiskIO", func(t *testing.T) {
+ cd := &system.CombinedData{
+ Stats: system.Stats{
+ DiskReadPs: 3.0, // 3 MB
+ DiskWritePs: 4.0, // 4 MB
+ },
+ }
+ migrateDeprecatedFields(cd, true)
+
+ expectedRead := uint64(3 * 1024 * 1024)
+ expectedWrite := uint64(4 * 1024 * 1024)
+
+ if cd.Stats.DiskIO[0] != expectedRead {
+ t.Errorf("expected DiskIO[0] %d, got %d", expectedRead, cd.Stats.DiskIO[0])
+ }
+ if cd.Stats.DiskIO[1] != expectedWrite {
+ t.Errorf("expected DiskIO[1] %d, got %d", expectedWrite, cd.Stats.DiskIO[1])
+ }
+ if cd.Stats.DiskReadPs != 0 || cd.Stats.DiskWritePs != 0 {
+ t.Errorf("expected DiskReadPs and DiskWritePs to be reset, got %f, %f", cd.Stats.DiskReadPs, cd.Stats.DiskWritePs)
+ }
+ })
+
+ t.Run("Migrate Info fields to Details struct", func(t *testing.T) {
+ cd := &system.CombinedData{
+ Stats: system.Stats{
+ Mem: 16.0, // 16 GB
+ },
+ Info: system.Info{
+ Hostname: "test-host",
+ KernelVersion: "6.8.0",
+ Cores: 8,
+ Threads: 16,
+ CpuModel: "Intel i7",
+ Podman: true,
+ Os: system.Linux,
+ },
+ }
+ migrateDeprecatedFields(cd, true)
+
+ if cd.Details == nil {
+ t.Fatal("expected Details struct to be created")
+ }
+ if cd.Details.Hostname != "test-host" {
+ t.Errorf("expected Hostname 'test-host', got '%s'", cd.Details.Hostname)
+ }
+ if cd.Details.Kernel != "6.8.0" {
+ t.Errorf("expected Kernel '6.8.0', got '%s'", cd.Details.Kernel)
+ }
+ if cd.Details.Cores != 8 {
+ t.Errorf("expected Cores 8, got %d", cd.Details.Cores)
+ }
+ if cd.Details.Threads != 16 {
+ t.Errorf("expected Threads 16, got %d", cd.Details.Threads)
+ }
+ if cd.Details.CpuModel != "Intel i7" {
+ t.Errorf("expected CpuModel 'Intel i7', got '%s'", cd.Details.CpuModel)
+ }
+ if cd.Details.Podman != true {
+ t.Errorf("expected Podman true, got %v", cd.Details.Podman)
+ }
+ if cd.Details.Os != system.Linux {
+ t.Errorf("expected Os Linux, got %d", cd.Details.Os)
+ }
+ expectedMem := uint64(16 * 1024 * 1024 * 1024)
+ if cd.Details.MemoryTotal != expectedMem {
+ t.Errorf("expected MemoryTotal %d, got %d", expectedMem, cd.Details.MemoryTotal)
+ }
+
+ if cd.Info.Hostname != "" || cd.Info.KernelVersion != "" || cd.Info.Cores != 0 || cd.Info.CpuModel != "" || cd.Info.Podman != false || cd.Info.Os != 0 {
+ t.Errorf("expected Info fields to be reset, got %+v", cd.Info)
+ }
+ })
+
+ t.Run("Do not migrate if Details already exists", func(t *testing.T) {
+ cd := &system.CombinedData{
+ Details: &system.Details{Hostname: "existing-host"},
+ Info: system.Info{
+ Hostname: "deprecated-host",
+ },
+ }
+ migrateDeprecatedFields(cd, true)
+
+ if cd.Details.Hostname != "existing-host" {
+ t.Errorf("expected Hostname 'existing-host', got '%s'", cd.Details.Hostname)
+ }
+ if cd.Info.Hostname != "deprecated-host" {
+ t.Errorf("expected Info.Hostname to remain 'deprecated-host', got '%s'", cd.Info.Hostname)
+ }
+ })
+
+ t.Run("Do not create details if migrateDetails is false", func(t *testing.T) {
+ cd := &system.CombinedData{
+ Info: system.Info{
+ Hostname: "deprecated-host",
+ },
+ }
+ migrateDeprecatedFields(cd, false)
+
+ if cd.Details != nil {
+ t.Fatal("expected Details struct to not be created")
+ }
+
+ if cd.Info.Hostname != "" {
+ t.Errorf("expected Info.Hostname to be reset, got '%s'", cd.Info.Hostname)
+ }
+ })
+}
diff --git a/internal/site/src/components/charts/load-average-chart.tsx b/internal/site/src/components/charts/load-average-chart.tsx
index c8763638..f79c013b 100644
--- a/internal/site/src/components/charts/load-average-chart.tsx
+++ b/internal/site/src/components/charts/load-average-chart.tsx
@@ -16,19 +16,16 @@ import { useYAxisWidth } from "./hooks"
export default memo(function LoadAverageChart({ chartData }: { chartData: ChartData }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
- const keys: { legacy: keyof SystemStats; color: string; label: string }[] = [
+ const keys: { color: string; label: string }[] = [
{
- legacy: "l1",
color: "hsl(271, 81%, 60%)", // Purple
label: t({ message: `1 min`, comment: "Load average" }),
},
{
- legacy: "l5",
color: "hsl(217, 91%, 60%)", // Blue
label: t({ message: `5 min`, comment: "Load average" }),
},
{
- legacy: "l15",
color: "hsl(25, 95%, 53%)", // Orange
label: t({ message: `15 min`, comment: "Load average" }),
},
@@ -66,27 +63,18 @@ export default memo(function LoadAverageChart({ chartData }: { chartData: ChartD
/>
}
/>
- {keys.map(({ legacy, color, label }, i) => {
- const dataKey = (value: { stats: SystemStats }) => {
- const { minor, patch } = chartData.agentVersion
- if (minor <= 12 && patch < 1) {
- return value.stats?.[legacy]
- }
- return value.stats?.la?.[i] ?? value.stats?.[legacy]
- }
- return (
-
- )
- })}
+ {keys.map(({ color, label }, i) => (
+ value.stats?.la?.[i]}
+ name={label}
+ type="monotoneX"
+ dot={false}
+ strokeWidth={1.5}
+ stroke={color}
+ isAnimationActive={false}
+ />
+ ))}
} />
diff --git a/internal/site/src/components/routes/system.tsx b/internal/site/src/components/routes/system.tsx
index 7d2bab7b..9959f844 100644
--- a/internal/site/src/components/routes/system.tsx
+++ b/internal/site/src/components/routes/system.tsx
@@ -654,7 +654,7 @@ export default memo(function SystemDetail({ id }: { id: string }) {
)}
{/* Load Average chart */}
- {chartData.agentVersion?.minor >= 12 && (
+ {chartData.agentVersion?.minor > 12 && (
{
- const sum = info.la?.reduce((acc, curr) => acc + curr, 0)
- // TODO: remove this in future release in favor of la array
- if (!sum) {
- return (info.l1 ?? 0) + (info.l5 ?? 0) + (info.l15 ?? 0) || undefined
- }
- return sum || undefined
- },
+ accessorFn: ({ info }) => info.la?.reduce((acc, curr) => acc + curr, 0),
name: () => t({ message: "Load Avg", comment: "Short label for load average" }),
size: 0,
Icon: HourglassIcon,
header: sortableHeader,
cell(info: CellContext) {
const { info: sysInfo, status } = info.row.original
+ const { major, minor } = parseSemVer(sysInfo.v)
const { colorWarn = 65, colorCrit = 90 } = useStore($userSettings, { keys: ["colorWarn", "colorCrit"] })
- // agent version
- const { minor, patch } = parseSemVer(sysInfo.v)
- let loadAverages = sysInfo.la
-
- // use legacy load averages if agent version is less than 12.1.0
- if (!loadAverages || (minor === 12 && patch < 1)) {
- loadAverages = [sysInfo.l1 ?? 0, sysInfo.l5 ?? 0, sysInfo.l15 ?? 0]
- }
+ const loadAverages = sysInfo.la || []
const max = Math.max(...loadAverages)
- if (max === 0 && (status === SystemStatus.Paused || minor < 12)) {
+ if (max === 0 && (status === SystemStatus.Paused || (major < 1 && minor < 13))) {
return null
}
@@ -248,19 +235,20 @@ export function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef info.bb || (info.b || 0) * 1024 * 1024 || undefined,
+ accessorFn: ({ info, status }) => (status !== SystemStatus.Up ? undefined : info.bb),
id: "net",
name: () => t`Net`,
size: 0,
Icon: EthernetIcon,
header: sortableHeader,
+ sortUndefined: "last",
cell(info) {
- const sys = info.row.original
- const userSettings = useStore($userSettings, { keys: ["unitNet"] })
- if (sys.status === SystemStatus.Paused) {
+ const val = info.getValue() as number | undefined
+ if (val === undefined) {
return null
}
- const { value, unit } = formatBytes((info.getValue() || 0) as number, true, userSettings.unitNet, false)
+ const userSettings = useStore($userSettings, { keys: ["unitNet"] })
+ const { value, unit } = formatBytes(val, true, userSettings.unitNet, false)
return (
{decimalString(value, value >= 100 ? 1 : 2)} {unit}
diff --git a/internal/site/src/types.d.ts b/internal/site/src/types.d.ts
index 53dce80c..fe98e4f3 100644
--- a/internal/site/src/types.d.ts
+++ b/internal/site/src/types.d.ts
@@ -45,12 +45,6 @@ export interface SystemInfo {
c: number
/** cpu model */
m: string
- /** load average 1 minute */
- l1?: number
- /** load average 5 minutes */
- l5?: number
- /** load average 15 minutes */
- l15?: number
/** load average */
la?: [number, number, number]
/** operating system */
@@ -94,13 +88,6 @@ export interface SystemStats {
cpub?: number[]
/** per-core cpu usage [CPU0..] (0-100 integers) */
cpus?: number[]
- // TODO: remove these in future release in favor of la
- /** load average 1 minute */
- l1?: number
- /** load average 5 minutes */
- l5?: number
- /** load average 15 minutes */
- l15?: number
/** load average */
la?: [number, number, number]
/** total memory (gb) */