diff --git a/allstarr/Controllers/AdminController.cs b/allstarr/Controllers/AdminController.cs index b0ceceb..0e06227 100644 --- a/allstarr/Controllers/AdminController.cs +++ b/allstarr/Controllers/AdminController.cs @@ -841,16 +841,24 @@ public class AdminController : ControllerBase { _logger.LogInformation("Triggering immediate playlist rebuild for {Playlist} with new manual mapping", decodedName); - // Wait for the rebuild to complete before responding to ensure UI gets updated cache - try + // Run rebuild in background with timeout to avoid blocking the response + _ = Task.Run(async () => { - await _matchingService.TriggerMatchingForPlaylistAsync(decodedName); - _logger.LogInformation("✓ Playlist {Playlist} rebuilt successfully with manual mapping", decodedName); - } - catch (Exception ex) - { - _logger.LogError(ex, "Failed to rebuild playlist {Playlist} after manual mapping", decodedName); - } + try + { + using var cts = new CancellationTokenSource(TimeSpan.FromMinutes(2)); // 2 minute timeout + await _matchingService.TriggerMatchingForPlaylistAsync(decodedName); + _logger.LogInformation("✓ Playlist {Playlist} rebuilt successfully with manual mapping", decodedName); + } + catch (OperationCanceledException) + { + _logger.LogWarning("Playlist rebuild for {Playlist} timed out after 2 minutes", decodedName); + } + catch (Exception ex) + { + _logger.LogError(ex, "Failed to rebuild playlist {Playlist} after manual mapping", decodedName); + } + }); } else { diff --git a/allstarr/wwwroot/index.html b/allstarr/wwwroot/index.html index 1e7bdf8..51f9414 100644 --- a/allstarr/wwwroot/index.html +++ b/allstarr/wwwroot/index.html @@ -2042,13 +2042,24 @@ return; } + // Show loading state + const saveBtn = document.getElementById('map-save-btn'); + const originalText = saveBtn.textContent; + saveBtn.textContent = 'Saving...'; + saveBtn.disabled = true; + try { + const controller = new AbortController(); + const timeoutId = setTimeout(() => controller.abort(), 30000); // 30 second timeout + const res = await fetch('/api/admin/playlists/' + encodeURIComponent(playlistName) + '/map', { method: 'POST', headers: { 'Content-Type': 'application/json' }, - body: JSON.stringify({ spotifyId, jellyfinId }) + body: JSON.stringify({ spotifyId, jellyfinId }), + signal: controller.signal }); + clearTimeout(timeoutId); const data = await res.json(); if (res.ok) { @@ -2093,7 +2104,15 @@ showToast(data.error || 'Failed to save mapping', 'error'); } } catch (error) { - showToast('Failed to save mapping', 'error'); + if (error.name === 'AbortError') { + showToast('Request timed out - mapping may still be processing', 'warning'); + } else { + showToast('Failed to save mapping', 'error'); + } + } finally { + // Reset button state + saveBtn.textContent = originalText; + saveBtn.disabled = false; } }