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:
@@ -900,6 +900,34 @@
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<!-- Update available banner -->
|
||||
<div id="update-banner" style="display:none"
|
||||
class="update-banner border-b transition-all duration-300 ease-in-out">
|
||||
<div class="mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-2" style="max-width:100%">
|
||||
<div class="flex items-center justify-between">
|
||||
<div class="flex items-center space-x-3">
|
||||
<i class="fas fa-arrow-circle-up text-lg"></i>
|
||||
<span class="text-sm font-medium" id="update-banner-text"
|
||||
aria-live="polite" aria-atomic="true">
|
||||
A new LEDMatrix update is available
|
||||
</span>
|
||||
</div>
|
||||
<div class="flex items-center space-x-3">
|
||||
<button onclick="applyUpdate()" id="update-banner-btn"
|
||||
class="inline-flex items-center px-3 py-1 text-xs font-semibold rounded-md
|
||||
update-banner-action transition-colors duration-150">
|
||||
<i class="fas fa-download mr-1"></i> Update Now
|
||||
</button>
|
||||
<button type="button" onclick="dismissUpdateBanner()"
|
||||
class="update-banner-dismiss rounded p-1 transition-colors duration-150"
|
||||
title="Dismiss" aria-label="Dismiss update">
|
||||
<i class="fas fa-times text-sm"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Main content -->
|
||||
<main class="mx-auto px-4 sm:px-6 lg:px-8 xl:px-12 2xl:px-16 py-8" style="max-width: 100%;">
|
||||
<!-- Navigation tabs -->
|
||||
@@ -4874,6 +4902,77 @@
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<!-- Update banner logic -->
|
||||
<script>
|
||||
(function() {
|
||||
var CHECK_INTERVAL = 30 * 60 * 1000; // 30 minutes
|
||||
|
||||
function getDismissedSha() {
|
||||
try { return sessionStorage.getItem('update-sha-dismissed'); } catch(e) { return null; }
|
||||
}
|
||||
|
||||
function checkForUpdate() {
|
||||
fetch('/api/v3/system/check-update')
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
if (data.update_available && getDismissedSha() !== data.remote_sha) {
|
||||
var n = data.commits_behind || 0;
|
||||
var msg = 'A new LEDMatrix update is available';
|
||||
if (n > 0) msg += ' (' + n + ' commit' + (n > 1 ? 's' : '') + ')';
|
||||
document.getElementById('update-banner-text').textContent = msg;
|
||||
document.getElementById('update-banner').style.display = '';
|
||||
try { sessionStorage.setItem('update-sha', data.remote_sha); } catch(e) {}
|
||||
} else {
|
||||
document.getElementById('update-banner').style.display = 'none';
|
||||
}
|
||||
})
|
||||
.catch(function() {});
|
||||
}
|
||||
|
||||
window.dismissUpdateBanner = function() {
|
||||
document.getElementById('update-banner').style.display = 'none';
|
||||
try {
|
||||
var sha = sessionStorage.getItem('update-sha');
|
||||
if (sha) sessionStorage.setItem('update-sha-dismissed', sha);
|
||||
} catch(e) {}
|
||||
};
|
||||
|
||||
window.applyUpdate = function() {
|
||||
var btn = document.getElementById('update-banner-btn');
|
||||
var originalHTML = '<i class="fas fa-download mr-1"></i> Update Now';
|
||||
btn.innerHTML = '<i class="fas fa-spinner fa-spin mr-1"></i> Updating...';
|
||||
btn.disabled = true;
|
||||
fetch('/api/v3/system/action', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ action: 'git_pull' })
|
||||
})
|
||||
.then(function(r) { return r.json(); })
|
||||
.then(function(data) {
|
||||
btn.innerHTML = originalHTML;
|
||||
btn.disabled = false;
|
||||
if (data.status === 'success') {
|
||||
document.getElementById('update-banner').style.display = 'none';
|
||||
try { sessionStorage.removeItem('update-sha-dismissed'); } catch(e) {}
|
||||
}
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification(data.message || 'Update complete', data.status || 'success');
|
||||
}
|
||||
})
|
||||
.catch(function() {
|
||||
btn.innerHTML = originalHTML;
|
||||
btn.disabled = false;
|
||||
if (typeof showNotification === 'function') {
|
||||
showNotification('Update failed — check your connection', 'error');
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
// Initial check shortly after page load, then periodic
|
||||
setTimeout(checkForUpdate, 2000);
|
||||
setInterval(checkForUpdate, CHECK_INTERVAL);
|
||||
})();
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user