From 114af496c76a0a5329cf7f03b28cdae3bbeb2173 Mon Sep 17 00:00:00 2001 From: Broque Thomas <26755000+Nezreka@users.noreply.github.com> Date: Wed, 4 Mar 2026 11:32:40 -0800 Subject: [PATCH] Track version --- .github/workflows/docker-publish.yml | 2 + Dockerfile | 4 ++ web_server.py | 58 ++++++++++++++++++++++++++++ webui/static/script.js | 33 ++++++++++++++++ webui/static/style.css | 17 ++++++++ 5 files changed, 114 insertions(+) diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 816ec349..11ba34a8 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -36,6 +36,8 @@ jobs: push: true no-cache: true pull: true + build-args: | + COMMIT_SHA=${{ github.sha }} tags: | boulderbadgedad/soulsync:latest boulderbadgedad/soulsync:${{ inputs.version_tag }} diff --git a/Dockerfile b/Dockerfile index 99d5fa8b..1b90257f 100644 --- a/Dockerfile +++ b/Dockerfile @@ -3,6 +3,10 @@ FROM python:3.11-slim +# Build-time commit SHA for update detection +ARG COMMIT_SHA="" +ENV SOULSYNC_COMMIT_SHA=${COMMIT_SHA} + # Set working directory WORKDIR /app diff --git a/web_server.py b/web_server.py index dbdc3998..b28df4c6 100644 --- a/web_server.py +++ b/web_server.py @@ -11925,6 +11925,64 @@ def _automatic_wishlist_cleanup_after_db_update(): import traceback traceback.print_exc() +# ── Update detection ───────────────────────────────────────────── +_GITHUB_REPO = "Nezreka/SoulSync" +_update_cache = {'latest_sha': None, 'last_check': 0, 'error': None} +_UPDATE_CHECK_INTERVAL = 3600 # 1 hour + +def _get_current_commit_sha(): + """Get the commit SHA of the running instance (env var for Docker, git for local).""" + # Docker: baked in at build time via COMMIT_SHA build arg + sha = os.environ.get('SOULSYNC_COMMIT_SHA', '').strip() + if sha: + return sha + # Local dev: read from git + try: + import subprocess + result = subprocess.run(['git', 'rev-parse', 'HEAD'], capture_output=True, text=True, cwd=os.path.dirname(__file__) or '.') + if result.returncode == 0: + return result.stdout.strip() + except Exception: + pass + return None + +_current_commit_sha = _get_current_commit_sha() + +def _check_for_updates(): + """Check GitHub for the latest commit SHA on main branch.""" + import time as _time + now = _time.time() + if now - _update_cache['last_check'] < _UPDATE_CHECK_INTERVAL: + return # Still fresh + _update_cache['last_check'] = now + try: + import urllib.request + import json as _json + req = urllib.request.Request( + f"https://api.github.com/repos/{_GITHUB_REPO}/commits/main", + headers={'Accept': 'application/vnd.github.v3+json', 'User-Agent': 'SoulSync-UpdateCheck'} + ) + with urllib.request.urlopen(req, timeout=10) as resp: + data = _json.loads(resp.read().decode()) + _update_cache['latest_sha'] = data.get('sha') + _update_cache['error'] = None + except Exception as e: + _update_cache['error'] = str(e) + logger.debug(f"Update check failed: {e}") + +@app.route('/api/update-check', methods=['GET']) +def check_for_update(): + """Check if a newer version is available on GitHub.""" + _check_for_updates() + current = _current_commit_sha + latest = _update_cache.get('latest_sha') + update_available = bool(current and latest and current != latest) + return jsonify({ + 'update_available': update_available, + 'current_sha': current[:8] if current else None, + 'latest_sha': latest[:8] if latest else None, + }) + @app.route('/api/version-info', methods=['GET']) def get_version_info(): """ diff --git a/webui/static/script.js b/webui/static/script.js index 44254801..6dcb540a 100644 --- a/webui/static/script.js +++ b/webui/static/script.js @@ -1241,6 +1241,10 @@ function initApp() { fetchAndUpdateServiceStatus(); setInterval(fetchAndUpdateServiceStatus, 10000); // Every 10 seconds (no-op when WebSocket active) + // Check for updates on load and every hour + checkForUpdates(); + setInterval(checkForUpdates, 3600000); + // Refresh key data immediately when user returns to this tab document.addEventListener('visibilitychange', () => { if (!document.hidden) { @@ -12076,7 +12080,36 @@ function formatArtists(artists) { return artistNames.join(', ') || 'Unknown Artist'; } +async function checkForUpdates() { + try { + const res = await fetch('/api/update-check'); + if (!res.ok) return; + const data = await res.json(); + const btn = document.querySelector('.version-button'); + if (!btn) return; + if (data.update_available) { + const dismissed = localStorage.getItem('soulsync-update-dismissed'); + if (dismissed !== data.latest_sha) { + btn.classList.add('update-available'); + } + } else { + btn.classList.remove('update-available'); + } + } catch (e) { + console.debug('Update check failed:', e); + } +} + async function showVersionInfo() { + // Dismiss update glow when user opens the modal (fire-and-forget, don't block modal) + const btn = document.querySelector('.version-button'); + if (btn && btn.classList.contains('update-available')) { + btn.classList.remove('update-available'); + fetch('/api/update-check').then(r => r.json()).then(data => { + if (data.latest_sha) localStorage.setItem('soulsync-update-dismissed', data.latest_sha); + }).catch(() => {}); + } + try { console.log('Fetching version info...'); diff --git a/webui/static/style.css b/webui/static/style.css index 4f13fe45..953b27d2 100644 --- a/webui/static/style.css +++ b/webui/static/style.css @@ -868,6 +868,23 @@ body { transform: scale(0.97); } +.version-button.update-available { + color: rgb(var(--accent-light-rgb)); + border-color: rgba(var(--accent-rgb), 0.4); + animation: version-glow 2s ease-in-out infinite; +} + +@keyframes version-glow { + 0%, 100% { + box-shadow: 0 0 4px rgba(var(--accent-rgb), 0.2), 0 0 8px rgba(var(--accent-rgb), 0.1); + border-color: rgba(var(--accent-rgb), 0.3); + } + 50% { + box-shadow: 0 0 8px rgba(var(--accent-rgb), 0.5), 0 0 20px rgba(var(--accent-rgb), 0.3), 0 0 30px rgba(var(--accent-rgb), 0.1); + border-color: rgba(var(--accent-rgb), 0.6); + } +} + /* Status Section - Premium Glassmorphic Design */ .status-section { height: fit-content;