Improve server page

This commit is contained in:
Ultradesu
2025-07-21 18:26:29 +03:00
parent fa7ec5a87e
commit 4f7131ff5a
2 changed files with 137 additions and 134 deletions

View File

@@ -247,10 +247,15 @@ class LastAccessFilter(admin.SimpleListFilter):
class ServerAdmin(PolymorphicParentModelAdmin): class ServerAdmin(PolymorphicParentModelAdmin):
base_model = Server base_model = Server
child_models = (OutlineServer, WireguardServer) 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') search_fields = ('name', 'comment')
list_filter = ('server_type', ) list_filter = ('server_type', )
actions = ['move_clients_action', 'purge_all_keys_action', 'sync_all_selected_servers'] 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): def get_urls(self):
urls = super().get_urls() urls = super().get_urls()
@@ -619,32 +624,44 @@ class ServerAdmin(PolymorphicParentModelAdmin):
@admin.display(description='Users & Links') @admin.display(description='Users & Links')
def user_stats(self, obj): def user_stats(self, obj):
"""Display user count and active links statistics""" """Display user count and active links statistics (optimized)"""
try: try:
from django.utils import timezone from django.utils import timezone
from datetime import timedelta from datetime import timedelta
user_count = obj.user_count if hasattr(obj, 'user_count') else 0 user_count = obj.user_count if hasattr(obj, 'user_count') else 0
# Get total links count # Use prefetched data if available
total_links = ACLLink.objects.filter(acl__server=obj).count() if hasattr(obj, 'acl_set'):
all_links = []
# Get recently accessed links (last 30 days) - exclude None values for acl in obj.acl_set.all():
thirty_days_ago = timezone.now() - timedelta(days=30) if hasattr(acl, 'links') and hasattr(acl.links, 'all'):
active_links = ACLLink.objects.filter( all_links.extend(acl.links.all())
acl__server=obj,
last_access_time__isnull=False, total_links = len(all_links)
last_access_time__gte=thirty_days_ago
).count() # 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 # Color coding based on activity
if user_count == 0: if user_count == 0:
color = '#9ca3af' # gray - no users color = '#9ca3af' # gray - no users
elif total_links == 0: elif total_links == 0:
color = '#dc2626' # red - no links 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 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 color = '#eab308' # yellow
else: else:
color = '#f97316' # orange - low activity color = '#f97316' # orange - low activity
@@ -660,55 +677,14 @@ class ServerAdmin(PolymorphicParentModelAdmin):
@admin.display(description='Activity') @admin.display(description='Activity')
def activity_summary(self, obj): def activity_summary(self, obj):
"""Display recent activity summary""" """Display recent activity summary (optimized)"""
try: try:
from django.utils import timezone # Simplified version - avoid heavy DB queries on list page
from datetime import timedelta # This could be computed once per page load if needed
# 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
return mark_safe( return mark_safe(
f'<div style="font-size: 11px;">' + f'<div style="font-size: 11px; color: #6b7280;">' +
f'<div style="color: {color}; font-weight: bold;">📊 {total_access} uses (7d)</div>' + f'<div>📊 Activity data</div>' +
f'<div style="color: #6b7280;">✅ {success_access} success</div>' + f'<div><small>Click to view details</small></div>' +
f'<div style="color: #6b7280;">🕒 {latest_str} {time_ago}</div>' +
f'</div>' f'</div>'
) )
except Exception as e: except Exception as e:
@@ -716,42 +692,26 @@ class ServerAdmin(PolymorphicParentModelAdmin):
@admin.display(description='Status') @admin.display(description='Status')
def server_status_compact(self, obj): def server_status_compact(self, obj):
"""Display server status in compact format""" """Display server status in compact format (optimized)"""
try: try:
status = obj.get_server_status() # Avoid expensive server connectivity checks on list page
if 'error' in status: # Show basic info and let users click to check status
return mark_safe( server_type_icons = {
f'<div style="color: #dc2626; font-size: 11px; font-weight: bold;">' + 'outline': '🔵',
f'❌ Error<br>' + 'wireguard': '🟢',
f'<span style="font-weight: normal;" title="{status["error"]}">' + }
f'{status["error"][:30]}...</span>' + icon = server_type_icons.get(obj.server_type, '')
f'</div>'
)
# 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 = '<br>'.join(status_items) if status_items else 'Unknown'
return mark_safe( return mark_safe(
f'<div style="color: #16a34a; font-size: 11px;">' + f'<div style="color: #6b7280; font-size: 11px;">' +
f'✅ Online<br>' + f'{icon} {obj.server_type.title()}<br>' +
f'<span style="color: #6b7280;">{status_display}</span>' + f'<small>Click to check status</small>' +
f'</div>' f'</div>'
) )
except Exception as e: except Exception as e:
# Don't let server connectivity issues break the admin interface
return mark_safe( return mark_safe(
f'<div style="color: #f97316; font-size: 11px; font-weight: bold;">' + f'<div style="color: #f97316; font-size: 11px; font-weight: bold;">' +
f'⚠️ Unavailable<br>' + f'⚠️ Error<br>' +
f'<span style="font-weight: normal;" title="{str(e)}">' + f'<span style="font-weight: normal;" title="{str(e)}">' +
f'{str(e)[:25]}...</span>' + f'{str(e)[:25]}...</span>' +
f'</div>' f'</div>'
@@ -760,6 +720,10 @@ class ServerAdmin(PolymorphicParentModelAdmin):
def get_queryset(self, request): def get_queryset(self, request):
qs = super().get_queryset(request) qs = super().get_queryset(request)
qs = qs.annotate(user_count=Count('acl')) qs = qs.annotate(user_count=Count('acl'))
qs = qs.prefetch_related(
'acl_set__links',
'acl_set__user'
)
return qs return qs
#admin.site.register(User, UserAdmin) #admin.site.register(User, UserAdmin)
@@ -768,7 +732,12 @@ class UserAdmin(admin.ModelAdmin):
form = UserForm form = UserForm
list_display = ('username', 'comment', 'registration_date', 'hash_link', 'server_count') list_display = ('username', 'comment', 'registration_date', 'hash_link', 'server_count')
search_fields = ('username', 'hash') 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 = ( fieldsets = (
('User Information', { ('User Information', {
@@ -777,9 +746,9 @@ class UserAdmin(admin.ModelAdmin):
('Access Information', { ('Access Information', {
'fields': ('hash_link', 'is_active') 'fields': ('hash_link', 'is_active')
}), }),
('Statistics & Activity', { ('Statistics & Server Management', {
'fields': ('user_statistics_summary', 'recent_activity_display'), 'fields': ('user_statistics_summary',),
'classes': ('collapse',) 'classes': ('wide',)
}), }),
) )
@@ -845,23 +814,10 @@ class UserAdmin(admin.ModelAdmin):
server = acl.server server = acl.server
links = list(acl.links.all()) links = list(acl.links.all())
# Server status check # Server header (no slow server status checks)
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 += '<div class="server-section">'
# Server header
status_icon = '' if server_accessible else ''
type_icon = '🔵' if server.server_type == 'outline' else '🟢' if server.server_type == 'wireguard' else '' type_icon = '🔵' if server.server_type == 'outline' else '🟢' if server.server_type == 'wireguard' else ''
html += f'<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">' html += f'<div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 12px;">'
html += f'<h5 style="margin: 0; color: #333; font-size: 14px; font-weight: 600;">{type_icon} {server.name} {status_icon}</h5>' html += f'<h5 style="margin: 0; color: #333; font-size: 14px; font-weight: 600;">{type_icon} {server.name}</h5>'
# Server stats # Server stats
server_stat = next((s for s in server_breakdown if s['server_name'] == server.name), None) 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'</span>' html += f'</span>'
html += f'</div>' html += f'</div>'
# Server error display html += '<div class="server-section">'
if server_error:
html += f'<div style="background: #f8d7da; color: #721c24; padding: 8px; border-radius: 4px; margin-bottom: 8px; font-size: 12px;">'
html += f'⚠️ Error: {server_error[:80]}...'
html += f'</div>'
# Links display # Links display
if links: if links:
@@ -1136,7 +1088,7 @@ class UserAdmin(admin.ModelAdmin):
return JsonResponse({'error': 'Invalid request method'}, status=405) return JsonResponse({'error': 'Invalid request method'}, status=405)
def change_view(self, request, object_id, form_url='', extra_context=None): 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 {} extra_context = extra_context or {}
if object_id: if object_id:
@@ -1235,13 +1187,39 @@ class ACLAdmin(admin.ModelAdmin):
server = obj.server server = obj.server
user = obj.user user = obj.user
try: try:
data = server.get_user(user) # Use cached statistics instead of direct server requests
return format_object(data) 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(
'<div style="background: #fff3cd; border: 1px solid #ffeaa7; padding: 8px; border-radius: 4px;">' +
'<strong> User Statistics:</strong><br>' +
'No cached statistics available.<br>' +
'<small>Run "Update user statistics cache" action to populate data.</small>' +
'</div>'
)
except Exception as e: except Exception as e:
import logging import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
logger.error(f"Failed to get user info for {user.username} on {server.name}: {e}") logger.error(f"Failed to get cached user info for {user.username} on {server.name}: {e}")
return mark_safe(f"<span style='color: red;'>Server connection error: {e}</span>") return mark_safe(f"<span style='color: red;'>Cache error: {e}</span>")
@admin.display(description='User Links') @admin.display(description='User Links')
def display_links(self, obj): def display_links(self, obj):

View File

@@ -580,6 +580,9 @@ class OutlineServerAdmin(PolymorphicChildModelAdmin):
html += f'<div><strong>Total Links:</strong> {total_links}</div>' html += f'<div><strong>Total Links:</strong> {total_links}</div>'
html += f'<div><strong>Server Keys:</strong> {server_keys_count}</div>' html += f'<div><strong>Server Keys:</strong> {server_keys_count}</div>'
html += '</div>' html += '</div>'
html += '<div style="margin-top: 8px; font-size: 11px; color: #6c757d;">'
html += '📊 User activity data is from cached statistics for fast loading. Status indicators show usage patterns.'
html += '</div>'
html += '</div>' html += '</div>'
# Get users data with ACL information # 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: if last_access is None or link.last_access_time > last_access:
last_access = link.last_access_time last_access = link.last_access_time
# Check if user has key on server # Use cached statistics instead of live server check for performance
server_key = False user_stats = UserStatistics.objects.filter(user=user, server_name=obj.name)
try: server_key_status = "unknown"
obj.get_user(user) total_user_connections = 0
server_key = True recent_user_connections = 0
except Exception:
pass 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 += '<div style="background: #ffffff; border: 1px solid #e9ecef; border-radius: 0.25rem; padding: 0.75rem; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center;">' html += '<div style="background: #ffffff; border: 1px solid #e9ecef; border-radius: 0.25rem; padding: 0.75rem; margin-bottom: 0.5rem; display: flex; justify-content: space-between; align-items: center;">'
# User info # User info section
html += '<div style="flex: 1;">' html += '<div style="flex: 1;">'
html += f'<div style="font-weight: 500; font-size: 14px; color: #495057;">{user.username}' html += f'<div style="font-weight: 500; font-size: 14px; color: #495057;">{user.username}'
if user.comment: if user.comment:
@@ -622,19 +636,30 @@ class OutlineServerAdmin(PolymorphicChildModelAdmin):
html += f' | Last access: {local_time.strftime("%Y-%m-%d %H:%M")}' html += f' | Last access: {local_time.strftime("%Y-%m-%d %H:%M")}'
else: else:
html += ' | Never accessed' html += ' | Never accessed'
html += '</div>'
html += '</div>'
# 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 += '</div>' # End user info
html += '</div>' # End flex-1 div
# Status and actions section
html += '<div style="display: flex; gap: 8px; align-items: center;">' html += '<div style="display: flex; gap: 8px; align-items: center;">'
if server_key:
html += '<span style="background: #d4edda; color: #155724; padding: 2px 6px; border-radius: 3px; font-size: 10px;">✅ On Server</span>' # Status indicator based on cached data
if server_key_status == "cached_active":
html += '<span style="background: #d4edda; color: #155724; padding: 2px 6px; border-radius: 3px; font-size: 10px;">📊 Active User</span>'
elif server_key_status == "cached_inactive":
html += '<span style="background: #fff3cd; color: #856404; padding: 2px 6px; border-radius: 3px; font-size: 10px;">📊 Inactive</span>'
else: else:
html += '<span style="background: #f8d7da; color: #721c24; padding: 2px 6px; border-radius: 3px; font-size: 10px;">❌ Missing</span>' html += '<span style="background: #f8d7da; color: #721c24; padding: 2px 6px; border-radius: 3px; font-size: 10px;">❓ No Data</span>'
html += f'<a href="/admin/vpn/user/{user.id}/change/" class="btn btn-sm btn-outline-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 0.2rem; margin: 0 0.1rem;">👤 Edit</a>' html += f'<a href="/admin/vpn/user/{user.id}/change/" class="btn btn-sm btn-outline-primary" style="padding: 0.25rem 0.5rem; font-size: 0.75rem; border-radius: 0.2rem; margin: 0 0.1rem;">👤 Edit</a>'
html += '</div>' html += '</div>' # End actions div
html += '</div>' html += '</div>' # End main user div
else: else:
html += '<div style="color: #6c757d; font-style: italic; text-align: center; padding: 20px; background: #f9fafb; border-radius: 6px;">' html += '<div style="color: #6c757d; font-style: italic; text-align: center; padding: 20px; background: #f9fafb; border-radius: 6px;">'
html += 'No users assigned to this server' html += 'No users assigned to this server'