feat: Add update checker to verify new versions on GitHub

- Add /api/check-update endpoint that queries GitHub API
- Compare current commit hash with latest on dev branch
- Add check button next to version in menu
- Show spinning icon during check, green checkmark when done
- Display "Update available" link when newer version exists
- Handle rate limits and network errors gracefully

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
MarekWo
2026-01-20 17:14:59 +01:00
parent 0dc66b8f3c
commit f36c2eb3c8
4 changed files with 199 additions and 2 deletions

View File

@@ -7,6 +7,7 @@ import json
import re
import base64
import time
import requests
from datetime import datetime
from io import BytesIO
from flask import Blueprint, jsonify, request, send_file
@@ -1999,6 +2000,126 @@ def get_version():
}), 200
# GitHub repository for update checks
GITHUB_REPO = "MarekWo/mc-webui"
GITHUB_BRANCH = "dev" # Check updates against dev branch
@api_bp.route('/check-update', methods=['GET'])
def check_update():
"""
Check if a newer version is available on GitHub.
Compares current commit hash with latest commit on GitHub.
Query parameters:
branch (str): Branch to check (default: dev)
Returns:
JSON with update status:
{
"success": true,
"update_available": true,
"current_version": "2026.01.18+abc1234",
"current_commit": "abc1234",
"latest_commit": "def5678",
"latest_date": "2026.01.20",
"latest_message": "feat: New feature",
"github_url": "https://github.com/MarekWo/mc-webui/commits/dev"
}
"""
from app.version import VERSION_STRING
try:
branch = request.args.get('branch', GITHUB_BRANCH)
# Extract current commit hash from VERSION_STRING (format: YYYY.MM.DD+hash or YYYY.MM.DD+hash+dirty)
current_commit = None
if '+' in VERSION_STRING:
parts = VERSION_STRING.split('+')
if len(parts) >= 2:
current_commit = parts[1] # Get hash part (skip date, ignore +dirty)
if not current_commit or current_commit == 'unknown':
return jsonify({
'success': False,
'error': 'Cannot determine current version. Run version freeze first.'
}), 400
# Fetch latest commit from GitHub API
github_api_url = f"https://api.github.com/repos/{GITHUB_REPO}/commits/{branch}"
headers = {
'Accept': 'application/vnd.github.v3+json',
'User-Agent': 'mc-webui-update-checker'
}
response = requests.get(github_api_url, headers=headers, timeout=10)
if response.status_code == 403:
return jsonify({
'success': False,
'error': 'GitHub API rate limit exceeded. Try again later.'
}), 429
if response.status_code != 200:
return jsonify({
'success': False,
'error': f'GitHub API error: {response.status_code}'
}), 502
data = response.json()
latest_full_sha = data.get('sha', '')
latest_commit = latest_full_sha[:7] # Short hash (7 chars like git default)
# Get commit details
commit_info = data.get('commit', {})
latest_message = commit_info.get('message', '').split('\n')[0] # First line only
commit_date = commit_info.get('committer', {}).get('date', '')
# Parse date to YYYY.MM.DD format
latest_date = ''
if commit_date:
try:
dt = datetime.fromisoformat(commit_date.replace('Z', '+00:00'))
latest_date = dt.strftime('%Y.%m.%d')
except ValueError:
latest_date = commit_date[:10]
# Compare commits (case-insensitive, compare first 7 chars)
update_available = current_commit.lower()[:7] != latest_commit.lower()[:7]
return jsonify({
'success': True,
'update_available': update_available,
'current_version': VERSION_STRING,
'current_commit': current_commit[:7],
'latest_commit': latest_commit,
'latest_date': latest_date,
'latest_message': latest_message,
'github_url': f"https://github.com/{GITHUB_REPO}/commits/{branch}"
}), 200
except requests.Timeout:
return jsonify({
'success': False,
'error': 'GitHub API request timed out'
}), 504
except requests.RequestException as e:
logger.error(f"Error checking for updates: {e}")
return jsonify({
'success': False,
'error': f'Network error: {str(e)}'
}), 502
except Exception as e:
logger.error(f"Error checking for updates: {e}")
return jsonify({
'success': False,
'error': str(e)
}), 500
@api_bp.route('/read_status/mark_read', methods=['POST'])
def mark_read_api():
"""

View File

@@ -364,6 +364,16 @@ main {
height: 100%;
}
/* Spin animation for update check button */
.spin {
animation: spin 1s linear infinite;
}
@keyframes spin {
from { transform: rotate(0deg); }
to { transform: rotate(360deg); }
}
/* Empty State */
.empty-state {
text-align: center;

View File

@@ -398,6 +398,14 @@ function setupEventListeners() {
}
});
// Check for app updates button
const checkUpdateBtn = document.getElementById('checkUpdateBtn');
if (checkUpdateBtn) {
checkUpdateBtn.addEventListener('click', async function() {
await checkForAppUpdates();
});
}
// Date selector (archive selection)
document.getElementById('dateSelector').addEventListener('change', function(e) {
currentArchiveDate = e.target.value || null;
@@ -1321,6 +1329,59 @@ function showNotification(message, type = 'info') {
toast.show();
}
/**
* Check for app updates from GitHub
*/
async function checkForAppUpdates() {
const btn = document.getElementById('checkUpdateBtn');
const icon = document.getElementById('checkUpdateIcon');
const versionText = document.getElementById('versionText');
if (!btn || !icon) return;
// Show loading state
btn.disabled = true;
icon.className = 'bi bi-arrow-repeat spin';
try {
const response = await fetch('/api/check-update');
const data = await response.json();
if (data.success) {
if (data.update_available) {
// Update available - show green with link
versionText.innerHTML = `${data.current_version} <a href="${data.github_url}" target="_blank" class="text-success" title="Update available: ${data.latest_date}+${data.latest_commit}"><i class="bi bi-arrow-up-circle-fill"></i> Update available</a>`;
icon.className = 'bi bi-check-circle-fill text-success';
showNotification(`Update available: ${data.latest_date}+${data.latest_commit}`, 'success');
} else {
// Up to date
icon.className = 'bi bi-check-circle text-success';
showNotification('You are running the latest version', 'success');
// Reset icon after 3 seconds
setTimeout(() => {
icon.className = 'bi bi-arrow-repeat';
}, 3000);
}
} else {
// Error
icon.className = 'bi bi-exclamation-triangle text-warning';
showNotification(data.error || 'Failed to check for updates', 'warning');
setTimeout(() => {
icon.className = 'bi bi-arrow-repeat';
}, 3000);
}
} catch (error) {
console.error('Error checking for updates:', error);
icon.className = 'bi bi-exclamation-triangle text-danger';
showNotification('Network error checking for updates', 'danger');
setTimeout(() => {
icon.className = 'bi bi-arrow-repeat';
}, 3000);
} finally {
btn.disabled = false;
}
}
/**
* Scroll to bottom of messages
*/

View File

@@ -58,8 +58,13 @@
<h5 class="offcanvas-title"><i class="bi bi-menu-button-wide"></i> Menu</h5>
<button type="button" class="btn-close" data-bs-dismiss="offcanvas"></button>
</div>
<div class="px-3 pb-2 text-muted small border-bottom">
<i class="bi bi-tag"></i> {{ version }}
<div class="px-3 pb-2 text-muted small border-bottom d-flex align-items-center justify-content-between">
<span id="versionDisplay">
<i class="bi bi-tag"></i> <span id="versionText">{{ version }}</span>
</span>
<button id="checkUpdateBtn" class="btn btn-sm btn-outline-secondary py-0 px-1" title="Check for updates">
<i class="bi bi-arrow-repeat" id="checkUpdateIcon"></i>
</button>
</div>
<div class="offcanvas-body">
<div class="list-group list-group-flush">