From 3ad331efce16ca503706618957f04f866a8c14c5 Mon Sep 17 00:00:00 2001 From: Chuck Date: Thu, 16 Apr 2026 15:56:13 -0400 Subject: [PATCH] feat(web): add update-available banner to web UI Adds a polite, dismissible banner between the header and navigation tabs that appears when the local repo is behind origin/main. Shows commit count and a one-click "Update Now" button that triggers the existing git_pull action. - New GET /api/v3/system/check-update endpoint (5-min cache, compares local HEAD vs origin/main SHA) - Banner auto-checks on page load then every 30 minutes - Dismiss persists for the browser session via sessionStorage - Styled for both light and dark themes - Cache invalidated after successful git_pull so banner hides immediately Co-Authored-By: Claude Opus 4.6 (1M context) --- web_interface/blueprints/api_v3.py | 54 ++++++++++++++++ web_interface/static/v3/app.css | 36 +++++++++++ web_interface/templates/v3/base.html | 93 ++++++++++++++++++++++++++++ 3 files changed, 183 insertions(+) diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index 7fe9c7b32..3204eaa20 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -1341,6 +1341,57 @@ def get_system_version(): logger.exception("[System] get_system_version failed") return jsonify({'status': 'error', 'message': 'Failed to get system version'}), 500 +_update_check_cache: Dict = {} +_UPDATE_CHECK_TTL = 300 # 5 minutes + +@api_v3.route('/system/check-update', methods=['GET']) +def check_for_update(): + """Check if a newer version is available on the remote.""" + import time as _time + now = _time.time() + if _update_check_cache.get('ts', 0) + _UPDATE_CHECK_TTL > now: + return jsonify(_update_check_cache['data']) + + project_dir = str(PROJECT_ROOT) + try: + subprocess.run( + ['git', 'fetch', 'origin', 'main'], + capture_output=True, text=True, timeout=15, cwd=project_dir + ) + local_sha = subprocess.run( + ['git', 'rev-parse', 'HEAD'], + capture_output=True, text=True, timeout=5, cwd=project_dir + ).stdout.strip() + remote_sha = subprocess.run( + ['git', 'rev-parse', 'origin/main'], + capture_output=True, text=True, timeout=5, cwd=project_dir + ).stdout.strip() + + if local_sha == remote_sha: + data = {'status': 'success', 'update_available': False, + 'local_sha': local_sha[:8], 'remote_sha': remote_sha[:8]} + else: + log_result = subprocess.run( + ['git', 'log', 'HEAD..origin/main', '--oneline'], + capture_output=True, text=True, timeout=5, cwd=project_dir + ) + lines = [l for l in log_result.stdout.strip().split('\n') if l] + data = { + 'status': 'success', + 'update_available': True, + 'local_sha': local_sha[:8], + 'remote_sha': remote_sha[:8], + 'commits_behind': len(lines), + 'latest_message': lines[0].split(' ', 1)[1] if lines else '', + } + except Exception as e: + logger.warning("[System] check-update failed: %s", e) + data = {'status': 'error', 'update_available': False, 'message': str(e)} + + _update_check_cache['ts'] = now + _update_check_cache['data'] = data + return jsonify(data) + @api_v3.route('/system/action', methods=['POST']) def execute_system_action(): """Execute system actions (start/stop/reboot/etc)""" @@ -1450,6 +1501,9 @@ def execute_system_action(): cwd=project_dir ) + # Invalidate update-check cache so the banner hides immediately + _update_check_cache.clear() + # Return custom response for git_pull if result.returncode == 0: pull_message = "Code updated successfully." diff --git a/web_interface/static/v3/app.css b/web_interface/static/v3/app.css index 38036194d..8894b9de7 100644 --- a/web_interface/static/v3/app.css +++ b/web_interface/static/v3/app.css @@ -1004,3 +1004,39 @@ button.bg-white { [data-theme="dark"] .theme-toggle-btn { color: #fbbf24; } + +/* Update available banner */ +.update-banner { + background-color: #eff6ff; + border-color: #bfdbfe; + color: #1e40af; +} +.update-banner-action { + background-color: #3b82f6; + color: #fff; +} +.update-banner-action:hover { + background-color: #2563eb; +} +.update-banner-dismiss { + color: #1e40af; + opacity: 0.6; +} +.update-banner-dismiss:hover { + opacity: 1; +} + +[data-theme="dark"] .update-banner { + background-color: #1e293b; + border-color: #334155; + color: #93c5fd; +} +[data-theme="dark"] .update-banner-action { + background-color: #2563eb; +} +[data-theme="dark"] .update-banner-action:hover { + background-color: #3b82f6; +} +[data-theme="dark"] .update-banner-dismiss { + color: #93c5fd; +} diff --git a/web_interface/templates/v3/base.html b/web_interface/templates/v3/base.html index e3da43d32..03a2f3a49 100644 --- a/web_interface/templates/v3/base.html +++ b/web_interface/templates/v3/base.html @@ -931,6 +931,33 @@

+ + +
@@ -4888,6 +4915,72 @@

Run Plugin On-Deman + +