mirror of
https://github.com/ChuckBuilds/LEDMatrix.git
synced 2026-06-20 11:38:37 +00:00
Compare commits
3 Commits
db9585cea9
...
5e6c40ad55
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5e6c40ad55 | ||
|
|
d6bd1ee215 | ||
|
|
acaf8a248e |
@@ -1,16 +1,8 @@
|
||||
{
|
||||
"ledmatrix-weather": {
|
||||
"api_key": "YOUR_OPENWEATHERMAP_API_KEY"
|
||||
},
|
||||
"youtube": {
|
||||
"api_key": "YOUR_YOUTUBE_API_KEY",
|
||||
"channel_id": "YOUR_YOUTUBE_CHANNEL_ID"
|
||||
},
|
||||
"music": {
|
||||
"SPOTIFY_CLIENT_ID": "YOUR_SPOTIFY_CLIENT_ID_HERE",
|
||||
"SPOTIFY_CLIENT_SECRET": "YOUR_SPOTIFY_CLIENT_SECRET_HERE",
|
||||
"SPOTIFY_REDIRECT_URI": "http://127.0.0.1:8888/callback"
|
||||
},
|
||||
"github": {
|
||||
"api_token": "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||
}
|
||||
|
||||
@@ -599,9 +599,13 @@ if [ ! -f "$PROJECT_ROOT_DIR/config/config_secrets.json" ]; then
|
||||
echo "⚠ Template config/config_secrets.template.json not found; creating a minimal secrets file"
|
||||
cat > "$PROJECT_ROOT_DIR/config/config_secrets.json" <<'EOF'
|
||||
{
|
||||
"weather": {
|
||||
"api_key": "YOUR_OPENWEATHERMAP_API_KEY"
|
||||
}
|
||||
"youtube": {
|
||||
"api_key": "YOUR_YOUTUBE_API_KEY",
|
||||
"channel_id": "YOUR_YOUTUBE_CHANNEL_ID"
|
||||
},
|
||||
"github": {
|
||||
"api_token": "YOUR_GITHUB_PERSONAL_ACCESS_TOKEN"
|
||||
}
|
||||
}
|
||||
EOF
|
||||
# Check if service runs as root and set ownership accordingly
|
||||
|
||||
@@ -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."
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -1165,9 +1165,11 @@ function initializePluginPageWhenReady() {
|
||||
if (target.id === 'plugins-content' ||
|
||||
target.querySelector('#installed-plugins-grid')) {
|
||||
console.log('HTMX swap detected for plugins, initializing...');
|
||||
// Reset initialization flag to allow re-initialization after HTMX swap
|
||||
// Reset all initialization flags so the fresh empty DOM gets populated
|
||||
window.pluginManager.initialized = false;
|
||||
window.pluginManager.initializing = false;
|
||||
window.pluginManager._reswap = true; // signal: use cached store, don't re-fetch GitHub
|
||||
pluginsInitialized = false;
|
||||
initTimer = setTimeout(attemptInit, 100);
|
||||
}
|
||||
}, { once: false }); // Allow multiple swaps
|
||||
@@ -1211,9 +1213,13 @@ function initializePlugins() {
|
||||
console.warn('[INIT] checkGitHubAuthStatus not available yet');
|
||||
}
|
||||
|
||||
// Load both installed plugins and plugin store
|
||||
// Load both installed plugins and plugin store.
|
||||
// On HTMX re-swaps use cached store data (fetchCommitInfo=false) to avoid
|
||||
// re-hitting GitHub on every tab switch; only fetch fresh on first load.
|
||||
const isReswap = !!window.pluginManager._reswap;
|
||||
window.pluginManager._reswap = false;
|
||||
loadInstalledPlugins();
|
||||
searchPluginStore(true); // Load plugin store with fresh metadata from GitHub
|
||||
searchPluginStore(!isReswap);
|
||||
|
||||
// Setup search functionality (with guard against duplicate listeners)
|
||||
const searchInput = document.getElementById('plugin-search');
|
||||
@@ -5127,7 +5133,7 @@ function refreshPlugins() {
|
||||
pluginStoreCache = null;
|
||||
cacheTimestamp = null;
|
||||
|
||||
loadInstalledPlugins();
|
||||
refreshInstalledPlugins(); // invalidates cache before fetching
|
||||
// Fetch latest metadata from GitHub when refreshing
|
||||
searchPluginStore(true);
|
||||
showNotification('Plugins refreshed with latest metadata from GitHub', 'success');
|
||||
|
||||
@@ -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