Просмотр исходного кода

Fix wrong pattern plays after playlist completion and improve pause/resume UX

- Clear stale playlist state before starting new single pattern to prevent
  bug where previous pattern's finally block may not complete on some systems
- Persist cleared state with state.save() so server restart won't load stale playlist
- Add idle check to pause endpoint using check_table_is_idle()
- Add paused check to resume endpoint
- Extract backend error details in frontend for user-friendly error messages

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
tuanchris 1 день назад
Родитель
Сommit
92a7d5d366

+ 17 - 2
frontend/src/components/NowPlayingBar.tsx

@@ -698,8 +698,23 @@ export function NowPlayingBar({ isLogsOpen = false, logsDrawerHeight = 256, isVi
       const endpoint = status?.is_paused ? '/resume_execution' : '/pause_execution'
       await apiClient.post(endpoint)
       toast.success(status?.is_paused ? 'Resumed' : 'Paused')
-    } catch {
-      toast.error('Failed to toggle pause')
+    } catch (error) {
+      // Extract error detail from backend response (format: "HTTP 400: {"detail":"message"}")
+      let errorMessage = 'Failed to toggle pause'
+      if (error instanceof Error) {
+        try {
+          const jsonMatch = error.message.match(/\{.*\}/)
+          if (jsonMatch) {
+            const parsed = JSON.parse(jsonMatch[0])
+            if (parsed.detail) {
+              errorMessage = parsed.detail
+            }
+          }
+        } catch {
+          // Keep default message if parsing fails
+        }
+      }
+      toast.error(errorMessage)
     }
   }
 

+ 16 - 1
main.py

@@ -1734,7 +1734,14 @@ async def run_theta_rho(request: ThetaRhoRequest, background_tasks: BackgroundTa
         if pattern_manager.get_pattern_lock().locked():
             logger.info("Another pattern is running, stopping it first...")
             await pattern_manager.stop_actions()
-            
+
+        # Clear any stale playlist state before starting a new single pattern.
+        # This prevents a bug where loading state.json after server restart could
+        # cause the old playlist to be used instead of the newly selected pattern.
+        state.current_playlist = None
+        state.current_playlist_index = None
+        state.playlist_mode = None
+
         files_to_run = [file_path]
         logger.info(f'Running theta-rho file: {request.file_name} with pre_execution={request.pre_execution}')
         
@@ -2287,12 +2294,20 @@ async def set_preferred_port(request: Request):
 
 @app.post("/pause_execution")
 async def pause_execution():
+    # Check if table is actually idle before trying to pause
+    if await pattern_manager.check_table_is_idle():
+        raise HTTPException(status_code=400, detail="Nothing is currently playing")
+
     if pattern_manager.pause_execution():
         return {"success": True, "message": "Execution paused"}
     raise HTTPException(status_code=500, detail="Failed to pause execution")
 
 @app.post("/resume_execution")
 async def resume_execution():
+    # Check if execution is actually paused before trying to resume
+    if not state.pause_requested:
+        raise HTTPException(status_code=400, detail="Execution is not paused")
+
     if pattern_manager.resume_execution():
         return {"success": True, "message": "Execution resumed"}
     raise HTTPException(status_code=500, detail="Failed to resume execution")

+ 3 - 0
modules/core/pattern_manager.py

@@ -1713,6 +1713,9 @@ async def run_theta_rho_files(file_paths, pause_time=0, clear_pattern=None, run_
             state.playlist_mode = None
             state.pause_time_remaining = 0
 
+            # Persist cleared state so server restart won't load stale playlist
+            state.save()
+
             await start_idle_led_timeout()
 
             logger.info("All requested patterns completed (or stopped) and state cleared")

+ 40 - 5
tests/unit/test_api_patterns.py

@@ -9,7 +9,6 @@ Tests the following endpoints:
 """
 import pytest
 from unittest.mock import patch, MagicMock, AsyncMock
-import os
 
 
 class TestListThetaRhoFiles:
@@ -246,23 +245,59 @@ class TestPauseResumeExecution:
     @pytest.mark.asyncio
     async def test_pause_execution(self, async_client):
         """Test pause_execution endpoint."""
-        with patch("main.pattern_manager.pause_execution", return_value=True):
-            response = await async_client.post("/pause_execution")
+        # Mock check_table_is_idle to return False (something is playing)
+        with patch("main.pattern_manager.check_table_is_idle", return_value=False):
+            with patch("main.pattern_manager.pause_execution", return_value=True):
+                response = await async_client.post("/pause_execution")
 
         assert response.status_code == 200
         data = response.json()
         assert data["success"] is True
 
+    @pytest.mark.asyncio
+    async def test_pause_execution_when_idle(self, async_client):
+        """Test pause_execution returns 400 when nothing is playing."""
+        # Mock check_table_is_idle to return True (table is idle)
+        with patch("main.pattern_manager.check_table_is_idle", return_value=True):
+            response = await async_client.post("/pause_execution")
+
+        assert response.status_code == 400
+        data = response.json()
+        assert "nothing is currently playing" in data["detail"].lower()
+
     @pytest.mark.asyncio
     async def test_resume_execution(self, async_client):
         """Test resume_execution endpoint."""
-        with patch("main.pattern_manager.resume_execution", return_value=True):
-            response = await async_client.post("/resume_execution")
+        # Mock state.pause_requested to True (execution is paused)
+        from main import state
+        original_value = state.pause_requested
+        try:
+            state.pause_requested = True
+            with patch("main.pattern_manager.resume_execution", return_value=True):
+                response = await async_client.post("/resume_execution")
+        finally:
+            state.pause_requested = original_value
 
         assert response.status_code == 200
         data = response.json()
         assert data["success"] is True
 
+    @pytest.mark.asyncio
+    async def test_resume_execution_when_not_paused(self, async_client):
+        """Test resume_execution returns 400 when not paused."""
+        # Mock state.pause_requested to False (not paused)
+        from main import state
+        original_value = state.pause_requested
+        try:
+            state.pause_requested = False
+            response = await async_client.post("/resume_execution")
+        finally:
+            state.pause_requested = original_value
+
+        assert response.status_code == 400
+        data = response.json()
+        assert "not paused" in data["detail"].lower()
+
 
 class TestDeleteThetaRhoFile:
     """Tests for /delete_theta_rho_file endpoint."""