feat: graceful shutdown with store persistence and restart overlay
Some checks failed
Lint & Test / test (push) Failing after 29s

- Add /api/v1/system/shutdown endpoint that triggers clean uvicorn exit
- Persist all 15 stores to disk during shutdown via _save_all_stores()
- Add force parameter to BaseJsonStore._save() to bypass restore freeze
- Restart script now requests graceful shutdown via API (15s timeout),
  falls back to force-kill only if server doesn't exit in time
- Broadcast server_restarting event over WebSocket before shutdown
- Frontend shows "Server restarting..." overlay instantly on WS event,
  replacing the old dynamically-created overlay from settings.ts
- Add server_ref module to share uvicorn Server + TrayManager refs
- Add i18n keys for restart overlay (en/ru/zh)
This commit is contained in:
2026-03-24 15:50:32 +03:00
parent 73947eb6cb
commit 9b4dbac088
14 changed files with 242 additions and 72 deletions

View File

@@ -92,6 +92,29 @@ processor_manager = ProcessorManager(
)
def _save_all_stores() -> None:
"""Persist every store to disk.
Called during graceful shutdown to ensure in-memory data survives
restarts even if no CRUD happened during the session.
"""
all_stores = [
device_store, template_store, pp_template_store,
picture_source_store, output_target_store, pattern_template_store,
color_strip_store, audio_source_store, audio_template_store,
value_source_store, automation_store, scene_preset_store,
sync_clock_store, cspt_store, gradient_store,
]
saved = 0
for store in all_stores:
try:
store._save(force=True)
saved += 1
except Exception as e:
logger.error(f"Failed to save {store._json_key} on shutdown: {e}")
logger.info(f"Shutdown save: persisted {saved}/{len(all_stores)} stores to disk")
@asynccontextmanager
async def lifespan(app: FastAPI):
"""Application lifespan manager.
@@ -230,6 +253,11 @@ async def lifespan(app: FastAPI):
# Shutdown
logger.info("Shutting down LED Grab")
# Persist all stores to disk before stopping anything.
# This ensures in-memory data survives force-kills and restarts
# where no CRUD happened during the session.
_save_all_stores()
# Stop auto-backup engine
try:
await auto_backup_engine.stop()