refactor(webui): replace real-time polling with click-to-check version updates

- Remove background PyPI polling loop and WebSocket broadcast
- Remove UpdateBanner from ThreadHeader (keep main page clean)
- Add on-demand version check endpoint (GET /api/settings/version-check)
- Add 'About' section in Settings > Overview with check-for-updates button
- Design: no auto-fetch, user initiates check explicitly via button click
This commit is contained in:
Jiajun Xie
2026-06-10 18:11:06 +08:00
committed by Xubin Ren
parent c00371c761
commit 4255656089
6 changed files with 200 additions and 0 deletions
+10
View File
@@ -34,9 +34,18 @@ from nanobot.webui.workspaces import (
write_webui_default_access_mode,
)
from nanobot import __version__
QueryParams = dict[str, list[str]]
RuntimeSurface = Literal["browser", "native"]
def _version_payload() -> dict[str, Any]:
"""Return version info for the settings payload."""
return {
"current": __version__,
}
_RUNTIME_CAPABILITIES = {
"can_restart_engine": False,
"can_pick_folder": False,
@@ -805,6 +814,7 @@ def settings_payload(
"exec_path_append_set": bool(exec_config.path_append),
},
"requires_restart": requires_restart,
"version": _version_payload(),
}
return decorate_settings_payload(
payload,
+15
View File
@@ -36,6 +36,7 @@ from nanobot.webui.settings_api import (
update_transcription_settings,
update_web_search_settings,
)
from nanobot.webui.version_check import check_for_update
QueryParams = dict[str, list[str]]
@@ -117,6 +118,8 @@ class WebUISettingsRouter:
return await self._handle_settings_cli_apps_action(request, "test")
if path == "/api/settings/mcp-presets":
return await self._handle_settings_mcp_presets(request)
if path == "/api/settings/version-check":
return await self._handle_settings_version_check(request)
mcp_action = _MCP_PRESET_ACTIONS_BY_PATH.get(path)
if mcp_action is not None:
return await self._handle_settings_mcp_presets(request, mcp_action)
@@ -347,3 +350,15 @@ class WebUISettingsRouter:
if action is None:
return self._json_response(payload)
return self._json_response(self._with_restart_state(payload, section="runtime"))
async def _handle_settings_version_check(self, request: WsRequest) -> Response:
if not self._authorized(request):
return self._unauthorized()
try:
update_info = await asyncio.to_thread(check_for_update)
except Exception:
self.logger.exception("version check failed")
return self._error_response(500, "version check failed")
return self._json_response({
"updateAvailable": update_info,
})
+51
View File
@@ -0,0 +1,51 @@
"""On-demand version checker for nanobot-ai releases.
Checks PyPI for newer versions when explicitly requested (no background polling).
"""
from __future__ import annotations
import logging
import time
from typing import Any
import httpx
from nanobot import __version__
logger = logging.getLogger(__name__)
_PYPI_URL = "https://pypi.org/pypi/nanobot-ai/json"
_CACHE_TTL_S = 300 # 5 minutes cache to avoid hammering PyPI
_cache: tuple[float, str | None] = (0.0, None)
def check_for_update() -> dict[str, Any] | None:
"""Check PyPI for a newer version. Returns update info dict or None if up-to-date.
Uses a short cache to avoid repeated requests within the TTL window.
This is a blocking call — invoke from a thread or background task.
"""
global _cache
now = time.monotonic()
cached_at, cached_val = _cache
if now - cached_at < _CACHE_TTL_S and cached_val is not None:
latest = cached_val
else:
try:
resp = httpx.get(_PYPI_URL, timeout=5.0, follow_redirects=True)
resp.raise_for_status()
latest = resp.json().get("info", {}).get("version")
except Exception:
logger.debug("PyPI version check failed", exc_info=True)
return None
_cache = (now, latest)
if not latest or latest == __version__:
return None
return {
"currentVersion": __version__,
"latestVersion": latest,
"pypiUrl": "https://pypi.org/project/nanobot-ai/",
}