diff --git a/docker/host-updater.py b/docker/host-updater.py index ff3c8d6..354f8f6 100755 --- a/docker/host-updater.py +++ b/docker/host-updater.py @@ -6,6 +6,7 @@ import signal import socketserver import subprocess import threading +import time from datetime import datetime, timezone from http import HTTPStatus from http.server import BaseHTTPRequestHandler @@ -67,7 +68,10 @@ def is_authorized(headers): def watch_process(process): + global current_process exit_code = process.wait() + with status_lock: + current_process = None update_status( status="success" if exit_code == 0 else "error", message="Synapsis update completed." if exit_code == 0 else "Synapsis update failed.", @@ -78,6 +82,32 @@ def watch_process(process): ) +def start_update_process(): + global current_process + + # Give the app enough time to return the 202 response before the container restarts. + time.sleep(2) + + with status_lock: + log_handle = open(LOG_FILE, "a", encoding="utf-8") + current_process = subprocess.Popen( + [UPDATE_SCRIPT], + cwd=INSTALL_DIR, + stdout=log_handle, + stderr=subprocess.STDOUT, + start_new_session=True, + env={ + **os.environ, + "INSTALL_DIR": INSTALL_DIR, + }, + ) + + update_status(pid=current_process.pid) + + thread = threading.Thread(target=watch_process, args=(current_process,), daemon=True) + thread.start() + + class ThreadedUnixServer(socketserver.ThreadingMixIn, socketserver.UnixStreamServer): daemon_threads = True @@ -133,30 +163,17 @@ class Handler(BaseHTTPRequestHandler): ) return - log_handle = open(LOG_FILE, "a", encoding="utf-8") - current_process = subprocess.Popen( - [UPDATE_SCRIPT], - cwd=INSTALL_DIR, - stdout=log_handle, - stderr=subprocess.STDOUT, - start_new_session=True, - env={ - **os.environ, - "INSTALL_DIR": INSTALL_DIR, - }, - ) - update_status( status="updating", - message="Synapsis update started.", + message="Synapsis update scheduled.", lastStartedAt=now_iso(), lastFinishedAt=None, lastExitCode=None, lastError=None, - pid=current_process.pid, + pid=None, ) - thread = threading.Thread(target=watch_process, args=(current_process,), daemon=True) + thread = threading.Thread(target=start_update_process, daemon=True) thread.start() self.send_json( @@ -164,7 +181,7 @@ class Handler(BaseHTTPRequestHandler): { "ok": True, "status": "updating", - "message": "Synapsis update started.", + "message": "Synapsis update scheduled. The node will restart shortly.", }, ) diff --git a/docker/update-local.sh b/docker/update-local.sh index b91bd2f..d681e47 100755 --- a/docker/update-local.sh +++ b/docker/update-local.sh @@ -63,11 +63,19 @@ case "${ACTIVE_PROXY}" in ;; esac +download_file "docker/host-updater.py" "${INSTALL_DIR}/host-updater.py" +download_file "docker/update-local.sh" "${INSTALL_DIR}/update-local.sh" +chmod 755 "${INSTALL_DIR}/host-updater.py" "${INSTALL_DIR}/update-local.sh" + echo "🐳 Pulling latest Synapsis image" compose_cmd pull echo "🚀 Restarting Synapsis" compose_cmd up -d --remove-orphans +if command -v systemctl >/dev/null 2>&1 && systemctl list-unit-files synapsis-updater.service >/dev/null 2>&1; then + systemctl restart synapsis-updater.service >/dev/null 2>&1 || true +fi + echo "" echo "✅ Synapsis has been updated to the latest published image." diff --git a/src/app/admin/page.tsx b/src/app/admin/page.tsx index 3635a53..cd186e1 100644 --- a/src/app/admin/page.tsx +++ b/src/app/admin/page.tsx @@ -231,7 +231,7 @@ export default function AdminPage() { const handleTriggerUpdate = async () => { setTriggeringUpdate(true); try { - const res = await fetch('/api/admin/update', { method: 'POST' }); + const res = await fetch('/api/admin/update', { method: 'POST', keepalive: true }); const data = await res.json().catch(() => ({})); if (!res.ok) { @@ -241,7 +241,15 @@ export default function AdminPage() { showToast(data.message || 'Update started. Synapsis will restart shortly.', 'success'); await loadUpdateStatus(); } catch (error) { - showToast(error instanceof Error ? error.message : 'Failed to start update', 'error'); + const message = error instanceof Error ? error.message : 'Failed to start update'; + if (message.toLowerCase().includes('fetch') || message.toLowerCase().includes('network')) { + showToast('Update likely started. The node is restarting, so this page may disconnect briefly.', 'success'); + window.setTimeout(() => { + window.location.reload(); + }, 5000); + } else { + showToast(message, 'error'); + } } finally { setTriggeringUpdate(false); }