From cc18e26d1cb4489989bec74364588f6a9095bbd7 Mon Sep 17 00:00:00 2001 From: ochenstarik-ui <267932263+ochenstarik-ui@users.noreply.github.com> Date: Wed, 2 Sep 2026 13:12:08 +0700 Subject: [PATCH 1/2] feat(updater): A59 visible update modal, progress bar, cancel, isolation and restart tracking --- .../router/action_handler.py | 10 + src/antigravity_provider/router/web/server.py | 31 +- .../router/web/static/app.js | 283 +++++++--- .../updater/update_manager.py | 496 ++++++++++++++++-- tests/test_a59_visible_update.py | 315 +++++++++++ 5 files changed, 1028 insertions(+), 107 deletions(-) create mode 100644 tests/test_a59_visible_update.py diff --git a/src/antigravity_provider/router/action_handler.py b/src/antigravity_provider/router/action_handler.py index 1bc1ecc..02ab1c1 100644 --- a/src/antigravity_provider/router/action_handler.py +++ b/src/antigravity_provider/router/action_handler.py @@ -1364,6 +1364,16 @@ class ActionExecutor: status = mgr.get_status_dict() return {'ok': True, 'message': status.get('message') or 'Статус получен', 'data': status} + elif action == 'get_update_progress': + mgr = UpdateManager() + progress = mgr.get_progress_dict() + return {'ok': True, 'message': progress.get('message') or 'Ход обновления', 'data': progress} + + elif action == 'cancel_update': + mgr = UpdateManager() + progress = mgr.cancel_download() + return {'ok': True, 'message': 'Загрузка обновления отменена', 'data': progress} + elif action == 'run_preflight': from antigravity_provider.router.preflight_service import PreflightCheckService service = PreflightCheckService.get() diff --git a/src/antigravity_provider/router/web/server.py b/src/antigravity_provider/router/web/server.py index e413e59..f5bde22 100644 --- a/src/antigravity_provider/router/web/server.py +++ b/src/antigravity_provider/router/web/server.py @@ -23,7 +23,13 @@ from antigravity_provider.router.state_store import HubStateStore from antigravity_provider.router.action_handler import ActionExecutor from antigravity_provider.router.router_config import load_router_config -from antigravity_provider.updater.update_manager import get_installed_commit, get_installed_build_time, UpdateManager +from antigravity_provider.updater.update_manager import ( + get_installed_commit, + get_installed_build_time, + get_last_applied_update, + acknowledge_last_applied_update, + UpdateManager, +) logger = logging.getLogger("hermes.router.web") @@ -258,6 +264,8 @@ def get_snapshot(authorized: bool = Depends(get_auth_token)): "config_dir": str(paths.get_config_dir()), "log_file": str(paths.get_log_file()), } + from antigravity_provider.updater.update_manager import get_last_applied_update + snap_dict["last_applied_update"] = get_last_applied_update() return JSONResponse(content=jsonable_encoder(snap_dict)) @app.post("/api/action") @@ -512,6 +520,7 @@ def get_settings(authorized: bool = Depends(get_auth_token)): "installed_at": get_installed_build_time(), "version": __version__, "last_update_check": last_check.to_dict() if last_check else None, + "last_applied_update": get_last_applied_update(), "network_security": { "is_external_bind": is_external, "is_tls": False, @@ -669,6 +678,26 @@ def _background_refresh_loop() -> None: except Exception as exc: logger.debug("Initial background update check skipped: %s", exc) + # Check if update was just applied and log event to EventLogService + try: + from antigravity_provider.updater.update_manager import get_last_applied_update, acknowledge_last_applied_update + from antigravity_provider.router.unified_health import EventLogService + applied = get_last_applied_update() + if applied and not applied.get("acknowledged"): + prev_v = applied.get("prev_version", "unknown") + prev_c = (applied.get("prev_commit") or "unknown")[:7] + new_v = applied.get("new_version", "unknown") + new_c = (applied.get("new_commit") or "unknown")[:7] + EventLogService.get().log( + "system", + f"Hermes Hub успешно обновлён с {prev_v} ({prev_c}) до {new_v} ({new_c})", + level="info", + ) + acknowledge_last_applied_update() + logger.info("Recorded post-update event in EventLogService: %s -> %s", prev_c, new_c) + except Exception as exc: + logger.debug("Check last_applied_update on startup skipped: %s", exc) + while not _background_stop.is_set(): try: AccountProbeService.get().tick() diff --git a/src/antigravity_provider/router/web/static/app.js b/src/antigravity_provider/router/web/static/app.js index 945c94d..7d17d2a 100644 --- a/src/antigravity_provider/router/web/static/app.js +++ b/src/antigravity_provider/router/web/static/app.js @@ -195,7 +195,7 @@ function initEventListeners() { const btnApplyUpdate = document.getElementById('btn-apply-update'); if (btnApplyUpdate) { - btnApplyUpdate.addEventListener('click', () => applyUpdate()); + btnApplyUpdate.addEventListener('click', () => openUpdateModal('details')); } // Preflight check listener @@ -549,6 +549,8 @@ async function saveAuthTokenFromPrompt() { // сам себя. Поэтому опросы не только молчат, но и не дёргают снапшот. const SILENT_ACTIONS = new Set([ 'get_compression_status', + 'get_update_progress', + 'cancel_update', 'poll_native_auth', 'poll_native_agy_login', 'poll_terminal_auth', 'poll_redirect_auth', 'poll_device_auth', ]); @@ -2364,6 +2366,7 @@ function closeModal() { stopDeviceAuthPolling(); // Опрос входа по ссылке иначе продолжал бы стучать в закрытое окно. stopRedirectAuthPolling(); + stopUpdateProgressPolling(); if (elements.modalBackdrop) elements.modalBackdrop.classList.add('hidden'); } @@ -2429,7 +2432,33 @@ function applyTheme(theme) { } } -// ── UPDATE MANAGEMENT (P0-1 / In-App Updates) ── +// ── UPDATE MANAGEMENT (A59 / In-App Updates) ── +let updateProgressInterval = null; + +function stopUpdateProgressPolling() { + if (updateProgressInterval) { + clearInterval(updateProgressInterval); + updateProgressInterval = null; + } +} + +function checkPostUpdateNotification() { + const applied = (currentSnapshot && currentSnapshot.last_applied_update) || (currentSettings && currentSettings.last_applied_update); + if (!applied || !applied.new_commit) return; + const key = 'hermes_notified_update_' + (applied.new_commit || applied.new_version); + if (!localStorage.getItem(key)) { + const prevC = applied.prev_commit ? applied.prev_commit.slice(0, 7) : '—'; + const newC = applied.new_commit ? applied.new_commit.slice(0, 7) : '—'; + const newV = applied.new_version || '0.1.3'; + showToast( + `Hermes Hub успешно обновлён до версии ${newV} (сборка ${newC}). Предыдущая сборка: ${prevC}`, + 'success', + 10000 + ); + localStorage.setItem(key, '1'); + } +} + async function checkUpdates(silent = false) { if (!silent) { showToast('Проверка обновлений...', 'info'); @@ -2439,11 +2468,20 @@ async function checkUpdates(silent = false) { if (res && res.ok && res.data) { latestUpdateInfo = res.data; renderUpdateUI(); - if (!silent) { - if (res.data.update_available) { + if (res.data.update_available) { + const versionKey = res.data.latest_commit ? res.data.latest_commit.slice(0, 7) : (res.data.latest_version || res.data.release_tag || ''); + if (silent) { + const dismissed = localStorage.getItem('hermes_dismissed_update_' + versionKey); + if (!dismissed) { + openUpdateModal('details'); + } + } else { const c = res.data.latest_commit ? res.data.latest_commit.slice(0, 7) : (res.data.release_tag || 'new'); showToast(`Доступно обновление (сборка ${c})`, 'info'); - } else { + openUpdateModal('details'); + } + } else { + if (!silent) { showToast(res.data.message || 'Установлена последняя сборка', 'success'); } } @@ -2468,14 +2506,6 @@ function renderUpdateUI() { const badgeText = document.getElementById('header-update-text'); const commitTag = document.getElementById('commit-tag'); - // Первым источником — снапшот работающего сервера: он приходит всегда, а - // панель обновлений заполняется только при её открытии. На Linux строка - // сборки поэтому оставалась пустой, и понять, дошло ли обновление, было - // нельзя. - // - // Берём running_commit — коммит, снятый при СТАРТЕ процесса. Поле commit - // читается с диска при каждом запросе, и переживший обновление процесс - // рапортует им свежий номер при старом поведении. const runningCommit = (currentSnapshot && currentSnapshot.running_commit) || ''; const installedCommit = runningCommit || (latestUpdateInfo && latestUpdateInfo.installed_commit && latestUpdateInfo.installed_commit !== 'unknown' @@ -2513,9 +2543,6 @@ function renderUpdateUI() { const releaseMeta = document.getElementById('update-release-meta'); const releaseNotes = document.getElementById('update-release-notes'); - // Версия берётся ТОЛЬКО из API. Раньше номер был зашит в разметке и в - // запасном значении: подъём версии в коде до интерфейса не доходил, и - // владелец видел старый номер при новой сборке. const curVer = (latestUpdateInfo && latestUpdateInfo.current_version) || (currentSettings && currentSettings.version) || ''; const cDisplay = installedCommit ? installedCommit.slice(0, 7) : 'неизвестно'; if (updateInfoDesc) { @@ -2573,9 +2600,11 @@ function renderUpdateUI() { detailsBlock.classList.add('hidden'); } } + + checkPostUpdateNotification(); } -function openUpdateModal() { +function openUpdateModal(mode = 'details') { if (!latestUpdateInfo) { checkUpdates(false); return; @@ -2585,62 +2614,196 @@ function openUpdateModal() { ? latestUpdateInfo.installed_commit.slice(0, 7) : 'неизвестно'; const latC = latestUpdateInfo.latest_commit ? latestUpdateInfo.latest_commit.slice(0, 7) : (latestUpdateInfo.release_tag || '—'); + const curVer = latestUpdateInfo.current_version || (currentSettings && currentSettings.version) || '0.1.3'; + const newVer = latestUpdateInfo.latest_version || latestUpdateInfo.release_tag || curVer; + const versionKey = latestUpdateInfo.latest_commit ? latestUpdateInfo.latest_commit.slice(0, 7) : (latestUpdateInfo.latest_version || latestUpdateInfo.release_tag || ''); - if (elements.modalTitle) elements.modalTitle.textContent = 'Обновление Hermes Hub'; - if (elements.modalBody) { - elements.modalBody.innerHTML = ` -