diff --git a/vpn/admin.py b/vpn/admin.py index 4932335..59ea2ba 100644 --- a/vpn/admin.py +++ b/vpn/admin.py @@ -247,10 +247,15 @@ class LastAccessFilter(admin.SimpleListFilter): class ServerAdmin(PolymorphicParentModelAdmin): base_model = Server child_models = (OutlineServer, WireguardServer) - list_display = ('name_with_icon', 'server_type', 'comment_short', 'user_stats', 'activity_summary', 'server_status_compact', 'registration_date') + list_display = ('name_with_icon', 'server_type', 'comment_short', 'user_stats', 'server_status_compact', 'registration_date') search_fields = ('name', 'comment') list_filter = ('server_type', ) actions = ['move_clients_action', 'purge_all_keys_action', 'sync_all_selected_servers'] + + class Media: + css = { + 'all': ('admin/css/vpn_server_admin.css',) + } def get_urls(self): urls = super().get_urls() @@ -619,32 +624,44 @@ class ServerAdmin(PolymorphicParentModelAdmin): @admin.display(description='Users & Links') def user_stats(self, obj): - """Display user count and active links statistics""" + """Display user count and active links statistics (optimized)""" try: from django.utils import timezone from datetime import timedelta user_count = obj.user_count if hasattr(obj, 'user_count') else 0 - # Get total links count - total_links = ACLLink.objects.filter(acl__server=obj).count() - - # Get recently accessed links (last 30 days) - exclude None values - thirty_days_ago = timezone.now() - timedelta(days=30) - active_links = ACLLink.objects.filter( - acl__server=obj, - last_access_time__isnull=False, - last_access_time__gte=thirty_days_ago - ).count() + # Use prefetched data if available + if hasattr(obj, 'acl_set'): + all_links = [] + for acl in obj.acl_set.all(): + if hasattr(acl, 'links') and hasattr(acl.links, 'all'): + all_links.extend(acl.links.all()) + + total_links = len(all_links) + + # Count active links from prefetched data + thirty_days_ago = timezone.now() - timedelta(days=30) + active_links = sum(1 for link in all_links + if link.last_access_time and link.last_access_time >= thirty_days_ago) + else: + # Fallback to direct queries (less efficient) + total_links = ACLLink.objects.filter(acl__server=obj).count() + thirty_days_ago = timezone.now() - timedelta(days=30) + active_links = ACLLink.objects.filter( + acl__server=obj, + last_access_time__isnull=False, + last_access_time__gte=thirty_days_ago + ).count() # Color coding based on activity if user_count == 0: color = '#9ca3af' # gray - no users elif total_links == 0: color = '#dc2626' # red - no links - elif active_links > total_links * 0.7: # High activity + elif total_links > 0 and active_links > total_links * 0.7: # High activity color = '#16a34a' # green - elif active_links > total_links * 0.3: # Medium activity + elif total_links > 0 and active_links > total_links * 0.3: # Medium activity color = '#eab308' # yellow else: color = '#f97316' # orange - low activity @@ -660,55 +677,14 @@ class ServerAdmin(PolymorphicParentModelAdmin): @admin.display(description='Activity') def activity_summary(self, obj): - """Display recent activity summary""" + """Display recent activity summary (optimized)""" try: - from django.utils import timezone - from datetime import timedelta - - # Get recent access logs for this server - seven_days_ago = timezone.now() - timedelta(days=7) - recent_logs = AccessLog.objects.filter( - server=obj.name, - timestamp__gte=seven_days_ago - ) - - total_access = recent_logs.count() - success_access = recent_logs.filter(action='Success').count() - - # Get latest access - latest_log = AccessLog.objects.filter(server=obj.name).order_by('-timestamp').first() - - if latest_log: - local_time = localtime(latest_log.timestamp) - latest_str = local_time.strftime('%m-%d %H:%M') - - # Time since last access - time_diff = timezone.now() - latest_log.timestamp - if time_diff.days > 0: - time_ago = f'{time_diff.days}d ago' - elif time_diff.seconds > 3600: - time_ago = f'{time_diff.seconds // 3600}h ago' - else: - time_ago = 'Recent' - else: - latest_str = 'Never' - time_ago = '' - - # Color coding - if total_access == 0: - color = '#dc2626' # red - no activity - elif total_access > 50: - color = '#16a34a' # green - high activity - elif total_access > 10: - color = '#eab308' # yellow - medium activity - else: - color = '#f97316' # orange - low activity - + # Simplified version - avoid heavy DB queries on list page + # This could be computed once per page load if needed return mark_safe( - f'
' + - f'
📊 {total_access} uses (7d)
' + - f'
✅ {success_access} success
' + - f'
🕒 {latest_str} {time_ago}
' + + f'
' + + f'
📊 Activity data
' + + f'
Click to view details
' + f'
' ) except Exception as e: @@ -716,42 +692,26 @@ class ServerAdmin(PolymorphicParentModelAdmin): @admin.display(description='Status') def server_status_compact(self, obj): - """Display server status in compact format""" + """Display server status in compact format (optimized)""" try: - status = obj.get_server_status() - if 'error' in status: - return mark_safe( - f'
' + - f'❌ Error
' + - f'' + - f'{status["error"][:30]}...' + - f'
' - ) - - # Extract key metrics - status_items = [] - if 'name' in status: - status_items.append(f"📛 {status['name'][:15]}") - if 'version' in status: - status_items.append(f"🔄 {status['version']}") - if 'keys' in status: - status_items.append(f"🔑 {status['keys']} keys") - if 'accessUrl' in status: - status_items.append("🌐 Available") - - status_display = '
'.join(status_items) if status_items else 'Unknown' + # Avoid expensive server connectivity checks on list page + # Show basic info and let users click to check status + server_type_icons = { + 'outline': 'đŸ”ĩ', + 'wireguard': 'đŸŸĸ', + } + icon = server_type_icons.get(obj.server_type, 'âšĒ') return mark_safe( - f'
' + - f'✅ Online
' + - f'{status_display}' + + f'
' + + f'{icon} {obj.server_type.title()}
' + + f'Click to check status' + f'
' ) except Exception as e: - # Don't let server connectivity issues break the admin interface return mark_safe( f'
' + - f'âš ī¸ Unavailable
' + + f'âš ī¸ Error
' + f'' + f'{str(e)[:25]}...' + f'
' @@ -760,6 +720,10 @@ class ServerAdmin(PolymorphicParentModelAdmin): def get_queryset(self, request): qs = super().get_queryset(request) qs = qs.annotate(user_count=Count('acl')) + qs = qs.prefetch_related( + 'acl_set__links', + 'acl_set__user' + ) return qs #admin.site.register(User, UserAdmin) @@ -768,7 +732,12 @@ class UserAdmin(admin.ModelAdmin): form = UserForm list_display = ('username', 'comment', 'registration_date', 'hash_link', 'server_count') search_fields = ('username', 'hash') - readonly_fields = ('hash_link', 'user_statistics_summary', 'recent_activity_display') + readonly_fields = ('hash_link', 'user_statistics_summary') + + class Media: + css = { + 'all': ('admin/css/vpn_server_admin.css',) + } fieldsets = ( ('User Information', { @@ -777,9 +746,9 @@ class UserAdmin(admin.ModelAdmin): ('Access Information', { 'fields': ('hash_link', 'is_active') }), - ('Statistics & Activity', { - 'fields': ('user_statistics_summary', 'recent_activity_display'), - 'classes': ('collapse',) + ('Statistics & Server Management', { + 'fields': ('user_statistics_summary',), + 'classes': ('wide',) }), ) @@ -845,23 +814,10 @@ class UserAdmin(admin.ModelAdmin): server = acl.server links = list(acl.links.all()) - # Server status check - try: - server_status = server.get_server_status() - server_accessible = True - server_error = None - except Exception as e: - server_status = {} - server_accessible = False - server_error = str(e) - - html += '
' - - # Server header - status_icon = '✅' if server_accessible else '❌' + # Server header (no slow server status checks) type_icon = 'đŸ”ĩ' if server.server_type == 'outline' else 'đŸŸĸ' if server.server_type == 'wireguard' else '' html += f'
' - html += f'
{type_icon} {server.name} {status_icon}
' + html += f'
{type_icon} {server.name}
' # Server stats server_stat = next((s for s in server_breakdown if s['server_name'] == server.name), None) @@ -871,11 +827,7 @@ class UserAdmin(admin.ModelAdmin): html += f'' html += f'
' - # Server error display - if server_error: - html += f'
' - html += f'âš ī¸ Error: {server_error[:80]}...' - html += f'
' + html += '
' # Links display if links: @@ -1136,7 +1088,7 @@ class UserAdmin(admin.ModelAdmin): return JsonResponse({'error': 'Invalid request method'}, status=405) def change_view(self, request, object_id, form_url='', extra_context=None): - """Override change view to add user management data to context""" + """Override change view to add user management data and fix layout""" extra_context = extra_context or {} if object_id: @@ -1235,13 +1187,39 @@ class ACLAdmin(admin.ModelAdmin): server = obj.server user = obj.user try: - data = server.get_user(user) - return format_object(data) + # Use cached statistics instead of direct server requests + from .models import UserStatistics + user_stats = UserStatistics.objects.filter( + user=user, + server_name=server.name + ).first() + + if user_stats: + # Format cached data nicely + data = { + 'user': user.username, + 'server': server.name, + 'total_connections': user_stats.total_connections, + 'recent_connections': user_stats.recent_connections, + 'max_daily': user_stats.max_daily, + 'last_updated': user_stats.updated_at.strftime('%Y-%m-%d %H:%M:%S'), + 'status': 'from_cache' + } + return format_object(data) + else: + # Fallback to minimal server check (avoid slow API calls on admin pages) + return mark_safe( + '
' + + 'â„šī¸ User Statistics:
' + + 'No cached statistics available.
' + + 'Run "Update user statistics cache" action to populate data.' + + '
' + ) except Exception as e: import logging logger = logging.getLogger(__name__) - logger.error(f"Failed to get user info for {user.username} on {server.name}: {e}") - return mark_safe(f"Server connection error: {e}") + logger.error(f"Failed to get cached user info for {user.username} on {server.name}: {e}") + return mark_safe(f"Cache error: {e}") @admin.display(description='User Links') def display_links(self, obj): diff --git a/vpn/server_plugins/outline.py b/vpn/server_plugins/outline.py index 3579dd4..e2c8ca5 100644 --- a/vpn/server_plugins/outline.py +++ b/vpn/server_plugins/outline.py @@ -580,6 +580,9 @@ class OutlineServerAdmin(PolymorphicChildModelAdmin): html += f'
Total Links: {total_links}
' html += f'
Server Keys: {server_keys_count}
' html += '
' + html += '
' + html += '📊 User activity data is from cached statistics for fast loading. Status indicators show usage patterns.' + html += '
' html += '
' # Get users data with ACL information @@ -599,17 +602,28 @@ class OutlineServerAdmin(PolymorphicChildModelAdmin): if last_access is None or link.last_access_time > last_access: last_access = link.last_access_time - # Check if user has key on server - server_key = False - try: - obj.get_user(user) - server_key = True - except Exception: - pass + # Use cached statistics instead of live server check for performance + user_stats = UserStatistics.objects.filter(user=user, server_name=obj.name) + server_key_status = "unknown" + total_user_connections = 0 + recent_user_connections = 0 + + if user_stats.exists(): + # User has cached data, likely has server access + total_user_connections = sum(stat.total_connections for stat in user_stats) + recent_user_connections = sum(stat.recent_connections for stat in user_stats) + + if total_user_connections > 0: + server_key_status = "cached_active" + else: + server_key_status = "cached_inactive" + else: + # No cached data - either new user or no access + server_key_status = "no_cache" html += '
' - # User info + # User info section html += '
' html += f'
{user.username}' if user.comment: @@ -622,19 +636,30 @@ class OutlineServerAdmin(PolymorphicChildModelAdmin): html += f' | Last access: {local_time.strftime("%Y-%m-%d %H:%M")}' else: html += ' | Never accessed' - html += '
' - html += '
' - # Status and actions + # Add usage statistics inside the same div + if total_user_connections > 0: + html += f' | {total_user_connections} total uses' + if recent_user_connections > 0: + html += f' ({recent_user_connections} recent)' + + html += '
' # End user info + html += '
' # End flex-1 div + + # Status and actions section html += '
' - if server_key: - html += '✅ On Server' + + # Status indicator based on cached data + if server_key_status == "cached_active": + html += '📊 Active User' + elif server_key_status == "cached_inactive": + html += '📊 Inactive' else: - html += '❌ Missing' + html += '❓ No Data' html += f'👤 Edit' - html += '
' - html += '
' + html += '' # End actions div + html += '' # End main user div else: html += '
' html += 'No users assigned to this server'