From 632bc6ae06e6b6a26c9e4195694b0362472ece44 Mon Sep 17 00:00:00 2001 From: brandonhs Date: Thu, 1 Oct 2026 15:02:44 -0700 Subject: [PATCH 1/2] fix(recordings): Stop deleting the last recording from returning a 404 The bulk and single delete routes treated an empty recordings list as a failure, so deleting the last remaining recordings returned a 404 and the frontend kept showing the deleted files. - Return the remaining list from the delete routes, even when it is empty - Remove deleted recordings from the cached list in bulk delete - Clear cached durations by full path, which is the key they are stored by - Show an error toast in the frontend when the delete request fails Fixes #554 Co-Authored-By: Claude Opus 5.5 --- backend_py/src/routes/recordings.py | 10 ++---- .../services/recordings/recordings_service.py | 33 ++++++++++--------- .../dwe/recordings/store/recording-store.tsx | 22 ++++++++----- 3 files changed, 32 insertions(+), 33 deletions(-) diff --git a/backend_py/src/routes/recordings.py b/backend_py/src/routes/recordings.py index 9f32e023..71b188b7 100644 --- a/backend_py/src/routes/recordings.py +++ b/backend_py/src/routes/recordings.py @@ -172,7 +172,7 @@ def delete_recording(request: Request, recording_path: str) -> list[RecordingInf recordings_service: RecordingsService = request.app.state.recordings_service response = recordings_service.delete_recording(recording_path) - if not response: + if response is None: raise HTTPException( status_code=404, detail="Recording not found or could not be deleted" ) @@ -187,13 +187,7 @@ def bulk_delete_recording( ) -> list[RecordingInfo]: recordings_service: RecordingsService = request.app.state.recordings_service - response = recordings_service.bulk_delete_recordings(filenames) - if not response: - raise HTTPException( - status_code=404, detail="Recordings not found or could not be deleted" - ) - - return response + return recordings_service.bulk_delete_recordings(filenames) @recordings_router.patch("/{old_name}/{new_name}", summary="Rename a recording") diff --git a/backend_py/src/services/recordings/recordings_service.py b/backend_py/src/services/recordings/recordings_service.py index 4f70ab30..03ca3386 100644 --- a/backend_py/src/services/recordings/recordings_service.py +++ b/backend_py/src/services/recordings/recordings_service.py @@ -132,28 +132,29 @@ def get_recording(self, filename: str) -> RecordingInfo | None: return recording return None + def _remove_recording_file(self, filename: str) -> bool: + # Caller must hold recordings_lock + recording_path = os.path.join(self.recordings_path, filename) + self.durations.pop(recording_path, None) + + if not os.path.exists(recording_path): + return False + + os.remove(recording_path) + self.recordings = [rec for rec in self.recordings if rec.path != recording_path] + return True + def delete_recording(self, filename: str) -> list[RecordingInfo] | None: - if filename in self.durations: - self.durations.pop(filename, None) + with self.recordings_lock: + if not self._remove_recording_file(filename): + return None - recording_path = os.path.join(self.recordings_path, filename) - if os.path.exists(recording_path): - os.remove(recording_path) - self.recordings = [ - rec for rec in self.recordings if rec.path != recording_path - ] - return self.recordings - return None + return self.get_recordings() def bulk_delete_recordings(self, filenames: list[str]) -> list[RecordingInfo]: with self.recordings_lock: for filename in filenames: - if filename in self.durations: - self.durations.pop(filename, None) - - recording_path = os.path.join(self.recordings_path, filename) - if os.path.exists(recording_path): - os.remove(recording_path) + self._remove_recording_file(filename) return self.get_recordings() diff --git a/frontend/src/components/dwe/recordings/store/recording-store.tsx b/frontend/src/components/dwe/recordings/store/recording-store.tsx index 42fdd5d7..47612fce 100644 --- a/frontend/src/components/dwe/recordings/store/recording-store.tsx +++ b/frontend/src/components/dwe/recordings/store/recording-store.tsx @@ -305,16 +305,20 @@ export const recordingsActions = { body: targetNames, }); - if (result.data) { - recordingsState.recordings = result.data; - recordingsActions.setSelectedNames([]); - - toast.success( - deleteTargets.length > 1 - ? `Deleted ${deleteTargets.length} recordings` - : `Recording deleted: ${deleteTargets[0].name}`, - ); + if (result.error || !result.data) { + console.error(result.error); + toast.error("Failed to delete recording"); + return; } + + recordingsState.recordings = result.data; + recordingsActions.setSelectedNames([]); + + toast.success( + deleteTargets.length > 1 + ? `Deleted ${deleteTargets.length} recordings` + : `Recording deleted: ${deleteTargets[0].name}`, + ); } catch (error) { console.error(error); toast.error("Failed to delete recording"); From 9b3fde0f354a54d4be98312c5a794bd7b159c6b3 Mon Sep 17 00:00:00 2001 From: brandonhs Date: Thu, 1 Oct 2026 15:16:14 -0700 Subject: [PATCH 2/2] fix(server): Return JSON 404s for API routes in the release server The 404 handler that serves index.html for frontend routes also caught 404s raised by API routes. It returned index.html with a 200 status, so the frontend tried to parse the HTML as JSON and showed an error toast. On the device, this made deleting the last recording show "Failed to delete recording" even though the files were deleted. Paths under /api/ now use FastAPI's default handler and return the JSON error. Refs #554 Co-Authored-By: Claude Opus 5.5 --- run_release.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/run_release.py b/run_release.py index f2360f7e..a721f168 100755 --- a/run_release.py +++ b/run_release.py @@ -5,10 +5,12 @@ from contextlib import asynccontextmanager import socketio -from fastapi import FastAPI, HTTPException, Request +from fastapi import FastAPI, Request +from fastapi.exception_handlers import http_exception_handler from fastapi.middleware.cors import CORSMiddleware -from fastapi.responses import FileResponse, JSONResponse +from fastapi.responses import FileResponse, JSONResponse, Response from fastapi.staticfiles import StaticFiles +from starlette.exceptions import HTTPException as StarletteHTTPException from backend_py.src import FeatureSupport, Server @@ -95,8 +97,11 @@ async def register_service() -> JSONResponse: app.mount("/", StaticFiles(directory=FRONTEND_DIR, html=True), name="static") @app.exception_handler(404) - async def not_found(request: Request, exc: HTTPException) -> FileResponse: + async def not_found(request: Request, exc: StarletteHTTPException) -> Response: """Serve index.html for any unknown paths (Frontend Routing Support)""" + # API 404s keep their JSON error, so the frontend can tell they failed + if request.url.path.startswith("/api/"): + return await http_exception_handler(request, exc) return FileResponse(os.path.join(FRONTEND_DIR, "index.html")) async def main() -> None: