mirror of
https://github.com/MarekWo/mc-webui.git
synced 2026-03-28 17:42:45 +01:00
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:
@@ -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():
|
||||
"""
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
*/
|
||||
|
||||
@@ -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">
|
||||
|
||||
Reference in New Issue
Block a user