Problem
After renaming a cloned voice from the profile page, the voices panel does not refresh — the old name keeps showing until a full reload, and a reload landing in the wrong moment can present an empty voices panel, which looks like all voices were deleted (they are not: the rows and reference audio are intact; a fresh load shows everything again).
Root cause (verified on main @ e4c1ef0)
PUT /profiles/{id} (update_profile), DELETE /profiles/{id}, DELETE /profiles/{id}/consent, the history star/clear/delete routes and the export recorders are sync FastAPI endpoints → Starlette runs their bodies in threadpool workers, where asyncio.get_running_loop() raises RuntimeError.
event_bus.emit() (backend/core/event_bus.py) treated that exception as "no event loop — event dropped" (DEBUG log only), so the profiles WebSocket event was silently dropped for exactly the mutations users notice most.
- The frontend only refetches the voice list on that WS event (
useRealtimeEvents → loadProfiles), so every open tab kept stale state.
- Compounding it: the initial list load ran exactly once with no retry (
useAppData), so a transient failure on first fetch left an empty panel until some later WS event or reload — the "my voices are gone" presentation.
Evidence: live probe against a running backend — WS client connected, PUT rename returned 200 and the row updated in SQLite, but no {"kind":"profiles"} frame ever arrived on the socket (3s window). Async endpoints (lock/unlock/consent-record) emit fine; only sync ones drop.
Repro
- Open the app in two tabs (or rename and watch the same tab's sidebar).
- Rename a cloned voice (profile page → Edit → rename → Save).
- The list still shows the old name; the other tab never updates.
Fix
emit() captures the serving loop in subscribe() and hands off from foreign threads via call_soon_threadsafe; async callers unchanged. Plus the initial loads retry until first success (WS-triggered reloads keep the keep-previous-list behavior). Regression test included (fails on old emit, passes on fix); PR to follow.
— happy to link the PR: paoloantinori
Problem
After renaming a cloned voice from the profile page, the voices panel does not refresh — the old name keeps showing until a full reload, and a reload landing in the wrong moment can present an empty voices panel, which looks like all voices were deleted (they are not: the rows and reference audio are intact; a fresh load shows everything again).
Root cause (verified on main @ e4c1ef0)
PUT /profiles/{id}(update_profile),DELETE /profiles/{id},DELETE /profiles/{id}/consent, the history star/clear/delete routes and the export recorders are sync FastAPI endpoints → Starlette runs their bodies in threadpool workers, whereasyncio.get_running_loop()raisesRuntimeError.event_bus.emit()(backend/core/event_bus.py) treated that exception as "no event loop — event dropped" (DEBUG log only), so theprofilesWebSocket event was silently dropped for exactly the mutations users notice most.useRealtimeEvents→loadProfiles), so every open tab kept stale state.useAppData), so a transient failure on first fetch left an empty panel until some later WS event or reload — the "my voices are gone" presentation.Evidence: live probe against a running backend — WS client connected, PUT rename returned 200 and the row updated in SQLite, but no
{"kind":"profiles"}frame ever arrived on the socket (3s window). Async endpoints (lock/unlock/consent-record) emit fine; only sync ones drop.Repro
Fix
emit()captures the serving loop insubscribe()and hands off from foreign threads viacall_soon_threadsafe; async callers unchanged. Plus the initial loads retry until first success (WS-triggered reloads keep the keep-previous-list behavior). Regression test included (fails on old emit, passes on fix); PR to follow.— happy to link the PR: paoloantinori