diff --git a/internal/monitoring/monitor.go b/internal/monitoring/monitor.go index 2d3d64076..86f88185e 100644 --- a/internal/monitoring/monitor.go +++ b/internal/monitoring/monitor.go @@ -3071,6 +3071,15 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config. memory.Usage = safePercentage(float64(memory.Used), float64(memory.Total)) } + smartSensors := convertAgentSMARTToModels(report.Sensors.SMART) + reportPlatform := strings.TrimSpace(strings.ToLower(report.Host.Platform)) + if len(smartSensors) == 0 && hasPrevious && len(previous.Sensors.SMART) > 0 && + (reportPlatform == "freebsd" || strings.EqualFold(previous.Platform, "freebsd")) { + // FreeBSD SMART collection can be intermittent; avoid wiping previously + // known disk temperatures on an otherwise healthy host report. + smartSensors = append([]models.HostDiskSMART(nil), previous.Sensors.SMART...) + } + disks := make([]models.Disk, 0, len(report.Disks)) for _, disk := range report.Disks { // Filter virtual/system filesystems and read-only filesystems to avoid cluttering @@ -3174,7 +3183,7 @@ func (m *Monitor) ApplyHostReport(report agentshost.Report, tokenRecord *config. TemperatureCelsius: cloneStringFloatMap(report.Sensors.TemperatureCelsius), FanRPM: cloneStringFloatMap(report.Sensors.FanRPM), Additional: cloneStringFloatMap(report.Sensors.Additional), - SMART: convertAgentSMARTToModels(report.Sensors.SMART), + SMART: smartSensors, }, RAID: raid, Ceph: cephData, diff --git a/internal/monitoring/monitor_host_agents_test.go b/internal/monitoring/monitor_host_agents_test.go index c913a6c95..6501a30fc 100644 --- a/internal/monitoring/monitor_host_agents_test.go +++ b/internal/monitoring/monitor_host_agents_test.go @@ -179,6 +179,73 @@ func TestApplyHostReportPersistsAndRemoveHostAgentClearsRuntime(t *testing.T) { } } +func TestApplyHostReportPreservesFreeBSDSMARTWhenNextReportOmitsIt(t *testing.T) { + t.Helper() + + monitor := &Monitor{ + state: models.NewState(), + alertManager: alerts.NewManager(), + hostTokenBindings: make(map[string]string), + config: &config.Config{}, + rateTracker: NewRateTracker(), + } + t.Cleanup(func() { monitor.alertManager.Stop() }) + + now := time.Now().UTC() + first := agentshost.Report{ + Agent: agentshost.AgentInfo{ + ID: "agent-freebsd", + Version: "1.0.0", + IntervalSeconds: 30, + }, + Host: agentshost.HostInfo{ + ID: "machine-freebsd", + Hostname: "pfsense.local", + Platform: "freebsd", + }, + Timestamp: now, + Metrics: agentshost.Metrics{ + CPUUsagePercent: 5.5, + }, + Sensors: agentshost.Sensors{ + SMART: []agentshost.DiskSMART{ + { + Device: "ada0", + Model: "Disk 0", + Temperature: 33, + Health: "PASSED", + }, + }, + }, + } + + host, err := monitor.ApplyHostReport(first, nil) + if err != nil { + t.Fatalf("ApplyHostReport first: %v", err) + } + if len(host.Sensors.SMART) != 1 { + t.Fatalf("expected initial SMART data, got %d entries", len(host.Sensors.SMART)) + } + + second := first + second.Timestamp = now.Add(30 * time.Second) + second.Sensors = agentshost.Sensors{} + + host, err = monitor.ApplyHostReport(second, nil) + if err != nil { + t.Fatalf("ApplyHostReport second: %v", err) + } + if len(host.Sensors.SMART) != 1 { + t.Fatalf("expected SMART data to be preserved, got %d entries", len(host.Sensors.SMART)) + } + if host.Sensors.SMART[0].Device != "ada0" { + t.Fatalf("expected preserved SMART device ada0, got %q", host.Sensors.SMART[0].Device) + } + if host.Sensors.SMART[0].Temperature != 33 { + t.Fatalf("expected preserved SMART temperature 33, got %d", host.Sensors.SMART[0].Temperature) + } +} + func TestClearUnauthenticatedAgentsClearsPersistedHostRuntime(t *testing.T) { dataDir := t.TempDir() runtimeStore := config.NewHostRuntimeStore(dataDir, nil)