feat: ✨ update available notification
This commit is contained in:
@@ -2,3 +2,4 @@ Flask
|
|||||||
mysql-connector-python
|
mysql-connector-python
|
||||||
dotenv
|
dotenv
|
||||||
gunicorn
|
gunicorn
|
||||||
|
requests
|
||||||
@@ -7,6 +7,7 @@ import csv
|
|||||||
from io import StringIO, BytesIO
|
from io import StringIO, BytesIO
|
||||||
import logging
|
import logging
|
||||||
import mysql.connector
|
import mysql.connector
|
||||||
|
import requests
|
||||||
|
|
||||||
app = None
|
app = None
|
||||||
|
|
||||||
@@ -1100,6 +1101,61 @@ def register_routes(app):
|
|||||||
download_name=filename
|
download_name=filename
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.route('/check_update')
|
||||||
|
@login_required
|
||||||
|
def check_update():
|
||||||
|
"""Check for available updates from GitHub"""
|
||||||
|
try:
|
||||||
|
# Get current version
|
||||||
|
version_file = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'VERSION')
|
||||||
|
current_version = 'unknown'
|
||||||
|
if os.path.exists(version_file):
|
||||||
|
with open(version_file, 'r') as f:
|
||||||
|
current_version = f.read().strip()
|
||||||
|
|
||||||
|
# Fetch latest release from GitHub
|
||||||
|
response = requests.get('https://api.github.com/repos/JDB-NET/ipam/releases/latest', timeout=5)
|
||||||
|
if response.status_code != 200:
|
||||||
|
return jsonify({'error': 'Failed to fetch release information'}), 500
|
||||||
|
|
||||||
|
release_data = response.json()
|
||||||
|
latest_version = release_data.get('tag_name', '').lstrip('v')
|
||||||
|
|
||||||
|
# Compare versions using semantic versioning
|
||||||
|
if latest_version and latest_version != current_version:
|
||||||
|
# Simple semantic version comparison
|
||||||
|
def version_tuple(v):
|
||||||
|
"""Convert version string to tuple for comparison"""
|
||||||
|
parts = v.split('.')
|
||||||
|
return tuple(int(x) if x.isdigit() else 0 for x in parts[:3])
|
||||||
|
|
||||||
|
try:
|
||||||
|
current_tuple = version_tuple(current_version)
|
||||||
|
latest_tuple = version_tuple(latest_version)
|
||||||
|
# Only show update if latest is actually newer
|
||||||
|
if latest_tuple <= current_tuple:
|
||||||
|
return jsonify({'update_available': False})
|
||||||
|
except (ValueError, AttributeError):
|
||||||
|
# Fallback to string comparison if parsing fails
|
||||||
|
if latest_version == current_version:
|
||||||
|
return jsonify({'update_available': False})
|
||||||
|
|
||||||
|
return jsonify({
|
||||||
|
'update_available': True,
|
||||||
|
'current_version': current_version,
|
||||||
|
'latest_version': latest_version,
|
||||||
|
'release_url': release_data.get('html_url', '')
|
||||||
|
})
|
||||||
|
else:
|
||||||
|
return jsonify({'update_available': False})
|
||||||
|
|
||||||
|
except requests.RequestException as e:
|
||||||
|
logging.error(f"Error checking for updates: {e}")
|
||||||
|
return jsonify({'error': 'Failed to check for updates'}), 500
|
||||||
|
except Exception as e:
|
||||||
|
logging.error(f"Unexpected error checking for updates: {e}")
|
||||||
|
return jsonify({'error': 'Failed to check for updates'}), 500
|
||||||
|
|
||||||
@app.route('/get_available_ips')
|
@app.route('/get_available_ips')
|
||||||
@permission_required('view_device')
|
@permission_required('view_device')
|
||||||
def get_available_ips():
|
def get_available_ips():
|
||||||
|
|||||||
@@ -67,4 +67,85 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/js/header.js"></script>
|
<script src="/static/js/header.js"></script>
|
||||||
|
|
||||||
|
<!-- Update Available Toast -->
|
||||||
|
<div id="update-toast" class="hidden fixed bottom-4 right-4 bg-gray-200 dark:bg-zinc-800 border border-gray-400 dark:border-zinc-600 rounded-lg shadow-lg max-w-md z-50">
|
||||||
|
<div class="p-4">
|
||||||
|
<div class="flex items-start justify-between mb-2">
|
||||||
|
<div class="flex items-center gap-2">
|
||||||
|
<i class="fas fa-bell text-gray-900 dark:text-gray-100"></i>
|
||||||
|
<h3 class="font-semibold text-lg text-gray-900 dark:text-gray-100">Update Available</h3>
|
||||||
|
</div>
|
||||||
|
<button id="toast-close" class="text-gray-600 dark:text-gray-400 hover:text-gray-800 dark:hover:text-gray-200 hover:cursor-pointer">
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<p class="text-sm text-gray-700 dark:text-gray-300 mb-3">
|
||||||
|
Version <span id="toast-latest-version" class="font-semibold"></span> is now available. You're currently on <span id="toast-current-version" class="font-semibold"></span>.
|
||||||
|
</p>
|
||||||
|
<div class="flex gap-2 mt-3">
|
||||||
|
<a id="toast-compare-link" href="#" target="_blank" rel="noopener noreferrer" class="flex-1 bg-gray-300 hover:bg-gray-400 dark:bg-zinc-700 dark:hover:bg-zinc-600 text-gray-900 dark:text-gray-100 px-3 py-2 rounded text-center text-sm hover:cursor-pointer">
|
||||||
|
View Changes
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
#update-toast {
|
||||||
|
animation: slideIn 0.3s ease-out;
|
||||||
|
}
|
||||||
|
@keyframes slideIn {
|
||||||
|
from {
|
||||||
|
transform: translateX(100%);
|
||||||
|
opacity: 0;
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
transform: translateX(0);
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
// Check if toast was dismissed in this session
|
||||||
|
const toastDismissed = sessionStorage.getItem('update-toast-dismissed');
|
||||||
|
if (toastDismissed) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check for updates
|
||||||
|
fetch('/check_update')
|
||||||
|
.then(response => response.json())
|
||||||
|
.then(data => {
|
||||||
|
if (data.update_available) {
|
||||||
|
const toast = document.getElementById('update-toast');
|
||||||
|
const currentVersionEl = document.getElementById('toast-current-version');
|
||||||
|
const latestVersionEl = document.getElementById('toast-latest-version');
|
||||||
|
const compareLink = document.getElementById('toast-compare-link');
|
||||||
|
const closeBtn = document.getElementById('toast-close');
|
||||||
|
|
||||||
|
// Set versions
|
||||||
|
currentVersionEl.textContent = 'v' + data.current_version;
|
||||||
|
latestVersionEl.textContent = 'v' + data.latest_version;
|
||||||
|
|
||||||
|
// Set compare link (current version to latest version)
|
||||||
|
compareLink.href = `https://github.com/JDB-NET/ipam/compare/v${data.current_version}...v${data.latest_version}`;
|
||||||
|
|
||||||
|
// Show toast
|
||||||
|
toast.classList.remove('hidden');
|
||||||
|
|
||||||
|
// Close button handler
|
||||||
|
closeBtn.addEventListener('click', function() {
|
||||||
|
toast.classList.add('hidden');
|
||||||
|
sessionStorage.setItem('update-toast-dismissed', 'true');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
console.error('Error checking for updates:', error);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
</header>
|
</header>
|
||||||
|
|||||||
Reference in New Issue
Block a user