From 2fe7bb6141d6344c87e1ef318bff7564c9b61d43 Mon Sep 17 00:00:00 2001 From: rcourtman Date: Wed, 26 Nov 2025 23:44:36 +0000 Subject: [PATCH] style: fix gofmt formatting inconsistencies Run gofmt -w to fix tab/space inconsistencies across 33 files. --- cmd/pulse-host-agent/service_stub.go | 1 + cmd/pulse-host-agent/service_windows.go | 1 + cmd/pulse-sensor-proxy/audit_test.go | 104 +++---- .../validation_fuzz_test.go | 46 +-- cmd/pulse/metrics_server.go | 57 ++-- .../alerts/per_metric_delay_example_test.go | 14 +- internal/alerts/time_threshold_test.go | 12 +- internal/api/config_handlers_host_test.go | 52 ++-- internal/api/csrf_store.go | 4 +- internal/api/docker_metadata.go | 1 + internal/api/host_agents_test.go | 14 +- internal/api/types.go | 24 +- internal/logging/logging.go | 42 +-- internal/logging/logging_test.go | 30 +- internal/models/converters_test.go | 72 ++--- internal/models/models_frontend.go | 96 +++---- internal/models/state_snapshot.go | 86 +++--- internal/monitoring/backoff_test.go | 2 +- internal/monitoring/circuit_breaker.go | 24 +- internal/monitoring/circuit_breaker_test.go | 2 +- .../integration_integration_test.go | 22 +- internal/monitoring/scheduler.go | 44 +-- internal/monitoring/staleness_tracker.go | 266 +++++++++--------- internal/notifications/queue.go | 8 +- internal/sensors/parser_test.go | 18 +- internal/system/container_test.go | 2 +- internal/updates/queue.go | 20 +- internal/updates/updater.go | 18 +- pkg/agents/docker/report_test.go | 6 +- pkg/agents/host/report.go | 30 +- pkg/discovery/envdetect/envdetect.go | 14 +- pkg/proxmox/client.go | 34 +-- pkg/proxmox/client_test.go | 2 - 33 files changed, 584 insertions(+), 584 deletions(-) diff --git a/cmd/pulse-host-agent/service_stub.go b/cmd/pulse-host-agent/service_stub.go index 7abbd06..2eb0198 100644 --- a/cmd/pulse-host-agent/service_stub.go +++ b/cmd/pulse-host-agent/service_stub.go @@ -1,3 +1,4 @@ +//go:build !windows // +build !windows package main diff --git a/cmd/pulse-host-agent/service_windows.go b/cmd/pulse-host-agent/service_windows.go index 8b23cbb..7756e2a 100644 --- a/cmd/pulse-host-agent/service_windows.go +++ b/cmd/pulse-host-agent/service_windows.go @@ -1,3 +1,4 @@ +//go:build windows // +build windows package main diff --git a/cmd/pulse-sensor-proxy/audit_test.go b/cmd/pulse-sensor-proxy/audit_test.go index 8821874..c8404ca 100644 --- a/cmd/pulse-sensor-proxy/audit_test.go +++ b/cmd/pulse-sensor-proxy/audit_test.go @@ -1,70 +1,70 @@ package main import ( - "bufio" - "encoding/json" - "os" - "testing" + "bufio" + "encoding/json" + "os" + "testing" ) type auditRecord map[string]interface{} func TestAuditLogValidationFailure(t *testing.T) { - tmp, err := os.CreateTemp("", "audit-test-*.log") - if err != nil { - t.Fatalf("temp file: %v", err) - } - path := tmp.Name() - tmp.Close() - defer os.Remove(path) + tmp, err := os.CreateTemp("", "audit-test-*.log") + if err != nil { + t.Fatalf("temp file: %v", err) + } + path := tmp.Name() + tmp.Close() + defer os.Remove(path) - logger := newAuditLogger(path) + logger := newAuditLogger(path) - cred := &peerCredentials{uid: 1000, gid: 1000, pid: 4242} - logger.LogValidationFailure("corr-123", cred, "remote", "get_temperature", []string{"node"}, "invalid_node") - logger.Close() + cred := &peerCredentials{uid: 1000, gid: 1000, pid: 4242} + logger.LogValidationFailure("corr-123", cred, "remote", "get_temperature", []string{"node"}, "invalid_node") + logger.Close() - file, err := os.Open(path) - if err != nil { - t.Fatalf("open log: %v", err) - } - defer file.Close() + file, err := os.Open(path) + if err != nil { + t.Fatalf("open log: %v", err) + } + defer file.Close() - scanner := bufio.NewScanner(file) - if !scanner.Scan() { - t.Fatalf("expected at least one audit entry (file may be empty)") - } + scanner := bufio.NewScanner(file) + if !scanner.Scan() { + t.Fatalf("expected at least one audit entry (file may be empty)") + } - line := scanner.Bytes() - if len(line) == 0 { - t.Fatalf("empty line in audit log") - } + line := scanner.Bytes() + if len(line) == 0 { + t.Fatalf("empty line in audit log") + } - t.Logf("Audit log line: %s", string(line)) + t.Logf("Audit log line: %s", string(line)) - var record auditRecord - if err := json.Unmarshal(line, &record); err != nil { - t.Fatalf("unmarshal (line=%s): %v", string(line), err) - } + var record auditRecord + if err := json.Unmarshal(line, &record); err != nil { + t.Fatalf("unmarshal (line=%s): %v", string(line), err) + } - t.Logf("Parsed record: %+v", record) + t.Logf("Parsed record: %+v", record) - if record["event_type"] != "command.validation_failed" { - t.Fatalf("unexpected event_type: %v (full record: %+v)", record["event_type"], record) - } - if record["correlation_id"] != "corr-123" { - t.Fatalf("unexpected correlation id: %v", record["correlation_id"]) - } - if record["command"] != "get_temperature" { - t.Fatalf("unexpected command: %v", record["command"]) - } - if record["reason"] != "invalid_node" { - t.Fatalf("unexpected reason: %v", record["reason"]) - } - if record["decision"] != "denied" { - t.Fatalf("unexpected decision: %v", record["decision"]) - } - if record["event_hash"] == "" { - t.Fatalf("expected event_hash to be set") - } + if record["event_type"] != "command.validation_failed" { + t.Fatalf("unexpected event_type: %v (full record: %+v)", record["event_type"], record) + } + if record["correlation_id"] != "corr-123" { + t.Fatalf("unexpected correlation id: %v", record["correlation_id"]) + } + if record["command"] != "get_temperature" { + t.Fatalf("unexpected command: %v", record["command"]) + } + if record["reason"] != "invalid_node" { + t.Fatalf("unexpected reason: %v", record["reason"]) + } + if record["decision"] != "denied" { + t.Fatalf("unexpected decision: %v", record["decision"]) + } + if record["event_hash"] == "" { + t.Fatalf("expected event_hash to be set") + } } diff --git a/cmd/pulse-sensor-proxy/validation_fuzz_test.go b/cmd/pulse-sensor-proxy/validation_fuzz_test.go index b45e68a..b4b4c96 100644 --- a/cmd/pulse-sensor-proxy/validation_fuzz_test.go +++ b/cmd/pulse-sensor-proxy/validation_fuzz_test.go @@ -1,31 +1,31 @@ package main import ( - "strings" - "testing" + "strings" + "testing" ) func FuzzValidateCommand(f *testing.F) { - seeds := []string{ - "sensors -j", - "ipmitool sdr", - "sensors", - "ipmitool lan print", - } - for _, seed := range seeds { - f.Add(seed) - } + seeds := []string{ + "sensors -j", + "ipmitool sdr", + "sensors", + "ipmitool lan print", + } + for _, seed := range seeds { + f.Add(seed) + } - f.Fuzz(func(t *testing.T, input string) { - fields := strings.Fields(input) - if len(fields) == 0 { - return - } - cmd := fields[0] - args := []string{} - if len(fields) > 1 { - args = fields[1:] - } - validateCommand(cmd, args) // ensure no panics - }) + f.Fuzz(func(t *testing.T, input string) { + fields := strings.Fields(input) + if len(fields) == 0 { + return + } + cmd := fields[0] + args := []string{} + if len(fields) > 1 { + args = fields[1:] + } + validateCommand(cmd, args) // ensure no panics + }) } diff --git a/cmd/pulse/metrics_server.go b/cmd/pulse/metrics_server.go index 4ee5de5..30682cd 100644 --- a/cmd/pulse/metrics_server.go +++ b/cmd/pulse/metrics_server.go @@ -1,40 +1,39 @@ package main import ( - "context" - "net/http" - "time" + "context" + "net/http" + "time" - "github.com/prometheus/client_golang/prometheus/promhttp" - "github.com/rs/zerolog/log" + "github.com/prometheus/client_golang/prometheus/promhttp" + "github.com/rs/zerolog/log" ) func startMetricsServer(ctx context.Context, addr string) { - mux := http.NewServeMux() - mux.Handle("/metrics", promhttp.Handler()) + mux := http.NewServeMux() + mux.Handle("/metrics", promhttp.Handler()) - srv := &http.Server{ - Addr: addr, - Handler: mux, - ReadTimeout: 5 * time.Second, - WriteTimeout: 10 * time.Second, - IdleTimeout: 30 * time.Second, - } + srv := &http.Server{ + Addr: addr, + Handler: mux, + ReadTimeout: 5 * time.Second, + WriteTimeout: 10 * time.Second, + IdleTimeout: 30 * time.Second, + } - go func() { - <-ctx.Done() - shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) - defer cancel() - if err := srv.Shutdown(shutdownCtx); err != nil && err != http.ErrServerClosed { - log.Warn().Err(err).Msg("Failed to shut down metrics server cleanly") - } - }() + go func() { + <-ctx.Done() + shutdownCtx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + if err := srv.Shutdown(shutdownCtx); err != nil && err != http.ErrServerClosed { + log.Warn().Err(err).Msg("Failed to shut down metrics server cleanly") + } + }() - go func() { - log.Info().Str("addr", addr).Msg("Metrics endpoint listening") - if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { - log.Warn().Err(err).Msg("Metrics server stopped unexpectedly") - } - }() + go func() { + log.Info().Str("addr", addr).Msg("Metrics endpoint listening") + if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { + log.Warn().Err(err).Msg("Metrics server stopped unexpectedly") + } + }() } - diff --git a/internal/alerts/per_metric_delay_example_test.go b/internal/alerts/per_metric_delay_example_test.go index 026411a..cef361e 100644 --- a/internal/alerts/per_metric_delay_example_test.go +++ b/internal/alerts/per_metric_delay_example_test.go @@ -10,12 +10,12 @@ import ( // This test shows how to configure different alert delays for different metrics func TestPerMetricDelayConfiguration(t *testing.T) { tests := []struct { - name string - config AlertConfig - resourceType string - metricType string - expectedDelay int - description string + name string + config AlertConfig + resourceType string + metricType string + expectedDelay int + description string }{ { name: "CPU alert with longer delay than memory", @@ -117,7 +117,7 @@ func TestPerMetricDelayConfiguration(t *testing.T) { }, MetricTimeThresholds: map[string]map[string]int{ "docker": { - "restartcount": 10, // Quick notification for container restarts + "restartcount": 10, // Quick notification for container restarts "cpu": 120, // Longer for CPU }, }, diff --git a/internal/alerts/time_threshold_test.go b/internal/alerts/time_threshold_test.go index 32e79ac..252c8d7 100644 --- a/internal/alerts/time_threshold_test.go +++ b/internal/alerts/time_threshold_test.go @@ -70,12 +70,12 @@ func TestGetTimeThresholdMetricOverrides(t *testing.T) { metricType string expected int }{ - {"vm-resource", "VM", "cpu", 5}, // guest metric override - {"vm-resource", "VM", "memory", 30}, // falls back to guest type delay - {"node-1", "Node", "temperature", 120}, // node metric override - {"node-1", "Node", "cpu", 60}, // node type delay - {"storage-1", "storage", "usage", 90}, // storage type delay - {"unknown", "unknown", "cpu", 20}, // global default metric override + {"vm-resource", "VM", "cpu", 5}, // guest metric override + {"vm-resource", "VM", "memory", 30}, // falls back to guest type delay + {"node-1", "Node", "temperature", 120}, // node metric override + {"node-1", "Node", "cpu", 60}, // node type delay + {"storage-1", "storage", "usage", 90}, // storage type delay + {"unknown", "unknown", "cpu", 20}, // global default metric override {"unknown", "unknown", "disk", 20}, } diff --git a/internal/api/config_handlers_host_test.go b/internal/api/config_handlers_host_test.go index 8bf1729..7869c94 100644 --- a/internal/api/config_handlers_host_test.go +++ b/internal/api/config_handlers_host_test.go @@ -6,19 +6,19 @@ func TestNormalizeNodeHost(t *testing.T) { tests := []struct { name string rawHost string - nodeType string - want string -}{ - { - name: "adds default port to explicit https without port", - rawHost: "https://example.com", - nodeType: "pve", - want: "https://example.com:8006", - }, - { - name: "adds default port for bare pve host", - rawHost: "pve.lan", - nodeType: "pve", + nodeType string + want string + }{ + { + name: "adds default port to explicit https without port", + rawHost: "https://example.com", + nodeType: "pve", + want: "https://example.com:8006", + }, + { + name: "adds default port for bare pve host", + rawHost: "pve.lan", + nodeType: "pve", want: "https://pve.lan:8006", }, { @@ -39,19 +39,19 @@ func TestNormalizeNodeHost(t *testing.T) { nodeType: "pmg", want: "https://[2001:db8::1]:8006", }, - { - name: "drops path segments", - rawHost: "https://example.com/api", - nodeType: "pve", - want: "https://example.com:8006", - }, - { - name: "adds default port to explicit http scheme", - rawHost: "http://example.com", - nodeType: "pve", - want: "http://example.com:8006", - }, -} + { + name: "drops path segments", + rawHost: "https://example.com/api", + nodeType: "pve", + want: "https://example.com:8006", + }, + { + name: "adds default port to explicit http scheme", + rawHost: "http://example.com", + nodeType: "pve", + want: "http://example.com:8006", + }, + } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { diff --git a/internal/api/csrf_store.go b/internal/api/csrf_store.go index 558c592..aaec335 100644 --- a/internal/api/csrf_store.go +++ b/internal/api/csrf_store.go @@ -25,11 +25,11 @@ type CSRFToken struct { type CSRFTokenStore struct { tokens map[string]*CSRFToken mu sync.RWMutex - saveMu sync.Mutex // Serializes disk writes to prevent save corruption + saveMu sync.Mutex // Serializes disk writes to prevent save corruption dataPath string saveTicker *time.Ticker stopChan chan bool - stopOnce sync.Once // Ensures Stop() can only close channel once + stopOnce sync.Once // Ensures Stop() can only close channel once } func csrfSessionKey(sessionID string) string { diff --git a/internal/api/docker_metadata.go b/internal/api/docker_metadata.go index c99cb17..d988ad5 100644 --- a/internal/api/docker_metadata.go +++ b/internal/api/docker_metadata.go @@ -159,4 +159,5 @@ func (h *DockerMetadataHandler) HandleDeleteMetadata(w http.ResponseWriter, r *h w.WriteHeader(http.StatusNoContent) } + // test comment diff --git a/internal/api/host_agents_test.go b/internal/api/host_agents_test.go index 4506995..5534b23 100644 --- a/internal/api/host_agents_test.go +++ b/internal/api/host_agents_test.go @@ -22,12 +22,12 @@ func TestHandleLookupByIDSuccess(t *testing.T) { lastSeen := time.Now().UTC() handler := newHostAgentHandlerForTests(t, models.Host{ - ID: hostID, - Hostname: "host.local", - DisplayName:"Host Local", - Status: "online", - TokenID: tokenID, - LastSeen: lastSeen, + ID: hostID, + Hostname: "host.local", + DisplayName: "Host Local", + Status: "online", + TokenID: tokenID, + LastSeen: lastSeen, }) req := httptest.NewRequest(http.MethodGet, "/api/agents/host/lookup?id="+hostID, nil) @@ -42,7 +42,7 @@ func TestHandleLookupByIDSuccess(t *testing.T) { var resp struct { Success bool `json:"success"` - Host struct { + Host struct { ID string `json:"id"` Hostname string `json:"hostname"` Status string `json:"status"` diff --git a/internal/api/types.go b/internal/api/types.go index 206f9de..be5c237 100644 --- a/internal/api/types.go +++ b/internal/api/types.go @@ -81,18 +81,18 @@ type ConfigResponse struct { // NodeConfig represents a node configuration type NodeConfig struct { - ID string `json:"id"` - Name string `json:"name"` - Type string `json:"type"` - Address string `json:"address"` - Port int `json:"port,omitempty"` - Username string `json:"username,omitempty"` - HasPassword bool `json:"hasPassword"` - HasToken bool `json:"hasToken"` - SkipTLS bool `json:"skipTLS,omitempty"` - TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"` - Tags []string `json:"tags,omitempty"` - Metadata map[string]string `json:"metadata,omitempty"` + ID string `json:"id"` + Name string `json:"name"` + Type string `json:"type"` + Address string `json:"address"` + Port int `json:"port,omitempty"` + Username string `json:"username,omitempty"` + HasPassword bool `json:"hasPassword"` + HasToken bool `json:"hasToken"` + SkipTLS bool `json:"skipTLS,omitempty"` + TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"` + Tags []string `json:"tags,omitempty"` + Metadata map[string]string `json:"metadata,omitempty"` } // SettingsConfig represents application settings diff --git a/internal/logging/logging.go b/internal/logging/logging.go index be47056..3559ae1 100644 --- a/internal/logging/logging.go +++ b/internal/logging/logging.go @@ -28,13 +28,13 @@ const ( // Config controls logger initialization. type Config struct { - Format string // "json", "console", or "auto" - Level string // "debug", "info", "warn", "error" - Component string // optional component name - FilePath string // optional log file path - MaxSizeMB int // rotate after this size (MB) - MaxAgeDays int // keep rotated logs for this many days - Compress bool // gzip rotated logs + Format string // "json", "console", or "auto" + Level string // "debug", "info", "warn", "error" + Component string // optional component name + FilePath string // optional log file path + MaxSizeMB int // rotate after this size (MB) + MaxAgeDays int // keep rotated logs for this many days + Compress bool // gzip rotated logs } // Option customizes logger construction. @@ -271,16 +271,16 @@ func collectOptions(opts ...Option) options { } func parseLevel(level string) zerolog.Level { - switch strings.ToLower(strings.TrimSpace(level)) { - case "debug": - return zerolog.DebugLevel - case "warn": - return zerolog.WarnLevel - case "error": - return zerolog.ErrorLevel - default: - return zerolog.InfoLevel - } + switch strings.ToLower(strings.TrimSpace(level)) { + case "debug": + return zerolog.DebugLevel + case "warn": + return zerolog.WarnLevel + case "error": + return zerolog.ErrorLevel + default: + return zerolog.InfoLevel + } } func isValidLevel(level string) bool { @@ -310,10 +310,10 @@ func selectWriter(format string) io.Writer { } func newConsoleWriter(out io.Writer) io.Writer { - return zerolog.ConsoleWriter{ - Out: out, - TimeFormat: defaultTimeFmt, - } + return zerolog.ConsoleWriter{ + Out: out, + TimeFormat: defaultTimeFmt, + } } func isTerminal(file *os.File) bool { diff --git a/internal/logging/logging_test.go b/internal/logging/logging_test.go index 2bd9588..9d10094 100644 --- a/internal/logging/logging_test.go +++ b/internal/logging/logging_test.go @@ -222,14 +222,14 @@ func TestContextHelpersWithRequestID(t *testing.T) { t.Fatalf("expected stored request id %s, got %s", generated, got) } - var buf bytes.Buffer - logger := New("api", WithWriter(&buf)) - ctx = WithLogger(ctx, logger) + var buf bytes.Buffer + logger := New("api", WithWriter(&buf)) + ctx = WithLogger(ctx, logger) - info := FromContext(ctx) - info.Info().Msg("ctx-log") + info := FromContext(ctx) + info.Info().Msg("ctx-log") - event := readJSONLine(t, &buf) + event := readJSONLine(t, &buf) if event["request_id"] != generated { t.Fatalf("expected request_id %s, got %v", generated, event["request_id"]) } @@ -299,16 +299,16 @@ func TestFromContextWithoutRequestID(t *testing.T) { Level: "info", }) - var buf bytes.Buffer - mu.Lock() - baseLogger = zerolog.New(&buf).With().Timestamp().Logger() - baseWriter = &buf - baseComponent = "" - log.Logger = baseLogger - mu.Unlock() + var buf bytes.Buffer + mu.Lock() + baseLogger = zerolog.New(&buf).With().Timestamp().Logger() + baseWriter = &buf + baseComponent = "" + log.Logger = baseLogger + mu.Unlock() - base := FromContext(context.Background()) - base.Info().Msg("no-request") + base := FromContext(context.Background()) + base.Info().Msg("no-request") event := readJSONLine(t, &buf) if _, ok := event["request_id"]; ok { diff --git a/internal/models/converters_test.go b/internal/models/converters_test.go index b261732..f77e187 100644 --- a/internal/models/converters_test.go +++ b/internal/models/converters_test.go @@ -181,28 +181,28 @@ func TestVMToFrontend(t *testing.T) { lastBackup := now.Add(-24 * time.Hour) vm := VM{ - ID: "vm-100", - VMID: 100, - Name: "test-vm", - Node: "pve1", - Instance: "default", - Status: "running", - Type: "qemu", - CPU: 0.15, - CPUs: 4, - Memory: Memory{Total: 8000000000, Used: 4000000000}, - Disk: Disk{Total: 100000000000, Used: 50000000000}, - NetworkIn: 1000000, - NetworkOut: 500000, - DiskRead: 100000, - DiskWrite: 50000, - Uptime: 3600, - Tags: []string{"production", "web"}, - LastSeen: now, - LastBackup: lastBackup, + ID: "vm-100", + VMID: 100, + Name: "test-vm", + Node: "pve1", + Instance: "default", + Status: "running", + Type: "qemu", + CPU: 0.15, + CPUs: 4, + Memory: Memory{Total: 8000000000, Used: 4000000000}, + Disk: Disk{Total: 100000000000, Used: 50000000000}, + NetworkIn: 1000000, + NetworkOut: 500000, + DiskRead: 100000, + DiskWrite: 50000, + Uptime: 3600, + Tags: []string{"production", "web"}, + LastSeen: now, + LastBackup: lastBackup, IPAddresses: []string{"192.168.1.50", "10.0.0.50"}, - OSName: "Ubuntu", - OSVersion: "22.04", + OSName: "Ubuntu", + OSVersion: "22.04", } frontend := vm.ToFrontend() @@ -268,21 +268,21 @@ func TestContainerToFrontend(t *testing.T) { now := time.Now() container := Container{ - ID: "ct-101", - VMID: 101, - Name: "test-ct", - Node: "pve1", - Status: "running", - Type: "lxc", - CPU: 0.10, - CPUs: 2, - Memory: Memory{Total: 4000000000, Used: 2000000000}, - Disk: Disk{Total: 50000000000, Used: 25000000000}, - NetworkIn: 500000, - NetworkOut: 250000, - Uptime: 7200, - Tags: []string{"dev", "database"}, - LastSeen: now, + ID: "ct-101", + VMID: 101, + Name: "test-ct", + Node: "pve1", + Status: "running", + Type: "lxc", + CPU: 0.10, + CPUs: 2, + Memory: Memory{Total: 4000000000, Used: 2000000000}, + Disk: Disk{Total: 50000000000, Used: 25000000000}, + NetworkIn: 500000, + NetworkOut: 250000, + Uptime: 7200, + Tags: []string{"dev", "database"}, + LastSeen: now, IPAddresses: []string{"192.168.1.51"}, } diff --git a/internal/models/models_frontend.go b/internal/models/models_frontend.go index d22a4ec..cfbe29d 100644 --- a/internal/models/models_frontend.go +++ b/internal/models/models_frontend.go @@ -5,31 +5,31 @@ package models // NodeFrontend represents a Node with frontend-friendly field names type NodeFrontend struct { - ID string `json:"id"` - Node string `json:"node"` // Maps to Name - Name string `json:"name"` - DisplayName string `json:"displayName"` - Instance string `json:"instance"` - Host string `json:"host,omitempty"` - Status string `json:"status"` - Type string `json:"type"` - CPU float64 `json:"cpu"` - Memory *Memory `json:"memory,omitempty"` // Full memory object with usage percentage - Mem int64 `json:"mem"` // Maps to Memory.Used (kept for backward compat) - MaxMem int64 `json:"maxmem"` // Maps to Memory.Total (kept for backward compat) - Disk *Disk `json:"disk,omitempty"` // Full disk object with usage percentage - MaxDisk int64 `json:"maxdisk"` // Maps to Disk.Total (kept for backward compat) - Uptime int64 `json:"uptime"` - LoadAverage []float64 `json:"loadAverage"` - KernelVersion string `json:"kernelVersion"` - PVEVersion string `json:"pveVersion"` - CPUInfo CPUInfo `json:"cpuInfo"` - Temperature *Temperature `json:"temperature,omitempty"` // CPU/NVMe temperatures - LastSeen int64 `json:"lastSeen"` // Unix timestamp - ConnectionHealth string `json:"connectionHealth"` - IsClusterMember bool `json:"isClusterMember,omitempty"` - ClusterName string `json:"clusterName,omitempty"` - TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"` // Per-node temperature monitoring override + ID string `json:"id"` + Node string `json:"node"` // Maps to Name + Name string `json:"name"` + DisplayName string `json:"displayName"` + Instance string `json:"instance"` + Host string `json:"host,omitempty"` + Status string `json:"status"` + Type string `json:"type"` + CPU float64 `json:"cpu"` + Memory *Memory `json:"memory,omitempty"` // Full memory object with usage percentage + Mem int64 `json:"mem"` // Maps to Memory.Used (kept for backward compat) + MaxMem int64 `json:"maxmem"` // Maps to Memory.Total (kept for backward compat) + Disk *Disk `json:"disk,omitempty"` // Full disk object with usage percentage + MaxDisk int64 `json:"maxdisk"` // Maps to Disk.Total (kept for backward compat) + Uptime int64 `json:"uptime"` + LoadAverage []float64 `json:"loadAverage"` + KernelVersion string `json:"kernelVersion"` + PVEVersion string `json:"pveVersion"` + CPUInfo CPUInfo `json:"cpuInfo"` + Temperature *Temperature `json:"temperature,omitempty"` // CPU/NVMe temperatures + LastSeen int64 `json:"lastSeen"` // Unix timestamp + ConnectionHealth string `json:"connectionHealth"` + IsClusterMember bool `json:"isClusterMember,omitempty"` + ClusterName string `json:"clusterName,omitempty"` + TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"` // Per-node temperature monitoring override } // VMFrontend represents a VM with frontend-friendly field names @@ -421,27 +421,27 @@ type ReplicationJobFrontend struct { // StateFrontend represents the state with frontend-friendly field names type StateFrontend struct { - Nodes []NodeFrontend `json:"nodes"` - VMs []VMFrontend `json:"vms"` - Containers []ContainerFrontend `json:"containers"` - DockerHosts []DockerHostFrontend `json:"dockerHosts"` - RemovedDockerHosts []RemovedDockerHostFrontend `json:"removedDockerHosts"` - Hosts []HostFrontend `json:"hosts"` - Storage []StorageFrontend `json:"storage"` - CephClusters []CephClusterFrontend `json:"cephClusters"` - PhysicalDisks []PhysicalDisk `json:"physicalDisks"` - PBS []PBSInstance `json:"pbs"` // Keep as is - PMG []PMGInstance `json:"pmg"` - PBSBackups []PBSBackup `json:"pbsBackups"` - PMGBackups []PMGBackup `json:"pmgBackups"` - Backups Backups `json:"backups"` - ReplicationJobs []ReplicationJobFrontend `json:"replicationJobs"` - ActiveAlerts []Alert `json:"activeAlerts"` // Active alerts - Metrics map[string]any `json:"metrics"` // Empty object for now - PVEBackups PVEBackups `json:"pveBackups"` // Keep as is - Performance map[string]any `json:"performance"` // Empty object for now - ConnectionHealth map[string]bool `json:"connectionHealth"` // Keep as is - Stats map[string]any `json:"stats"` // Empty object for now - LastUpdate int64 `json:"lastUpdate"` // Unix timestamp - TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"` // Global temperature monitoring setting + Nodes []NodeFrontend `json:"nodes"` + VMs []VMFrontend `json:"vms"` + Containers []ContainerFrontend `json:"containers"` + DockerHosts []DockerHostFrontend `json:"dockerHosts"` + RemovedDockerHosts []RemovedDockerHostFrontend `json:"removedDockerHosts"` + Hosts []HostFrontend `json:"hosts"` + Storage []StorageFrontend `json:"storage"` + CephClusters []CephClusterFrontend `json:"cephClusters"` + PhysicalDisks []PhysicalDisk `json:"physicalDisks"` + PBS []PBSInstance `json:"pbs"` // Keep as is + PMG []PMGInstance `json:"pmg"` + PBSBackups []PBSBackup `json:"pbsBackups"` + PMGBackups []PMGBackup `json:"pmgBackups"` + Backups Backups `json:"backups"` + ReplicationJobs []ReplicationJobFrontend `json:"replicationJobs"` + ActiveAlerts []Alert `json:"activeAlerts"` // Active alerts + Metrics map[string]any `json:"metrics"` // Empty object for now + PVEBackups PVEBackups `json:"pveBackups"` // Keep as is + Performance map[string]any `json:"performance"` // Empty object for now + ConnectionHealth map[string]bool `json:"connectionHealth"` // Keep as is + Stats map[string]any `json:"stats"` // Empty object for now + LastUpdate int64 `json:"lastUpdate"` // Unix timestamp + TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"` // Global temperature monitoring setting } diff --git a/internal/models/state_snapshot.go b/internal/models/state_snapshot.go index 86434fb..0b84827 100644 --- a/internal/models/state_snapshot.go +++ b/internal/models/state_snapshot.go @@ -4,25 +4,25 @@ import "time" // StateSnapshot represents a snapshot of the state without mutex type StateSnapshot struct { - Nodes []Node `json:"nodes"` - VMs []VM `json:"vms"` - Containers []Container `json:"containers"` - DockerHosts []DockerHost `json:"dockerHosts"` - RemovedDockerHosts []RemovedDockerHost `json:"removedDockerHosts"` - Hosts []Host `json:"hosts"` - Storage []Storage `json:"storage"` - CephClusters []CephCluster `json:"cephClusters"` - PhysicalDisks []PhysicalDisk `json:"physicalDisks"` - PBSInstances []PBSInstance `json:"pbs"` - PMGInstances []PMGInstance `json:"pmg"` - PBSBackups []PBSBackup `json:"pbsBackups"` - PMGBackups []PMGBackup `json:"pmgBackups"` - Backups Backups `json:"backups"` - ReplicationJobs []ReplicationJob `json:"replicationJobs"` - Metrics []Metric `json:"metrics"` - PVEBackups PVEBackups `json:"pveBackups"` - Performance Performance `json:"performance"` - ConnectionHealth map[string]bool `json:"connectionHealth"` + Nodes []Node `json:"nodes"` + VMs []VM `json:"vms"` + Containers []Container `json:"containers"` + DockerHosts []DockerHost `json:"dockerHosts"` + RemovedDockerHosts []RemovedDockerHost `json:"removedDockerHosts"` + Hosts []Host `json:"hosts"` + Storage []Storage `json:"storage"` + CephClusters []CephCluster `json:"cephClusters"` + PhysicalDisks []PhysicalDisk `json:"physicalDisks"` + PBSInstances []PBSInstance `json:"pbs"` + PMGInstances []PMGInstance `json:"pmg"` + PBSBackups []PBSBackup `json:"pbsBackups"` + PMGBackups []PMGBackup `json:"pmgBackups"` + Backups Backups `json:"backups"` + ReplicationJobs []ReplicationJob `json:"replicationJobs"` + Metrics []Metric `json:"metrics"` + PVEBackups PVEBackups `json:"pveBackups"` + Performance Performance `json:"performance"` + ConnectionHealth map[string]bool `json:"connectionHealth"` Stats Stats `json:"stats"` ActiveAlerts []Alert `json:"activeAlerts"` RecentlyResolved []ResolvedAlert `json:"recentlyResolved"` @@ -63,11 +63,11 @@ func (s *State) GetSnapshot() StateSnapshot { PBS: pbsBackups, PMG: pmgBackups, }, - ReplicationJobs: append([]ReplicationJob{}, s.ReplicationJobs...), - Metrics: append([]Metric{}, s.Metrics...), - PVEBackups: pveBackups, - Performance: s.Performance, - ConnectionHealth: make(map[string]bool), + ReplicationJobs: append([]ReplicationJob{}, s.ReplicationJobs...), + Metrics: append([]Metric{}, s.Metrics...), + PVEBackups: pveBackups, + Performance: s.Performance, + ConnectionHealth: make(map[string]bool), Stats: s.Stats, ActiveAlerts: append([]Alert{}, s.ActiveAlerts...), RecentlyResolved: append([]ResolvedAlert{}, s.RecentlyResolved...), @@ -136,25 +136,25 @@ func (s StateSnapshot) ToFrontend() StateFrontend { } return StateFrontend{ - Nodes: nodes, - VMs: vms, - Containers: containers, - DockerHosts: dockerHosts, - RemovedDockerHosts: removedDockerHosts, - Hosts: hosts, - Storage: storage, - CephClusters: cephClusters, - PhysicalDisks: s.PhysicalDisks, - PBS: s.PBSInstances, - PMG: s.PMGInstances, - PBSBackups: s.PBSBackups, - PMGBackups: s.PMGBackups, - Backups: s.Backups, - ReplicationJobs: replicationJobs, - ActiveAlerts: s.ActiveAlerts, - Metrics: make(map[string]any), - PVEBackups: s.PVEBackups, - Performance: make(map[string]any), + Nodes: nodes, + VMs: vms, + Containers: containers, + DockerHosts: dockerHosts, + RemovedDockerHosts: removedDockerHosts, + Hosts: hosts, + Storage: storage, + CephClusters: cephClusters, + PhysicalDisks: s.PhysicalDisks, + PBS: s.PBSInstances, + PMG: s.PMGInstances, + PBSBackups: s.PBSBackups, + PMGBackups: s.PMGBackups, + Backups: s.Backups, + ReplicationJobs: replicationJobs, + ActiveAlerts: s.ActiveAlerts, + Metrics: make(map[string]any), + PVEBackups: s.PVEBackups, + Performance: make(map[string]any), ConnectionHealth: s.ConnectionHealth, Stats: make(map[string]any), LastUpdate: s.LastUpdate.Unix() * 1000, // JavaScript timestamp diff --git a/internal/monitoring/backoff_test.go b/internal/monitoring/backoff_test.go index 6305706..de883c6 100644 --- a/internal/monitoring/backoff_test.go +++ b/internal/monitoring/backoff_test.go @@ -77,7 +77,7 @@ func TestBackoffConfig_NextDelay(t *testing.T) { Max: 5 * time.Minute, }, attempt: 0, - rng: 0.5, // neutral jitter + rng: 0.5, // neutral jitter wantMin: 8 * time.Second, // 10s * (1 - 0.2) wantMax: 12 * time.Second, // 10s * (1 + 0.2) }, diff --git a/internal/monitoring/circuit_breaker.go b/internal/monitoring/circuit_breaker.go index 10cd66e..3bdb71c 100644 --- a/internal/monitoring/circuit_breaker.go +++ b/internal/monitoring/circuit_breaker.go @@ -23,8 +23,8 @@ type circuitBreaker struct { maxDelay time.Duration openThreshold int halfOpenWindow time.Duration - stateSince time.Time - lastTransition time.Time + stateSince time.Time + lastTransition time.Time } func newCircuitBreaker(openThreshold int, retryInterval, maxDelay, halfOpenWindow time.Duration) *circuitBreaker { @@ -40,16 +40,16 @@ func newCircuitBreaker(openThreshold int, retryInterval, maxDelay, halfOpenWindo if halfOpenWindow <= 0 { halfOpenWindow = 30 * time.Second } - now := time.Now() - return &circuitBreaker{ - state: breakerClosed, - retryInterval: retryInterval, - maxDelay: maxDelay, - openThreshold: openThreshold, - halfOpenWindow: halfOpenWindow, - stateSince: now, - lastTransition: now, - } + now := time.Now() + return &circuitBreaker{ + state: breakerClosed, + retryInterval: retryInterval, + maxDelay: maxDelay, + openThreshold: openThreshold, + halfOpenWindow: halfOpenWindow, + stateSince: now, + lastTransition: now, + } } func (b *circuitBreaker) allow(now time.Time) bool { diff --git a/internal/monitoring/circuit_breaker_test.go b/internal/monitoring/circuit_breaker_test.go index edcdf11..d264dc9 100644 --- a/internal/monitoring/circuit_breaker_test.go +++ b/internal/monitoring/circuit_breaker_test.go @@ -272,7 +272,7 @@ func TestCircuitBreaker_RetryIntervalBackoff(t *testing.T) { // All subsequent failures should keep it at maxDelay for i := 0; i < 5; i++ { - cb.allow(now.Add(time.Duration(42+i*2)*time.Minute)) + cb.allow(now.Add(time.Duration(42+i*2) * time.Minute)) cb.recordFailure(now.Add(time.Duration(42+i*2) * time.Minute)) } diff --git a/internal/monitoring/integration_integration_test.go b/internal/monitoring/integration_integration_test.go index b6eb069..b16c528 100644 --- a/internal/monitoring/integration_integration_test.go +++ b/internal/monitoring/integration_integration_test.go @@ -163,11 +163,11 @@ func TestAdaptiveSchedulerIntegration(t *testing.T) { t.Fatalf("expected transient instance to recover with successes, got 0") } - dlqKeys := map[string]struct{}{} - for _, task := range report.Health.DeadLetter.Tasks { - dlqKeys[instanceKey(task.Type, task.Instance)] = struct{}{} - } - if len(report.Health.Breakers) > len(dlqKeys) { + dlqKeys := map[string]struct{}{} + for _, task := range report.Health.DeadLetter.Tasks { + dlqKeys[instanceKey(task.Type, task.Instance)] = struct{}{} + } + if len(report.Health.Breakers) > len(dlqKeys) { t.Fatalf("unexpected number of circuit breaker entries: got %d want <= %d", len(report.Health.Breakers), len(dlqKeys)) } for _, breaker := range report.Health.Breakers { @@ -218,12 +218,12 @@ func TestAdaptiveSchedulerSoak(t *testing.T) { t.Skip("skipping soak test (enable with -soak or HARNESS_SOAK_MINUTES)") } - minutes := 15 - if minutesEnv != "" { - if parsed, err := strconv.Atoi(minutesEnv); err == nil && parsed > 0 { - minutes = parsed - } - } + minutes := 15 + if minutesEnv != "" { + if parsed, err := strconv.Atoi(minutesEnv); err == nil && parsed > 0 { + minutes = parsed + } + } duration := time.Duration(minutes) * time.Minute warmup := 2 * time.Minute diff --git a/internal/monitoring/scheduler.go b/internal/monitoring/scheduler.go index 52659a6..2d38e6a 100644 --- a/internal/monitoring/scheduler.go +++ b/internal/monitoring/scheduler.go @@ -52,14 +52,14 @@ type IntervalRequest struct { // InstanceDescriptor describes a monitored endpoint for scheduling purposes. type InstanceDescriptor struct { - Name string - Type InstanceType - LastSuccess time.Time - LastFailure time.Time + Name string + Type InstanceType + LastSuccess time.Time + LastFailure time.Time LastScheduled time.Time - LastInterval time.Duration - ErrorCount int - Metadata map[string]any + LastInterval time.Duration + ErrorCount int + Metadata map[string]any } // ScheduledTask represents a single polling opportunity planned by the scheduler. @@ -90,13 +90,13 @@ func DefaultSchedulerConfig() SchedulerConfig { // AdaptiveScheduler orchestrates poll execution plans using pluggable scoring strategies. type AdaptiveScheduler struct { - cfg SchedulerConfig - staleness StalenessSource - interval IntervalSelector - enqueuer TaskEnqueuer + cfg SchedulerConfig + staleness StalenessSource + interval IntervalSelector + enqueuer TaskEnqueuer - mu sync.RWMutex - lastPlan map[string]ScheduledTask + mu sync.RWMutex + lastPlan map[string]ScheduledTask } // NewAdaptiveScheduler constructs a scheduler with safe defaults. @@ -110,15 +110,15 @@ func NewAdaptiveScheduler(cfg SchedulerConfig, staleness StalenessSource, interv if cfg.MaxInterval <= 0 || cfg.MaxInterval < cfg.MinInterval { cfg.MaxInterval = DefaultSchedulerConfig().MaxInterval } - if staleness == nil { - staleness = noopStalenessSource{} - } - if interval == nil { - interval = newAdaptiveIntervalSelector(cfg) - } - if enqueuer == nil { - enqueuer = noopTaskEnqueuer{} - } + if staleness == nil { + staleness = noopStalenessSource{} + } + if interval == nil { + interval = newAdaptiveIntervalSelector(cfg) + } + if enqueuer == nil { + enqueuer = noopTaskEnqueuer{} + } return &AdaptiveScheduler{ cfg: cfg, diff --git a/internal/monitoring/staleness_tracker.go b/internal/monitoring/staleness_tracker.go index 0c16ca8..326a068 100644 --- a/internal/monitoring/staleness_tracker.go +++ b/internal/monitoring/staleness_tracker.go @@ -1,204 +1,204 @@ package monitoring import ( - "crypto/sha1" - "encoding/hex" - "sync" - "time" + "crypto/sha1" + "encoding/hex" + "sync" + "time" ) // FreshnessSnapshot captures the most recent freshness metadata available for a target instance. type FreshnessSnapshot struct { - InstanceType InstanceType - Instance string - LastSuccess time.Time - LastError time.Time - LastMutated time.Time - ChangeHash string + InstanceType InstanceType + Instance string + LastSuccess time.Time + LastError time.Time + LastMutated time.Time + ChangeHash string } // StalenessTracker maintains freshness metadata and exposes normalized staleness scores. type StalenessTracker struct { - mu sync.RWMutex - entries map[string]FreshnessSnapshot - baseTTL time.Duration - maxStale time.Duration - metrics *PollMetrics + mu sync.RWMutex + entries map[string]FreshnessSnapshot + baseTTL time.Duration + maxStale time.Duration + metrics *PollMetrics } // NewStalenessTracker builds a tracker wired to poll metrics for last-success signal and using default parameters. func NewStalenessTracker(metrics *PollMetrics) *StalenessTracker { - return &StalenessTracker{ - entries: make(map[string]FreshnessSnapshot), - baseTTL: 10 * time.Second, - maxStale: 5 * time.Minute, - metrics: metrics, - } + return &StalenessTracker{ + entries: make(map[string]FreshnessSnapshot), + baseTTL: 10 * time.Second, + maxStale: 5 * time.Minute, + metrics: metrics, + } } // SetBounds allows overriding score decay windows. func (t *StalenessTracker) SetBounds(baseTTL, maxStale time.Duration) { - t.mu.Lock() - defer t.mu.Unlock() - if baseTTL > 0 { - t.baseTTL = baseTTL - } - if maxStale > 0 { - t.maxStale = maxStale - } + t.mu.Lock() + defer t.mu.Unlock() + if baseTTL > 0 { + t.baseTTL = baseTTL + } + if maxStale > 0 { + t.maxStale = maxStale + } } // UpdateSuccess records a successful poll along with a change hash derived from the payload. func (t *StalenessTracker) UpdateSuccess(instanceType InstanceType, instance string, payload []byte) { - if t == nil { - return - } + if t == nil { + return + } - now := time.Now() - snapshot := FreshnessSnapshot{ - InstanceType: instanceType, - Instance: instance, - LastSuccess: now, - } + now := time.Now() + snapshot := FreshnessSnapshot{ + InstanceType: instanceType, + Instance: instance, + LastSuccess: now, + } - if len(payload) > 0 { - sum := sha1.Sum(payload) - snapshot.ChangeHash = hex.EncodeToString(sum[:]) - snapshot.LastMutated = now - } + if len(payload) > 0 { + sum := sha1.Sum(payload) + snapshot.ChangeHash = hex.EncodeToString(sum[:]) + snapshot.LastMutated = now + } - t.setSnapshot(snapshot) + t.setSnapshot(snapshot) } // UpdateError records the most recent error time for a target instance. func (t *StalenessTracker) UpdateError(instanceType InstanceType, instance string) { - if t == nil { - return - } + if t == nil { + return + } - snapshot := FreshnessSnapshot{ - InstanceType: instanceType, - Instance: instance, - LastError: time.Now(), - } + snapshot := FreshnessSnapshot{ + InstanceType: instanceType, + Instance: instance, + LastError: time.Now(), + } - t.mergeSnapshot(snapshot) + t.mergeSnapshot(snapshot) } // SetChangeHash updates the change fingerprint without affecting success timestamps. func (t *StalenessTracker) SetChangeHash(instanceType InstanceType, instance string, payload []byte) { - if t == nil || len(payload) == 0 { - return - } + if t == nil || len(payload) == 0 { + return + } - now := time.Now() - sum := sha1.Sum(payload) - hash := hex.EncodeToString(sum[:]) + now := time.Now() + sum := sha1.Sum(payload) + hash := hex.EncodeToString(sum[:]) - t.mu.Lock() - defer t.mu.Unlock() + t.mu.Lock() + defer t.mu.Unlock() - key := trackerKey(instanceType, instance) - snap := t.entries[key] - snap.InstanceType = instanceType - snap.Instance = instance - snap.ChangeHash = hash - snap.LastMutated = now - t.entries[key] = snap + key := trackerKey(instanceType, instance) + snap := t.entries[key] + snap.InstanceType = instanceType + snap.Instance = instance + snap.ChangeHash = hash + snap.LastMutated = now + t.entries[key] = snap } // StalenessScore implements the StalenessSource interface and returns a normalized value in [0,1]. func (t *StalenessTracker) StalenessScore(instanceType InstanceType, instance string) (float64, bool) { - if t == nil { - return 0, false - } + if t == nil { + return 0, false + } - snap, ok := t.snapshot(instanceType, instance) - if !ok { - return 0, false - } + snap, ok := t.snapshot(instanceType, instance) + if !ok { + return 0, false + } - if !snap.LastSuccess.IsZero() && t.metrics != nil { - if ts, ok := t.metrics.lastSuccessFor(string(instanceType), instance); ok { - snap.LastSuccess = ts - } - } + if !snap.LastSuccess.IsZero() && t.metrics != nil { + if ts, ok := t.metrics.lastSuccessFor(string(instanceType), instance); ok { + snap.LastSuccess = ts + } + } - if snap.LastSuccess.IsZero() { - return 1, true - } + if snap.LastSuccess.IsZero() { + return 1, true + } - now := time.Now() - age := now.Sub(snap.LastSuccess) - if age <= 0 { - return 0, true - } + now := time.Now() + age := now.Sub(snap.LastSuccess) + if age <= 0 { + return 0, true + } - max := t.maxStale - if max <= 0 { - max = 5 * time.Minute - } - score := age.Seconds() / max.Seconds() - if score > 1 { - score = 1 - } - if score < 0 { - score = 0 - } - return score, true + max := t.maxStale + if max <= 0 { + max = 5 * time.Minute + } + score := age.Seconds() / max.Seconds() + if score > 1 { + score = 1 + } + if score < 0 { + score = 0 + } + return score, true } func (t *StalenessTracker) setSnapshot(snapshot FreshnessSnapshot) { - t.mu.Lock() - defer t.mu.Unlock() - key := trackerKey(snapshot.InstanceType, snapshot.Instance) - t.entries[key] = snapshot + t.mu.Lock() + defer t.mu.Unlock() + key := trackerKey(snapshot.InstanceType, snapshot.Instance) + t.entries[key] = snapshot } func (t *StalenessTracker) mergeSnapshot(snapshot FreshnessSnapshot) { - t.mu.Lock() - defer t.mu.Unlock() + t.mu.Lock() + defer t.mu.Unlock() - key := trackerKey(snapshot.InstanceType, snapshot.Instance) - existing := t.entries[key] + key := trackerKey(snapshot.InstanceType, snapshot.Instance) + existing := t.entries[key] - if snapshot.LastSuccess.After(existing.LastSuccess) { - existing.LastSuccess = snapshot.LastSuccess - } - if snapshot.LastError.After(existing.LastError) { - existing.LastError = snapshot.LastError - } - if snapshot.LastMutated.After(existing.LastMutated) { - existing.LastMutated = snapshot.LastMutated - } - if snapshot.ChangeHash != "" { - existing.ChangeHash = snapshot.ChangeHash - } + if snapshot.LastSuccess.After(existing.LastSuccess) { + existing.LastSuccess = snapshot.LastSuccess + } + if snapshot.LastError.After(existing.LastError) { + existing.LastError = snapshot.LastError + } + if snapshot.LastMutated.After(existing.LastMutated) { + existing.LastMutated = snapshot.LastMutated + } + if snapshot.ChangeHash != "" { + existing.ChangeHash = snapshot.ChangeHash + } - existing.InstanceType = snapshot.InstanceType - existing.Instance = snapshot.Instance + existing.InstanceType = snapshot.InstanceType + existing.Instance = snapshot.Instance - t.entries[key] = existing + t.entries[key] = existing } func (t *StalenessTracker) snapshot(instanceType InstanceType, instance string) (FreshnessSnapshot, bool) { - t.mu.RLock() - defer t.mu.RUnlock() - snap, ok := t.entries[trackerKey(instanceType, instance)] - return snap, ok + t.mu.RLock() + defer t.mu.RUnlock() + snap, ok := t.entries[trackerKey(instanceType, instance)] + return snap, ok } func trackerKey(instanceType InstanceType, instance string) string { - return string(instanceType) + "::" + instance + return string(instanceType) + "::" + instance } // StalenessSnapshot represents staleness data for a single instance. type StalenessSnapshot struct { - Instance string `json:"instance"` - Type string `json:"type"` - Score float64 `json:"score"` - LastSuccess time.Time `json:"lastSuccess"` - LastError time.Time `json:"lastError,omitempty"` + Instance string `json:"instance"` + Type string `json:"type"` + Score float64 `json:"score"` + LastSuccess time.Time `json:"lastSuccess"` + LastError time.Time `json:"lastError,omitempty"` } // Snapshot returns a copy of all staleness data for API exposure. diff --git a/internal/notifications/queue.go b/internal/notifications/queue.go index 4c197be..bbe2791 100644 --- a/internal/notifications/queue.go +++ b/internal/notifications/queue.go @@ -9,9 +9,9 @@ import ( "sync" "time" + _ "github.com/mattn/go-sqlite3" "github.com/rcourtman/pulse-go-rewrite/internal/alerts" "github.com/rcourtman/pulse-go-rewrite/internal/utils" - _ "github.com/mattn/go-sqlite3" "github.com/rs/zerolog/log" ) @@ -54,9 +54,9 @@ type NotificationQueue struct { wg sync.WaitGroup processorTicker *time.Ticker cleanupTicker *time.Ticker - notifyChan chan struct{} // Signal when new notifications are added - processor func(*QueuedNotification) error // Notification processor function - workerSem chan struct{} // Semaphore for limiting concurrent workers + notifyChan chan struct{} // Signal when new notifications are added + processor func(*QueuedNotification) error // Notification processor function + workerSem chan struct{} // Semaphore for limiting concurrent workers } // NewNotificationQueue creates a new persistent notification queue diff --git a/internal/sensors/parser_test.go b/internal/sensors/parser_test.go index d7827ad..81adcb0 100644 --- a/internal/sensors/parser_test.go +++ b/internal/sensors/parser_test.go @@ -377,19 +377,19 @@ func TestExtractTempInput(t *testing.T) { expected: 45.0, }, { - name: "no input field", - input: map[string]interface{}{"temp1_max": 100.0}, - isNaN: true, + name: "no input field", + input: map[string]interface{}{"temp1_max": 100.0}, + isNaN: true, }, { - name: "empty map", - input: map[string]interface{}{}, - isNaN: true, + name: "empty map", + input: map[string]interface{}{}, + isNaN: true, }, { - name: "wrong suffix", - input: map[string]interface{}{"temp1_max": 45.0}, - isNaN: true, + name: "wrong suffix", + input: map[string]interface{}{"temp1_max": 45.0}, + isNaN: true, }, } diff --git a/internal/system/container_test.go b/internal/system/container_test.go index 90282a7..f7e160a 100644 --- a/internal/system/container_test.go +++ b/internal/system/container_test.go @@ -17,7 +17,7 @@ func TestIsHexString(t *testing.T) { {"xyz", false}, {"123g", false}, {"hello-world", false}, - {"12 34", false}, // space + {"12 34", false}, // space {"12\n34", false}, // newline } diff --git a/internal/updates/queue.go b/internal/updates/queue.go index 4f34450..a88e818 100644 --- a/internal/updates/queue.go +++ b/internal/updates/queue.go @@ -12,12 +12,12 @@ import ( type JobState string const ( - JobStateIdle JobState = "idle" - JobStateQueued JobState = "queued" - JobStateRunning JobState = "running" - JobStateCompleted JobState = "completed" - JobStateFailed JobState = "failed" - JobStateCancelled JobState = "cancelled" + JobStateIdle JobState = "idle" + JobStateQueued JobState = "queued" + JobStateRunning JobState = "running" + JobStateCompleted JobState = "completed" + JobStateFailed JobState = "failed" + JobStateCancelled JobState = "cancelled" ) // UpdateJob represents a single update job @@ -34,10 +34,10 @@ type UpdateJob struct { // UpdateQueue manages the update job queue ensuring only one update runs at a time type UpdateQueue struct { - mu sync.RWMutex - currentJob *UpdateJob - jobHistory []*UpdateJob - maxHistory int + mu sync.RWMutex + currentJob *UpdateJob + jobHistory []*UpdateJob + maxHistory int } // NewUpdateQueue creates a new update queue diff --git a/internal/updates/updater.go b/internal/updates/updater.go index 6d9c645..ef7880b 100644 --- a/internal/updates/updater.go +++ b/internal/updates/updater.go @@ -7,10 +7,10 @@ import ( // UpdateRequest represents a request to update type UpdateRequest struct { - Version string - Channel string - Force bool - DryRun bool + Version string + Channel string + Force bool + DryRun bool } // UpdatePlan contains information about how an update will be performed @@ -26,11 +26,11 @@ type UpdatePlan struct { // UpdateProgress represents progress updates during an update type UpdateProgress struct { - Stage string `json:"stage"` - Progress int `json:"progress"` // 0-100 - Message string `json:"message"` - IsComplete bool `json:"isComplete"` - Error string `json:"error,omitempty"` + Stage string `json:"stage"` + Progress int `json:"progress"` // 0-100 + Message string `json:"message"` + IsComplete bool `json:"isComplete"` + Error string `json:"error,omitempty"` } // ProgressCallback is called during update execution diff --git a/pkg/agents/docker/report_test.go b/pkg/agents/docker/report_test.go index d7c6545..52067b8 100644 --- a/pkg/agents/docker/report_test.go +++ b/pkg/agents/docker/report_test.go @@ -66,9 +66,9 @@ func TestReport_JSONMarshal(t *testing.T) { IntervalSeconds: 30, }, Host: HostInfo{ - Hostname: "docker-host", - DockerVersion: "24.0.0", - TotalCPU: 8, + Hostname: "docker-host", + DockerVersion: "24.0.0", + TotalCPU: 8, TotalMemoryBytes: 16000000000, }, Containers: []Container{ diff --git a/pkg/agents/host/report.go b/pkg/agents/host/report.go index 0c209df..8242d3a 100644 --- a/pkg/agents/host/report.go +++ b/pkg/agents/host/report.go @@ -89,24 +89,24 @@ type Sensors struct { // RAIDArray represents an mdadm RAID array. type RAIDArray struct { - Device string `json:"device"` // e.g., /dev/md0 - Name string `json:"name,omitempty"` // Array name if set - Level string `json:"level"` // RAID level: raid0, raid1, raid5, raid6, raid10 - State string `json:"state"` // clean, active, degraded, recovering, resyncing, etc. - TotalDevices int `json:"totalDevices"` // Total number of devices in array - ActiveDevices int `json:"activeDevices"` // Number of active devices - WorkingDevices int `json:"workingDevices"` // Number of working devices - FailedDevices int `json:"failedDevices"` // Number of failed devices - SpareDevices int `json:"spareDevices"` // Number of spare devices - UUID string `json:"uuid,omitempty"` // Array UUID - Devices []RAIDDevice `json:"devices"` // Individual devices in array - RebuildPercent float64 `json:"rebuildPercent"` // Rebuild/resync progress (0-100) + Device string `json:"device"` // e.g., /dev/md0 + Name string `json:"name,omitempty"` // Array name if set + Level string `json:"level"` // RAID level: raid0, raid1, raid5, raid6, raid10 + State string `json:"state"` // clean, active, degraded, recovering, resyncing, etc. + TotalDevices int `json:"totalDevices"` // Total number of devices in array + ActiveDevices int `json:"activeDevices"` // Number of active devices + WorkingDevices int `json:"workingDevices"` // Number of working devices + FailedDevices int `json:"failedDevices"` // Number of failed devices + SpareDevices int `json:"spareDevices"` // Number of spare devices + UUID string `json:"uuid,omitempty"` // Array UUID + Devices []RAIDDevice `json:"devices"` // Individual devices in array + RebuildPercent float64 `json:"rebuildPercent"` // Rebuild/resync progress (0-100) RebuildSpeed string `json:"rebuildSpeed,omitempty"` // Rebuild speed (e.g., "50000K/sec") } // RAIDDevice represents a single device in a RAID array. type RAIDDevice struct { - Device string `json:"device"` // e.g., /dev/sda1 - State string `json:"state"` // active, spare, faulty, removed - Slot int `json:"slot"` // Position in array (-1 if not applicable) + Device string `json:"device"` // e.g., /dev/sda1 + State string `json:"state"` // active, spare, faulty, removed + Slot int `json:"slot"` // Position in array (-1 if not applicable) } diff --git a/pkg/discovery/envdetect/envdetect.go b/pkg/discovery/envdetect/envdetect.go index 66557e5..644d6df 100644 --- a/pkg/discovery/envdetect/envdetect.go +++ b/pkg/discovery/envdetect/envdetect.go @@ -83,13 +83,13 @@ type SubnetPhase struct { // EnvironmentProfile captures detection results and scanning plan. type EnvironmentProfile struct { - Type Environment // Detected environment. - Phases []SubnetPhase // Subnet scanning phases. - ExtraTargets []net.IP // IPs to always probe. - Policy ScanPolicy // Applied scan policy. - Confidence float64 // Overall confidence (0.0 - 1.0). - Warnings []string // Non-fatal detection warnings. - Metadata map[string]string // Misc metadata (container type, gateway, etc.). + Type Environment // Detected environment. + Phases []SubnetPhase // Subnet scanning phases. + ExtraTargets []net.IP // IPs to always probe. + Policy ScanPolicy // Applied scan policy. + Confidence float64 // Overall confidence (0.0 - 1.0). + Warnings []string // Non-fatal detection warnings. + Metadata map[string]string // Misc metadata (container type, gateway, etc.). } // DetectEnvironment performs environment detection and returns a profile. diff --git a/pkg/proxmox/client.go b/pkg/proxmox/client.go index a4449e1..2d413d7 100644 --- a/pkg/proxmox/client.go +++ b/pkg/proxmox/client.go @@ -1839,23 +1839,23 @@ func (a *VMAgentField) UnmarshalJSON(data []byte) error { // VMStatus represents detailed VM status returned by Proxmox. type VMStatus struct { - Status string `json:"status"` - CPU float64 `json:"cpu"` - CPUs int `json:"cpus"` - Mem uint64 `json:"mem"` - MaxMem uint64 `json:"maxmem"` - Balloon uint64 `json:"balloon"` - BalloonMin uint64 `json:"balloon_min"` - FreeMem uint64 `json:"freemem"` - MemInfo *VMMemInfo `json:"meminfo,omitempty"` - Disk uint64 `json:"disk"` - MaxDisk uint64 `json:"maxdisk"` - DiskRead uint64 `json:"diskread"` - DiskWrite uint64 `json:"diskwrite"` - NetIn uint64 `json:"netin"` - NetOut uint64 `json:"netout"` - Uptime uint64 `json:"uptime"` - Agent VMAgentField `json:"agent"` + Status string `json:"status"` + CPU float64 `json:"cpu"` + CPUs int `json:"cpus"` + Mem uint64 `json:"mem"` + MaxMem uint64 `json:"maxmem"` + Balloon uint64 `json:"balloon"` + BalloonMin uint64 `json:"balloon_min"` + FreeMem uint64 `json:"freemem"` + MemInfo *VMMemInfo `json:"meminfo,omitempty"` + Disk uint64 `json:"disk"` + MaxDisk uint64 `json:"maxdisk"` + DiskRead uint64 `json:"diskread"` + DiskWrite uint64 `json:"diskwrite"` + NetIn uint64 `json:"netin"` + NetOut uint64 `json:"netout"` + Uptime uint64 `json:"uptime"` + Agent VMAgentField `json:"agent"` } // GetZFSPoolStatus gets the status of ZFS pools on a node diff --git a/pkg/proxmox/client_test.go b/pkg/proxmox/client_test.go index a4f64e3..d40b371 100644 --- a/pkg/proxmox/client_test.go +++ b/pkg/proxmox/client_test.go @@ -246,8 +246,6 @@ func TestMemoryStatusUnmarshalFlexibleValues(t *testing.T) { } } - - // TestMemoryStatusEffectiveAvailable_RegressionIssue435 tests the specific scenarios // reported in GitHub issue #435 where memory calculations incorrectly included cache/buffers func TestMemoryStatusEffectiveAvailable_RegressionIssue435(t *testing.T) {