diff --git a/web_interface/blueprints/api_v3.py b/web_interface/blueprints/api_v3.py index dbe4d016..1eae921e 100644 --- a/web_interface/blueprints/api_v3.py +++ b/web_interface/blueprints/api_v3.py @@ -9,6 +9,7 @@ import time import hashlib import uuid import logging +import threading from datetime import datetime, timezone from pathlib import Path from typing import Optional, Tuple, Dict, Any, Type @@ -1626,6 +1627,71 @@ 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 +_update_check_lock = threading.Lock() + +@api_v3.route('/system/check-update', methods=['GET']) +def check_for_update(): + """Check if a newer version is available on the remote.""" + now = time.time() + project_dir = str(PROJECT_ROOT) + with _update_check_lock: + if _update_check_cache.get('ts', 0) + _UPDATE_CHECK_TTL > now: + return jsonify(_update_check_cache['data']) + + try: + fetch_result = subprocess.run( + ['git', 'fetch', 'origin', 'main'], + capture_output=True, text=True, timeout=15, cwd=project_dir + ) + if fetch_result.returncode != 0: + raise RuntimeError(f"git fetch failed: {fetch_result.stderr.strip()}") + + local_result = subprocess.run( + ['git', 'rev-parse', 'HEAD'], + capture_output=True, text=True, timeout=5, cwd=project_dir + ) + if local_result.returncode != 0: + raise RuntimeError(f"git rev-parse HEAD failed: {local_result.stderr.strip()}") + local_sha = local_result.stdout.strip() + + remote_result = subprocess.run( + ['git', 'rev-parse', 'origin/main'], + capture_output=True, text=True, timeout=5, cwd=project_dir + ) + if remote_result.returncode != 0: + raise RuntimeError(f"git rev-parse origin/main failed: {remote_result.stderr.strip()}") + remote_sha = remote_result.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 + ) + if log_result.returncode != 0: + raise RuntimeError(f"git log failed: {log_result.stderr.strip()}") + lines = [commit_line for commit_line in log_result.stdout.strip().split('\n') if commit_line] + commits_behind = len(lines) + data = { + 'status': 'success', + 'update_available': commits_behind > 0, + 'local_sha': local_sha[:8], + 'remote_sha': remote_sha[:8], + 'commits_behind': commits_behind, + '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)""" @@ -1735,6 +1801,10 @@ def execute_system_action(): cwd=project_dir ) + # Invalidate update-check cache so the banner hides immediately + with _update_check_lock: + _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 38036194..8894b9de 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 a6aa748d..8e26515f 100644 --- a/web_interface/templates/v3/base.html +++ b/web_interface/templates/v3/base.html @@ -900,6 +900,34 @@ + +
+