diff --git a/vpn/admin.py b/vpn/admin.py index 24fa403..2f0ff01 100644 --- a/vpn/admin.py +++ b/vpn/admin.py @@ -19,6 +19,15 @@ from vpn.models import User, ACL, ACLLink from vpn.forms import UserForm from mysite.settings import EXTERNAL_ADDRESS from django.db.models import Max, Subquery, OuterRef, Q + + +def format_bytes(bytes_val): + """Format bytes to human readable format""" + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + if bytes_val < 1024.0: + return f"{bytes_val:.1f}{unit}" + bytes_val /= 1024.0 + return f"{bytes_val:.1f}PB" from .server_plugins import ( Server, WireguardServer, @@ -802,48 +811,121 @@ class ServerAdmin(PolymorphicParentModelAdmin): user_count = obj.user_count if hasattr(obj, 'user_count') else 0 - # 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()) + # Different logic for Xray vs legacy servers + if obj.server_type == 'xray_v2': + # For Xray servers, count inbounds and active subscriptions + from vpn.models_xray import ServerInbound + total_inbounds = ServerInbound.objects.filter(server=obj, active=True).count() - total_links = len(all_links) - - # Count active links from prefetched data + # Count recent subscription accesses via AccessLog 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) + from vpn.models import AccessLog + active_accesses = AccessLog.objects.filter( + server='Xray-Subscription', + action='Success', + timestamp__gte=thirty_days_ago + ).values('user').distinct().count() + + total_links = total_inbounds + active_links = min(active_accesses, user_count) # Can't be more than total users 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() + # Legacy servers: use ACL links as before + 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 total_links > 0 and active_links > total_links * 0.7: # High activity - color = '#16a34a' # green - elif total_links > 0 and active_links > total_links * 0.3: # Medium activity - color = '#eab308' # yellow + color = '#dc2626' # red - no links/inbounds + elif obj.server_type == 'xray_v2': + # For Xray: base on user activity rather than link activity + if active_links > user_count * 0.5: # More than half users active + color = '#16a34a' # green + elif active_links > user_count * 0.2: # More than 20% users active + color = '#eab308' # yellow + else: + color = '#f97316' # orange - low activity else: - color = '#f97316' # orange - low activity + # Legacy servers: base on link activity + if total_links > 0 and active_links > total_links * 0.7: # High activity + color = '#16a34a' # green + elif total_links > 0 and active_links > total_links * 0.3: # Medium activity + color = '#eab308' # yellow + else: + color = '#f97316' # orange - low activity - return mark_safe( - f'
📊 Traffic Statistics:
' + html += f'↑ Upload: {format_bytes(traffic_total_up)}
' + html += f'↓ Download: {format_bytes(traffic_total_down)}
' + except Exception as e: + import logging + logger = logging.getLogger(__name__) + logger.debug(f"Could not get traffic stats for user {obj.username}: {e}") else: html += 'No Xray subscriptions
' html += '' diff --git a/vpn/server_plugins/xray_v2.py b/vpn/server_plugins/xray_v2.py index 2a84deb..8f579c0 100644 --- a/vpn/server_plugins/xray_v2.py +++ b/vpn/server_plugins/xray_v2.py @@ -782,7 +782,7 @@ class XrayServerV2Admin(admin.ModelAdmin): list_display = ['name', 'client_hostname', 'api_address', 'api_enabled', 'stats_enabled', 'registration_date'] list_filter = ['api_enabled', 'stats_enabled', 'registration_date'] search_fields = ['name', 'client_hostname', 'comment'] - readonly_fields = ['server_type', 'registration_date'] + readonly_fields = ['server_type', 'registration_date', 'traffic_statistics'] inlines = [ServerInboundInline] def has_module_permission(self, request): @@ -799,6 +799,10 @@ class XrayServerV2Admin(admin.ModelAdmin): ('API Settings', { 'fields': ('api_enabled', 'stats_enabled') }), + ('Traffic Statistics', { + 'fields': ('traffic_statistics',), + 'description': 'Real-time traffic statistics from Xray server' + }), ('Timestamps', { 'fields': ('registration_date',), 'classes': ('collapse',) @@ -825,4 +829,119 @@ class XrayServerV2Admin(admin.ModelAdmin): status = server.get_server_status() statuses.append(f"{server.name}: {status.get('accessible', 'Unknown')}") self.message_user(request, f"Server statuses: {', '.join(statuses)}") - get_status.short_description = "Check status of selected servers" \ No newline at end of file + get_status.short_description = "Check status of selected servers" + + def traffic_statistics(self, obj): + """Display traffic statistics for this server""" + from django.utils.safestring import mark_safe + from django.utils.html import format_html + + if not obj.pk: + return "Save server first to see statistics" + + if not obj.api_enabled or not obj.stats_enabled: + return "Statistics are disabled. Enable API and stats to see traffic data." + + try: + from vpn.xray_api_v2.client import XrayClient + from vpn.xray_api_v2.stats import StatsManager + + client = XrayClient(server=obj.api_address) + stats_manager = StatsManager(client) + + # Get traffic summary + traffic_summary = stats_manager.get_traffic_summary() + + # Format bytes + def format_bytes(bytes_val): + for unit in ['B', 'KB', 'MB', 'GB', 'TB']: + if bytes_val < 1024.0: + return f"{bytes_val:.1f}{unit}" + bytes_val /= 1024.0 + return f"{bytes_val:.1f}PB" + + html = 'User | ' + html += 'Upload | ' + html += 'Download | ' + html += 'Total | ' + html += '
---|---|---|---|
{email} | ' + html += f'↑ {format_bytes(up)} | ' + html += f'↓ {format_bytes(down)} | ' + html += f'{format_bytes(total)} | ' + html += '
... and {len(users) - 20} more users | |||
Total ({len(users)} users) | ' + html += f'↑ {format_bytes(total_up)} | ' + html += f'↓ {format_bytes(total_down)} | ' + html += f'{format_bytes(total_up + total_down)} | ' + html += '
No user traffic data available
' + + # Inbound statistics + inbounds = traffic_summary.get('inbounds', {}) + if inbounds: + html += 'Inbound | ' + html += 'Upload | ' + html += 'Download | ' + html += 'Total | ' + html += '
---|---|---|---|
{tag} | ' + html += f'↑ {format_bytes(up)} | ' + html += f'↓ {format_bytes(down)} | ' + html += f'{format_bytes(total)} | ' + html += '