From 8597eca010205f5e0841e4f18d308289eb3a361d Mon Sep 17 00:00:00 2001 From: rcourtman Date: Tue, 2 Dec 2025 00:11:33 +0000 Subject: [PATCH] Fix backup indicator being reset when VMs/Containers are re-polled UpdateVMsForInstance and UpdateContainersForInstance were replacing guest data without preserving the LastBackup field that was populated by SyncGuestBackupTimes. This caused backup indicators to always show "no backup found" since the LastBackup would be wiped every time guests were polled (which happens more frequently than backup polling). Now both functions preserve LastBackup from existing data when the incoming guest data has a zero value. Related to #762 --- internal/api/security_test.go | 25 +++++++++++++++++++++++++ internal/models/models.go | 26 ++++++++++++++++++++++++-- 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/internal/api/security_test.go b/internal/api/security_test.go index f7e2f4c..f11d657 100644 --- a/internal/api/security_test.go +++ b/internal/api/security_test.go @@ -616,6 +616,31 @@ func TestGetLockoutInfo(t *testing.T) { t.Error("expected lockedUntil to be set") } }) + + t.Run("expired lockout returns zeros", func(t *testing.T) { + resetFailedLogins() + identifier := "expired-lockout-user" + + // Directly set an expired lockout entry + failedMu.Lock() + failedLogins[identifier] = &FailedLogin{ + Count: maxFailedAttempts, + LastAttempt: time.Now().Add(-time.Hour), + LockedUntil: time.Now().Add(-time.Minute), // Expired + } + failedMu.Unlock() + + attempts, lockedUntil, isLocked := GetLockoutInfo(identifier) + if attempts != 0 { + t.Errorf("attempts = %d, want 0 for expired lockout", attempts) + } + if !lockedUntil.IsZero() { + t.Errorf("lockedUntil = %v, want zero time for expired lockout", lockedUntil) + } + if isLocked { + t.Error("expected isLocked = false for expired lockout") + } + }) } func TestResetLockout(t *testing.T) { diff --git a/internal/models/models.go b/internal/models/models.go index 7d1cd86..af20f17 100644 --- a/internal/models/models.go +++ b/internal/models/models.go @@ -1083,6 +1083,14 @@ func (s *State) UpdateVMsForInstance(instanceName string, vms []VM) { s.mu.Lock() defer s.mu.Unlock() + // Build a lookup of existing VMs for this instance to preserve LastBackup + existingByVMID := make(map[int]VM) + for _, vm := range s.VMs { + if vm.Instance == instanceName { + existingByVMID[vm.VMID] = vm + } + } + // Create a map of existing VMs, excluding those from this instance vmMap := make(map[string]VM) for _, vm := range s.VMs { @@ -1091,8 +1099,11 @@ func (s *State) UpdateVMsForInstance(instanceName string, vms []VM) { } } - // Add or update VMs from this instance + // Add or update VMs from this instance, preserving LastBackup from existing data for _, vm := range vms { + if existing, ok := existingByVMID[vm.VMID]; ok && vm.LastBackup.IsZero() { + vm.LastBackup = existing.LastBackup + } vmMap[vm.ID] = vm } @@ -1169,6 +1180,14 @@ func (s *State) UpdateContainersForInstance(instanceName string, containers []Co s.mu.Lock() defer s.mu.Unlock() + // Build a lookup of existing containers for this instance to preserve LastBackup + existingByVMID := make(map[int]Container) + for _, ct := range s.Containers { + if ct.Instance == instanceName { + existingByVMID[ct.VMID] = ct + } + } + // Create a map of existing containers, excluding those from this instance containerMap := make(map[string]Container) for _, container := range s.Containers { @@ -1177,8 +1196,11 @@ func (s *State) UpdateContainersForInstance(instanceName string, containers []Co } } - // Add or update containers from this instance + // Add or update containers from this instance, preserving LastBackup from existing data for _, container := range containers { + if existing, ok := existingByVMID[container.VMID]; ok && container.LastBackup.IsZero() { + container.LastBackup = existing.LastBackup + } containerMap[container.ID] = container }