From a590242382a24ed78c78a7079d7d245373fc207b Mon Sep 17 00:00:00 2001 From: Mikhail Chusavitin Date: Mon, 17 Aug 2026 09:52:33 +0300 Subject: [PATCH] fix(exporter): disambiguate PSU slot collisions to avoid Reanimator "other" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Some collectors (BEE-SP observed so far) report every PSU at slot "0". The export itself already keeps every PSU as a separate record (dedupePSUs keys on serial first), but Reanimator relies on slot to track installed position, so PSUs sharing a slot value land in "other components" instead of being tracked as PSU installations. Renumber colliding slots to stable 0-based positions by encounter order. This doesn't invent a serial or other identity — the PSUs are already distinguished by serial — it only assigns the position field Reanimator needs when the source failed to. Files where every PSU already has a unique slot are unaffected. Co-Authored-By: Claude Sonnet 5 --- internal/exporter/reanimator_converter.go | 31 ++++++++++++++++++++++- 1 file changed, 30 insertions(+), 1 deletion(-) diff --git a/internal/exporter/reanimator_converter.go b/internal/exporter/reanimator_converter.go index 36031b2..537f947 100644 --- a/internal/exporter/reanimator_converter.go +++ b/internal/exporter/reanimator_converter.go @@ -49,7 +49,7 @@ func ConvertToReanimator(result *models.AnalysisResult) (*ReanimatorExport, erro Memory: dedupeMemory(convertMemoryFromDevices(devices, collectedAt)), Storage: dedupeStorage(convertStorageFromDevices(devices, collectedAt)), PCIeDevices: dedupePCIe(convertPCIeFromDevices(devices, collectedAt)), - PowerSupplies: dedupePSUs(convertPSUsFromDevices(devices, collectedAt)), + PowerSupplies: disambiguatePSUSlots(dedupePSUs(convertPSUsFromDevices(devices, collectedAt))), Sensors: convertSensors(result.Sensors), EventLogs: convertEventLogs(result.Events, collectedAt), Licenses: dedupeLicenses(convertLicenses(result.Hardware.Licenses, collectedAt)), @@ -2183,6 +2183,35 @@ func dedupePSUs(items []ReanimatorPSU) []ReanimatorPSU { return result } +// disambiguatePSUSlots renumbers PSU slots when two or more distinct PSUs +// (different serials, already survived dedupePSUs) report the same slot +// string from the source. Some collectors (e.g. BEE-SP) report every PSU +// at slot "0". A shared slot value prevents Reanimator from telling the +// PSUs apart by installed position, so they land in "other components" +// instead of being tracked as PSU installations. Reassigning stable 0-based +// positions by encounter order is the best available disambiguation without +// inventing a serial or other identity data we don't have. +func disambiguatePSUSlots(items []ReanimatorPSU) []ReanimatorPSU { + if len(items) < 2 { + return items + } + counts := make(map[string]int, len(items)) + for _, it := range items { + counts[strings.ToLower(strings.TrimSpace(it.Slot))]++ + } + next := make(map[string]int, len(items)) + for i := range items { + key := strings.ToLower(strings.TrimSpace(items[i].Slot)) + if counts[key] < 2 { + continue + } + idx := next[key] + next[key]++ + items[i].Slot = strconv.Itoa(idx) + } + return items +} + func dedupePCIe(items []ReanimatorPCIe) []ReanimatorPCIe { if len(items) < 2 { return items