From e5275340162c981a9b6f4e1a8946d97288abc4cd Mon Sep 17 00:00:00 2001 From: henrygd Date: Tue, 10 Mar 2026 18:46:57 -0400 Subject: [PATCH] ensure deprecated system fields are migrated to newer structures also removes refs to legacy load avg fields (l1, l5, l15) that were around for a very short period --- agent/network.go | 10 +- agent/network_test.go | 22 --- internal/alerts/alerts.go | 29 ++-- internal/alerts/alerts_system.go | 5 +- internal/entities/system/system.go | 64 +++---- internal/hub/systems/system.go | 50 ++++++ internal/hub/systems/system_test.go | 159 ++++++++++++++++++ .../components/charts/load-average-chart.tsx | 38 ++--- .../site/src/components/routes/system.tsx | 2 +- .../systems-table/systems-table-columns.tsx | 32 ++-- internal/site/src/types.d.ts | 13 -- 11 files changed, 284 insertions(+), 140 deletions(-) create mode 100644 internal/hub/systems/system_test.go 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) */