diff --git a/internal/exporter/reanimator_converter_test.go b/internal/exporter/reanimator_converter_test.go index eabf9bd..911a1e1 100644 --- a/internal/exporter/reanimator_converter_test.go +++ b/internal/exporter/reanimator_converter_test.go @@ -656,6 +656,43 @@ func TestConvertToReanimator_DeduplicatesAllSections(t *testing.T) { } } +func TestConvertToReanimator_StatusFallbackUsesCollectedAt(t *testing.T) { + collectedAt := time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC) + input := &models.AnalysisResult{ + Filename: "status-fallback.json", + CollectedAt: collectedAt, + Hardware: &models.HardwareConfig{ + BoardInfo: models.BoardInfo{SerialNumber: "BOARD-001"}, + Storage: []models.Storage{ + { + Slot: "U.2-1", + Model: "PM9A3", + SerialNumber: "SSD-001", + Present: true, + Status: "OK", + }, + }, + }, + } + + out, err := ConvertToReanimator(input) + if err != nil { + t.Fatalf("ConvertToReanimator() failed: %v", err) + } + if len(out.Hardware.Storage) != 1 { + t.Fatalf("expected 1 storage entry, got %d", len(out.Hardware.Storage)) + } + + wantTs := collectedAt.UTC().Format(time.RFC3339) + got := out.Hardware.Storage[0] + if got.StatusCheckedAt != wantTs { + t.Fatalf("expected status_checked_at=%q, got %q", wantTs, got.StatusCheckedAt) + } + if got.StatusAtCollect == nil || got.StatusAtCollect.At != wantTs { + t.Fatalf("expected status_at_collection.at=%q, got %#v", wantTs, got.StatusAtCollect) + } +} + func TestConvertToReanimator_FirmwareExcludesDeviceBoundEntries(t *testing.T) { input := &models.AnalysisResult{ Filename: "fw-filter-test.json", diff --git a/internal/server/handlers.go b/internal/server/handlers.go index c80c8d5..35892fb 100644 --- a/internal/server/handlers.go +++ b/internal/server/handlers.go @@ -1589,7 +1589,29 @@ func applyArchiveSourceMetadata(result *models.AnalysisResult) { result.SourceType = models.SourceTypeArchive result.Protocol = "" result.TargetHost = "" - result.CollectedAt = time.Now().UTC() + if result.CollectedAt.IsZero() { + result.CollectedAt = inferArchiveCollectedAt(result) + } +} + +func inferArchiveCollectedAt(result *models.AnalysisResult) time.Time { + if result == nil { + return time.Now().UTC() + } + + var latest time.Time + for _, event := range result.Events { + if event.Timestamp.IsZero() { + continue + } + if latest.IsZero() || event.Timestamp.After(latest) { + latest = event.Timestamp + } + } + if !latest.IsZero() { + return latest.UTC() + } + return time.Now().UTC() } func applyCollectSourceMetadata(result *models.AnalysisResult, req CollectRequest) { diff --git a/internal/server/source_metadata_test.go b/internal/server/source_metadata_test.go index e8e6396..6934904 100644 --- a/internal/server/source_metadata_test.go +++ b/internal/server/source_metadata_test.go @@ -30,6 +30,36 @@ func TestApplyArchiveSourceMetadata(t *testing.T) { } } +func TestApplyArchiveSourceMetadata_PreservesExistingCollectedAt(t *testing.T) { + expected := time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC) + result := &models.AnalysisResult{ + CollectedAt: expected, + } + + applyArchiveSourceMetadata(result) + + if !result.CollectedAt.Equal(expected) { + t.Fatalf("expected collected_at to be preserved: got %s want %s", result.CollectedAt, expected) + } +} + +func TestApplyArchiveSourceMetadata_InferCollectedAtFromEvents(t *testing.T) { + oldTs := time.Date(2026, 2, 10, 13, 0, 0, 0, time.UTC) + newTs := time.Date(2026, 2, 10, 15, 30, 0, 0, time.UTC) + result := &models.AnalysisResult{ + Events: []models.Event{ + {Timestamp: oldTs}, + {Timestamp: newTs}, + }, + } + + applyArchiveSourceMetadata(result) + + if !result.CollectedAt.Equal(newTs) { + t.Fatalf("expected collected_at from latest event: got %s want %s", result.CollectedAt, newTs) + } +} + func TestApplyCollectSourceMetadata(t *testing.T) { req := CollectRequest{ Host: "bmc-api.local",