From 17f9f5c0450a1355ad4b92161a40244d95739852 Mon Sep 17 00:00:00 2001 From: Ultradesu Date: Mon, 21 Jul 2025 18:55:59 +0300 Subject: [PATCH] Improve server page --- static/admin/css/vpn_admin.css | 52 +++++++++++++ static/admin/js/server_status_check.js | 94 +++++++++++++++++++++++ vpn/admin.py | 100 +++++++++++++++++++++---- 3 files changed, 233 insertions(+), 13 deletions(-) create mode 100644 static/admin/js/server_status_check.js diff --git a/static/admin/css/vpn_admin.css b/static/admin/css/vpn_admin.css index 6522f05..ccc242d 100644 --- a/static/admin/css/vpn_admin.css +++ b/static/admin/css/vpn_admin.css @@ -281,3 +281,55 @@ table.changelist-results td:nth-child(6) { .help li { margin-bottom: 5px; } + +/* Make user statistics section wider */ +.field-user_statistics_summary { + width: 100% !important; +} + +.field-user_statistics_summary .readonly { + max-width: none !important; + width: 100% !important; +} + +.field-user_statistics_summary .user-management-section { + width: 100% !important; + max-width: none !important; +} + +/* Wider fieldset for statistics */ +.wide { + width: 100% !important; +} + +.wide .form-row { + width: 100% !important; +} + +/* Server status button styles */ +.check-status-btn { + transition: all 0.2s ease; + white-space: nowrap; +} + +.check-status-btn:hover { + opacity: 0.8; + transform: scale(1.05); +} + +.check-status-btn:disabled { + opacity: 0.6; + cursor: not-allowed; +} + +/* Make admin tables more responsive */ +.changelist-results table { + width: 100%; + table-layout: auto; +} + +/* Improve button spacing */ +.btn-sm-custom { + margin: 0 2px; + display: inline-block; +} diff --git a/static/admin/js/server_status_check.js b/static/admin/js/server_status_check.js new file mode 100644 index 0000000..18bce2e --- /dev/null +++ b/static/admin/js/server_status_check.js @@ -0,0 +1,94 @@ +// Server status check functionality for admin +document.addEventListener('DOMContentLoaded', function() { + // Add event listeners to all check status buttons + document.querySelectorAll('.check-status-btn').forEach(button => { + button.addEventListener('click', async function(e) { + e.preventDefault(); + + const serverId = this.dataset.serverId; + const serverName = this.dataset.serverName; + const serverType = this.dataset.serverType; + const originalText = this.textContent; + const originalColor = this.style.background; + + // Show loading state + this.textContent = '⏳ Checking...'; + this.style.background = '#6c757d'; + this.disabled = true; + + try { + // Try AJAX request first + const response = await fetch(`/admin/vpn/server/${serverId}/check-status/`, { + method: 'POST', + headers: { + 'Content-Type': 'application/x-www-form-urlencoded', + 'X-CSRFToken': getCookie('csrftoken') + } + }); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}: ${response.statusText}`); + } + + const data = await response.json(); + + if (data.success) { + // Update button based on status + if (data.status === 'online') { + this.textContent = '✅ Online'; + this.style.background = '#28a745'; + } else if (data.status === 'offline') { + this.textContent = '❌ Offline'; + this.style.background = '#dc3545'; + } else if (data.status === 'error') { + this.textContent = '⚠️ Error'; + this.style.background = '#fd7e14'; + } else { + this.textContent = '❓ Unknown'; + this.style.background = '#6c757d'; + } + + // Show additional info if available + if (data.message) { + this.title = data.message; + } + + } else { + throw new Error(data.error || 'Failed to check status'); + } + + } catch (error) { + console.error('Error checking server status:', error); + + // Fallback: show basic server info + this.textContent = `📊 ${serverType}`; + this.style.background = '#17a2b8'; + this.title = `Server: ${serverName} (${serverType}) - Status check failed: ${error.message}`; + } + + // Reset after 5 seconds in all cases + setTimeout(() => { + this.textContent = originalText; + this.style.background = originalColor; + this.title = ''; + this.disabled = false; + }, 5000); + }); + }); +}); + +// Helper function to get CSRF token +function getCookie(name) { + let cookieValue = null; + if (document.cookie && document.cookie !== '') { + const cookies = document.cookie.split(';'); + for (let i = 0; i < cookies.length; i++) { + const cookie = cookies[i].trim(); + if (cookie.substring(0, name.length + 1) === (name + '=')) { + cookieValue = decodeURIComponent(cookie.substring(name.length + 1)); + break; + } + } + } + return cookieValue; +} diff --git a/vpn/admin.py b/vpn/admin.py index 59ea2ba..45faf4c 100644 --- a/vpn/admin.py +++ b/vpn/admin.py @@ -254,13 +254,15 @@ class ServerAdmin(PolymorphicParentModelAdmin): class Media: css = { - 'all': ('admin/css/vpn_server_admin.css',) + 'all': ('admin/css/vpn_admin.css',) } + js = ('admin/js/server_status_check.js',) def get_urls(self): urls = super().get_urls() custom_urls = [ path('move-clients/', self.admin_site.admin_view(self.move_clients_view), name='server_move_clients'), + path('/check-status/', self.admin_site.admin_view(self.check_server_status_view), name='server_check_status'), ] return custom_urls + urls @@ -471,6 +473,77 @@ class ServerAdmin(PolymorphicParentModelAdmin): except Exception as e: messages.error(request, f"Database error during link transfer: {e}") return redirect('admin:vpn_server_changelist') + + def check_server_status_view(self, request, server_id): + """AJAX view to check server status""" + from django.http import JsonResponse + import logging + + logger = logging.getLogger(__name__) + + if request.method == 'POST': + try: + logger.info(f"Checking status for server ID: {server_id}") + server = Server.objects.get(pk=server_id) + real_server = server.get_real_instance() + logger.info(f"Server found: {server.name}, type: {type(real_server).__name__}") + + # Check server status based on type + from vpn.server_plugins.outline import OutlineServer + + if isinstance(real_server, OutlineServer): + try: + logger.info(f"Checking Outline server: {server.name}") + # Try to get server info to check if it's online + info = real_server.client.get_server_information() + if info: + logger.info(f"Server {server.name} is online with {info.get('accessKeyCount', info.get('access_key_count', 'unknown'))} keys") + return JsonResponse({ + 'success': True, + 'status': 'online', + 'message': f'Server is online. Keys: {info.get("accessKeyCount", info.get("access_key_count", "unknown"))}' + }) + else: + logger.warning(f"Server {server.name} returned no info") + return JsonResponse({ + 'success': True, + 'status': 'offline', + 'message': 'Server not responding' + }) + except Exception as e: + logger.error(f"Error checking Outline server {server.name}: {e}") + return JsonResponse({ + 'success': True, + 'status': 'error', + 'message': f'Connection error: {str(e)[:100]}' + }) + else: + # For non-Outline servers, just return basic info + logger.info(f"Non-Outline server {server.name}, type: {server.server_type}") + return JsonResponse({ + 'success': True, + 'status': 'unknown', + 'message': f'Status check not implemented for {server.server_type} servers' + }) + + except Server.DoesNotExist: + logger.error(f"Server with ID {server_id} not found") + return JsonResponse({ + 'success': False, + 'error': 'Server not found' + }, status=404) + except Exception as e: + logger.error(f"Unexpected error checking server {server_id}: {e}") + return JsonResponse({ + 'success': False, + 'error': f'Unexpected error: {str(e)}' + }, status=500) + + logger.warning(f"Invalid request method {request.method} for server status check") + return JsonResponse({ + 'success': False, + 'error': 'Invalid request method' + }, status=405) def purge_all_keys_action(self, request, queryset): """Purge all keys from selected servers without changing database""" @@ -705,7 +778,13 @@ class ServerAdmin(PolymorphicParentModelAdmin): return mark_safe( f'
' + f'{icon} {obj.server_type.title()}
' + - f'Click to check status' + + f'' + f'
' ) except Exception as e: @@ -736,7 +815,7 @@ class UserAdmin(admin.ModelAdmin): class Media: css = { - 'all': ('admin/css/vpn_server_admin.css',) + 'all': ('admin/css/vpn_admin.css',) } fieldsets = ( @@ -1223,20 +1302,15 @@ class ACLAdmin(admin.ModelAdmin): @admin.display(description='User Links') def display_links(self, obj): - links = obj.links.all() + links_count = obj.links.count() portal_url = f"{EXTERNAL_ADDRESS}/u/{obj.user.hash}" - links_html = [] - for link in links: - link_url = f"{EXTERNAL_ADDRESS}/ss/{link.link}#{obj.server.name}" - links_html.append(f"{link.comment} - {link_url}") - - links_text = '
'.join(links_html) if links_html else 'No links' - return format_html( - '
{}
' + + '
' + '🔗 {} link(s)' + '
' '🌐 User Portal', - links_text, portal_url + links_count, portal_url )