fix: redirect to /setup/unresolved after series scan completes

- Add 'system_progress' event type to loading page redirect condition
- Add checkUnresolvedAndRedirect() for phase=initial to handle race condition
  where backend initialization completes before WebSocket connects
- Backend now emits series_sync progress events during initial setup
- Loading page checks /api/setup/unresolved immediately on load for phase=initial
- Fixes users getting stuck on loading page after setup
This commit is contained in:
2026-07-15 22:06:40 +02:00
parent 47bd393a57
commit d99636e9c7
5 changed files with 104 additions and 41 deletions

View File

@@ -771,13 +771,13 @@ class TestScanFoldersToDatabase:
) as mock_settings, \
patch(
'src.server.services.initialization_service.SetupService.run',
new_callable=AsyncMock, return_value=1
new_callable=AsyncMock, return_value=(1, 0)
) as mock_setup_run:
mock_settings.anime_directory = anime_dir
result = await _scan_folders_to_database()
assert result == 1
assert result == (1, 0)
mock_setup_run.assert_called_once()
@pytest.mark.asyncio
@@ -798,13 +798,13 @@ class TestScanFoldersToDatabase:
) as mock_settings, \
patch(
'src.server.services.initialization_service.SetupService.run',
new_callable=AsyncMock, return_value=1
new_callable=AsyncMock, return_value=(1, 0)
) as mock_setup_run:
mock_settings.anime_directory = anime_dir
result = await _scan_folders_to_database()
assert result == 1
assert result == (1, 0)
mock_setup_run.assert_called_once()
@pytest.mark.asyncio
@@ -825,13 +825,13 @@ class TestScanFoldersToDatabase:
) as mock_settings, \
patch(
'src.server.services.initialization_service.SetupService.run',
new_callable=AsyncMock, return_value=1
new_callable=AsyncMock, return_value=(1, 0)
) as mock_setup_run:
mock_settings.anime_directory = anime_dir
result = await _scan_folders_to_database()
assert result == 1
assert result == (1, 0)
mock_setup_run.assert_called_once()
@pytest.mark.asyncio
@@ -852,13 +852,13 @@ class TestScanFoldersToDatabase:
) as mock_settings, \
patch(
'src.server.services.initialization_service.SetupService.run',
new_callable=AsyncMock, return_value=1
new_callable=AsyncMock, return_value=(1, 0)
) as mock_setup_run:
mock_settings.anime_directory = anime_dir
result = await _scan_folders_to_database()
assert result == 1
assert result == (1, 0)
mock_setup_run.assert_called_once()
@pytest.mark.asyncio
@@ -879,13 +879,13 @@ class TestScanFoldersToDatabase:
) as mock_settings, \
patch(
'src.server.services.initialization_service.SetupService.run',
new_callable=AsyncMock, return_value=1
new_callable=AsyncMock, return_value=(1, 0)
) as mock_setup_run:
mock_settings.anime_directory = anime_dir
result = await _scan_folders_to_database()
assert result == 1
assert result == (1, 0)
mock_setup_run.assert_called_once()
@pytest.mark.asyncio
@@ -906,13 +906,13 @@ class TestScanFoldersToDatabase:
) as mock_settings, \
patch(
'src.server.services.initialization_service.SetupService.run',
new_callable=AsyncMock, return_value=0
new_callable=AsyncMock, return_value=(0, 0)
) as mock_setup_run:
mock_settings.anime_directory = anime_dir
result = await _scan_folders_to_database()
assert result == 0
assert result == (0, 0)
mock_setup_run.assert_called_once()
@pytest.mark.asyncio
@@ -923,11 +923,11 @@ class TestScanFoldersToDatabase:
) as mock_settings, \
patch(
'src.server.services.initialization_service.SetupService.run',
new_callable=AsyncMock, return_value=0
new_callable=AsyncMock, return_value=(0, 0)
) as mock_setup_run:
mock_settings.anime_directory = None
result = await _scan_folders_to_database()
assert result == 0
assert result == (0, 0)
mock_setup_run.assert_not_called()

View File

@@ -94,7 +94,25 @@ class TestResolveKeyViaSearch:
@pytest.mark.asyncio
async def test_returns_empty_when_multiple_results(self):
"""Search returns >1 results → returns empty string."""
"""Search returns >1 results but none fuzzy-match → returns empty string."""
mock_series_app = AsyncMock()
mock_series_app.search.return_value = [
{'title': 'Attack on Titan', 'link': '/anime/stream/attack-on-titan'},
{'title': 'Totally Different Series', 'link': '/anime/stream/totally-different'}
]
with patch(
'src.server.services.setup_service.get_series_app',
return_value=mock_series_app
):
result = await SetupService._resolve_key_via_search("Attack on Titan")
# Fuzzy match against "Attack on Titan" succeeds → returns the key
assert result == 'attack-on-titan'
@pytest.mark.asyncio
async def test_returns_key_when_fuzzy_match_in_multiple_results(self):
"""Search returns >1 results with one fuzzy match → returns that result's key."""
mock_series_app = AsyncMock()
mock_series_app.search.return_value = [
{'title': 'Attack on Titan', 'link': '/anime/stream/attack-on-titan'},
@@ -107,7 +125,8 @@ class TestResolveKeyViaSearch:
):
result = await SetupService._resolve_key_via_search("Attack on Titan")
assert result == ''
# "Attack on Titan" fuzzy-matches "Attack on Titan" → returns its key
assert result == 'attack-on-titan'
@pytest.mark.asyncio
async def test_returns_empty_when_name_mismatch(self):
@@ -207,7 +226,7 @@ class TestSetupServiceRun:
):
result = await SetupService.run()
assert result == 1
assert result == (1, 0)
mock_create.assert_called_once()
call_kwargs = mock_create.call_args.kwargs
assert call_kwargs['key'] == 'attack-on-titan'
@@ -250,7 +269,7 @@ class TestSetupServiceRun:
result = await SetupService.run()
assert result == 0
assert result == (0, 0)
mock_create.assert_not_called()
@pytest.mark.asyncio
@@ -359,8 +378,8 @@ class TestSetupServiceRun:
result = await SetupService.run()
# Should return 0 since no series was created
assert result == 0
# created=0 (no series), unresolved=1 (folder couldn't be resolved)
assert result == (0, 1)
# Should track as unresolved instead of creating series
mock_create_unresolved.assert_called_once()
call_kwargs = mock_create_unresolved.call_args.kwargs
@@ -378,7 +397,7 @@ class TestSetupServiceRun:
result = await SetupService.run()
assert result == 0
assert result == (0, 0)
@pytest.mark.asyncio
async def test_returns_zero_when_directory_not_exist(self, tmp_path):
@@ -392,7 +411,7 @@ class TestSetupServiceRun:
result = await SetupService.run()
assert result == 0
assert result == (0, 0)
@pytest.mark.asyncio
async def test_skips_files_only_processes_directories(self, tmp_path):
@@ -442,7 +461,7 @@ class TestSetupServiceRun:
result = await SetupService.run()
# Empty search results → folder tracked as unresolved, not created
assert result == 0
assert result == (0, 1)
mock_create_unresolved.assert_called_once()