diff --git a/backend/internal/listener/service.go b/backend/internal/listener/service.go index 84a205f..a1e5a9e 100644 --- a/backend/internal/listener/service.go +++ b/backend/internal/listener/service.go @@ -123,6 +123,9 @@ func (s *Service) Delete(id uint) error { if err := s.db.Delete(&models.Listener{}, id).Error; err != nil { return fmt.Errorf("failed to delete listener: %w", err) } + // Soft-delete keeps the row; rename so UNIQUE(name) can be reused immediately. + freed := fmt.Sprintf("%s__deleted_%d", previous.Name, id) + _ = s.db.Unscoped().Model(&models.Listener{}).Where("id = ?", id).Update("name", freed).Error if err := s.regenerateConfigLocked(); err != nil { if rollbackErr := s.db.Unscoped().Save(&previous).Error; rollbackErr != nil { return fmt.Errorf("%v; rollback deleted listener failed: %w", err, rollbackErr) @@ -145,17 +148,33 @@ func (s *Service) ensureUniqueName(candidate *models.Listener) error { if name == "" { return fmt.Errorf("listener name is required") } - var count int64 + // Only live rows should block create/update. Soft-deleted rows still occupy + // the SQLite UNIQUE index, so reclaim those names first. + var active int64 q := s.db.Model(&models.Listener{}).Where("name = ?", name) if candidate.ID != 0 { q = q.Where("id <> ?", candidate.ID) } - if err := q.Count(&count).Error; err != nil { + if err := q.Count(&active).Error; err != nil { return fmt.Errorf("check listener name uniqueness: %w", err) } - if count > 0 { + if active > 0 { return fmt.Errorf("listener name %q already exists", name) } + var soft []models.Listener + sq := s.db.Unscoped().Where("name = ? AND deleted_at IS NOT NULL", name) + if candidate.ID != 0 { + sq = sq.Where("id <> ?", candidate.ID) + } + if err := sq.Find(&soft).Error; err != nil { + return fmt.Errorf("check soft-deleted listener names: %w", err) + } + for _, row := range soft { + newName := fmt.Sprintf("%s__deleted_%d", name, row.ID) + if err := s.db.Unscoped().Model(&models.Listener{}).Where("id = ?", row.ID).Update("name", newName).Error; err != nil { + return fmt.Errorf("reclaim soft-deleted name %q: %w", name, err) + } + } return nil }