diff --git a/app/routes/api.py b/app/routes/api.py index b0f991f..5b0e7f7 100644 --- a/app/routes/api.py +++ b/app/routes/api.py @@ -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(): """ diff --git a/app/static/css/style.css b/app/static/css/style.css index a2bdda2..4e0607c 100644 --- a/app/static/css/style.css +++ b/app/static/css/style.css @@ -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; diff --git a/app/static/js/app.js b/app/static/js/app.js index d61d5f2..ddab805 100644 --- a/app/static/js/app.js +++ b/app/static/js/app.js @@ -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} Update available`; + 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 */ diff --git a/app/templates/base.html b/app/templates/base.html index 52f37e3..2713c4c 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -58,8 +58,13 @@
Menu
-
- {{ version }} +
+ + {{ version }} + +