mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-05-01 13:03:01 +00:00
feat(web): update-available banner in web UI (#311)
* 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) <noreply@anthropic.com> * fix(update-banner): address review findings — lock, returncode checks, update_available logic, a11y, button state - Add _update_check_lock (threading.Lock) around all reads/writes to _update_check_cache in check_for_update() and git_pull, preventing races on concurrent requests - Validate returncode for git fetch, rev-parse HEAD, and rev-parse origin/main; raise RuntimeError on failure so errors are caught and returned as error payloads instead of silently producing stale/empty SHAs - Set update_available = commits_behind > 0 (was unconditionally True when local_sha != remote_sha); prevents false positive when local is ahead of remote - Add type="button" and aria-label="Dismiss update" to the icon-only dismiss button - Restore btn.innerHTML and btn.disabled in both success and error paths of applyUpdate(); only hide the banner and clear sessionStorage when data.status === 'success' Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> * fix(update-banner): address second-round review findings api_v3.py: - Move all git work inside _update_check_lock so concurrent requests re-check cache staleness after acquiring the lock; only the first caller runs git fetch/rev-parse/log, subsequent callers return the cached result - Check log_result.returncode and raise on failure so a broken git log doesn't produce a silent false-negative (commits_behind=0) - Rename loop variable l → commit_line base.html: - Replace boolean _dismissed flag with SHA-scoped sessionStorage key 'update-sha-dismissed'; dismissing for SHA X still allows the banner to reappear when origin/main advances to SHA Y - Successful applyUpdate clears 'update-sha-dismissed' so the next update cycle can show the banner again - Add aria-live="polite" aria-atomic="true" to #update-banner-text so screen readers announce content changes Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> --------- Co-authored-by: Chuck <chuck@example.com> Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -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."
|
||||
|
||||
Reference in New Issue
Block a user