feat: graceful shutdown with store persistence and restart overlay
Some checks failed
Lint & Test / test (push) Failing after 29s
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:
@@ -347,9 +347,6 @@ export async function handleRestoreFileSelected(input: HTMLInputElement): Promis
|
||||
showToast(data.message || t('settings.restore.success'), 'success');
|
||||
settingsModal.forceClose();
|
||||
|
||||
if (data.restart_scheduled) {
|
||||
showRestartOverlay();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Restore failed:', err);
|
||||
showToast(t('settings.restore.error') + ': ' + err.message, 'error');
|
||||
@@ -369,63 +366,12 @@ export async function restartServer(): Promise<void> {
|
||||
throw new Error(err.detail || `HTTP ${resp.status}`);
|
||||
}
|
||||
settingsModal.forceClose();
|
||||
showRestartOverlay(t('settings.restarting'));
|
||||
} catch (err) {
|
||||
console.error('Server restart failed:', err);
|
||||
showToast(t('settings.restore.error') + ': ' + err.message, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
// ─── Restart overlay ───────────────────────────────────────
|
||||
|
||||
function showRestartOverlay(message?: string): void {
|
||||
const msg = message || t('settings.restore.restarting');
|
||||
const overlay = document.createElement('div');
|
||||
overlay.id = 'restart-overlay';
|
||||
overlay.style.cssText =
|
||||
'position:fixed;inset:0;z-index:100000;display:flex;flex-direction:column;' +
|
||||
'align-items:center;justify-content:center;background:rgba(0,0,0,0.85);color:#fff;font-size:1.2rem;';
|
||||
overlay.innerHTML =
|
||||
'<div class="spinner" style="width:48px;height:48px;border:4px solid rgba(255,255,255,0.3);' +
|
||||
'border-top-color:#fff;border-radius:50%;animation:spin 0.8s linear infinite;margin-bottom:1rem;"></div>' +
|
||||
`<div id="restart-msg">${msg}</div>`;
|
||||
|
||||
// Add spinner animation if not present
|
||||
if (!document.getElementById('restart-spinner-style')) {
|
||||
const style = document.createElement('style');
|
||||
style.id = 'restart-spinner-style';
|
||||
style.textContent = '@keyframes spin{to{transform:rotate(360deg)}}';
|
||||
document.head.appendChild(style);
|
||||
}
|
||||
|
||||
document.body.appendChild(overlay);
|
||||
pollHealth();
|
||||
}
|
||||
|
||||
function pollHealth(): void {
|
||||
const start = Date.now();
|
||||
const maxWait = 30000;
|
||||
const interval = 1500;
|
||||
|
||||
const check = async () => {
|
||||
if (Date.now() - start > maxWait) {
|
||||
const msg = document.getElementById('restart-msg');
|
||||
if (msg) msg.textContent = t('settings.restore.restart_timeout');
|
||||
return;
|
||||
}
|
||||
try {
|
||||
const resp = await fetch('/health', { signal: AbortSignal.timeout(3000) });
|
||||
if (resp.ok) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
} catch { /* server still down */ }
|
||||
setTimeout(check, interval);
|
||||
};
|
||||
// Wait a moment before first check to let the server shut down
|
||||
setTimeout(check, 2000);
|
||||
}
|
||||
|
||||
// ─── Auto-Backup settings ─────────────────────────────────
|
||||
|
||||
export async function loadAutoBackupSettings(): Promise<void> {
|
||||
@@ -567,9 +513,6 @@ export async function restoreSavedBackup(filename: string): Promise<void> {
|
||||
showToast(data.message || t('settings.restore.success'), 'success');
|
||||
settingsModal.forceClose();
|
||||
|
||||
if (data.restart_scheduled) {
|
||||
showRestartOverlay();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Restore from saved backup failed:', err);
|
||||
showToast(t('settings.restore.error') + ': ' + err.message, 'error');
|
||||
@@ -687,9 +630,6 @@ export async function handlePartialImportFileSelected(input: HTMLInputElement):
|
||||
showToast(data.message || t('settings.partial.import_success'), 'success');
|
||||
settingsModal.forceClose();
|
||||
|
||||
if (data.restart_scheduled) {
|
||||
showRestartOverlay();
|
||||
}
|
||||
} catch (err) {
|
||||
console.error('Partial import failed:', err);
|
||||
showToast(t('settings.partial.import_error') + ': ' + err.message, 'error');
|
||||
|
||||
Reference in New Issue
Block a user