diff --git a/vpn/templates/vpn/user_portal.html b/vpn/templates/vpn/user_portal.html index 9cdc8ba..895886c 100644 --- a/vpn/templates/vpn/user_portal.html +++ b/vpn/templates/vpn/user_portal.html @@ -211,11 +211,17 @@ } .link-header { + display: flex; + justify-content: space-between; + align-items: flex-start; margin-bottom: 15px; + flex-wrap: wrap; + gap: 15px; } .link-info { flex: 1; + min-width: 200px; } .link-comment { @@ -246,6 +252,70 @@ transform: scale(1.05); } + .recent-count { + color: #9ca3af; + font-size: 0.8rem; + background: rgba(168, 85, 247, 0.1); + padding: 3px 8px; + border-radius: 10px; + border: 1px solid rgba(168, 85, 247, 0.2); + transition: all 0.3s ease; + } + + .recent-count:hover { + background: rgba(168, 85, 247, 0.2); + border-color: rgba(168, 85, 247, 0.4); + transform: scale(1.05); + } + + .usage-chart { + display: flex; + flex-direction: column; + align-items: center; + background: rgba(0, 0, 0, 0.2); + padding: 12px; + border-radius: 12px; + border: 1px solid rgba(255, 255, 255, 0.1); + min-width: 120px; + } + + .chart-title { + color: #9ca3af; + font-size: 0.7rem; + margin-bottom: 8px; + text-align: center; + font-weight: 500; + } + + .chart-bars { + display: flex; + align-items: end; + gap: 2px; + height: 30px; + width: 70px; + } + + .chart-bar { + background: linear-gradient(to top, #4ade80, #22c55e); + width: 8px; + border-radius: 2px 2px 0 0; + transition: all 0.3s ease; + min-height: 2px; + opacity: 0.7; + transform-origin: bottom; + } + + .chart-bar:hover { + opacity: 1; + transform: scaleY(1.1); + background: linear-gradient(to top, #22c55e, #16a34a); + } + + .chart-bar.zero { + background: rgba(107, 114, 128, 0.3); + height: 2px !important; + } + .link-url { background: rgba(0, 0, 0, 0.5); padding: 12px; @@ -328,6 +398,20 @@ .link-stats { gap: 8px; } + + .usage-chart { + min-width: 100px; + padding: 8px; + } + + .chart-bars { + width: 60px; + height: 25px; + } + + .chart-bar { + width: 6px; + } } .no-servers { @@ -407,6 +491,15 @@ + +
+
7-day activity
+
+ {% for day_usage in link_data.daily_usage %} +
+ {% endfor %}
@@ -499,6 +592,10 @@ 50% { transform: scale(1.1); } 100% { transform: scale(1); } } + @keyframes barGrow { + from { transform: scaleY(0); } + to { transform: scaleY(1); } + } `; document.head.appendChild(style); @@ -507,6 +604,9 @@ // Add some interactivity on load document.addEventListener('DOMContentLoaded', function() { + // Initialize chart bars + initializeCharts(); + // Animate cards on load const cards = document.querySelectorAll('.server-card'); cards.forEach((card, index) => { @@ -542,14 +642,51 @@ // Add pulse animation to connection counts setTimeout(() => { - const connectionCounts = document.querySelectorAll('.connection-count, .usage-count'); + const connectionCounts = document.querySelectorAll('.connection-count, .usage-count, .recent-count'); connectionCounts.forEach((count, index) => { setTimeout(() => { count.style.animation = 'pulse 0.6s ease-in-out'; }, index * 100); }); }, 1000); + + // Animate chart bars + setTimeout(() => { + const chartBars = document.querySelectorAll('.chart-bar'); + chartBars.forEach((bar, index) => { + setTimeout(() => { + bar.style.animation = 'barGrow 0.8s ease-out'; + }, index * 50); + }); + }, 1500); }); + + function initializeCharts() { + const charts = document.querySelectorAll('.usage-chart'); + + charts.forEach(chart => { + const maxValue = parseInt(chart.dataset.max) || 1; + const bars = chart.querySelectorAll('.chart-bar'); + + bars.forEach(bar => { + const height = parseInt(bar.dataset.height) || 0; + const maxHeight = parseInt(bar.dataset.max) || 1; + + if (height === 0) { + bar.classList.add('zero'); + bar.style.height = '2px'; + } else { + // Calculate height as percentage of container (30px max) + const percentage = Math.max(10, (height / Math.max(maxHeight, 1)) * 100); + const pixelHeight = Math.max(3, (percentage / 100) * 28); // 28px max for padding + bar.style.height = pixelHeight + 'px'; + + // Add tooltip + bar.title = `${height} connections`; + } + }); + }); + } \ No newline at end of file diff --git a/vpn/views.py b/vpn/views.py index 13ceeeb..f2bad3e 100644 --- a/vpn/views.py +++ b/vpn/views.py @@ -23,11 +23,17 @@ def userPortal(request, user_hash): # Get connection statistics for all user's links # Count successful connections for each specific link connection_stats = {} + recent_connection_stats = {} + usage_frequency = {} total_connections = 0 + # Get date ranges for analysis + from datetime import datetime, timedelta + now = datetime.now() + recent_date = now - timedelta(days=30) + for acl_link in acl_links: # Count successful connections for this specific link by checking if the link appears in the access log data - # This is more accurate as it counts actual uses of the specific link link_connections = AccessLog.objects.filter( user=user.username, server=acl_link.acl.server.name, @@ -37,15 +43,55 @@ def userPortal(request, user_hash): params=[f'%{acl_link.link}%'] ).count() + # Get recent connections (last 30 days) + recent_connections = AccessLog.objects.filter( + user=user.username, + server=acl_link.acl.server.name, + action='Success', + timestamp__gte=recent_date + ).extra( + where=["data LIKE %s"], + params=[f'%{acl_link.link}%'] + ).count() + + # Calculate usage frequency (connections per week over last 30 days) + # Get daily usage for the last 7 days for mini chart + daily_usage = [] + for i in range(7): + day_start = now - timedelta(days=i+1) + day_end = now - timedelta(days=i) + + day_connections = AccessLog.objects.filter( + user=user.username, + server=acl_link.acl.server.name, + action='Success', + timestamp__gte=day_start, + timestamp__lt=day_end + ).extra( + where=["data LIKE %s"], + params=[f'%{acl_link.link}%'] + ).count() + + daily_usage.append(day_connections) + + # Reverse to show oldest to newest + daily_usage.reverse() + # If no specific link matches found, fall back to general server connection count for this user if link_connections == 0: - # This gives a rough estimate based on server connections divided by number of links for this server server_connections = AccessLog.objects.filter( user=user.username, server=acl_link.acl.server.name, action='Success' ).count() + server_recent_connections = AccessLog.objects.filter( + user=user.username, + server=acl_link.acl.server.name, + action='Success', + timestamp__gte=recent_date + ).count() + # Get number of links for this server for this user user_links_on_server = ACLLink.objects.filter( acl__user=user, @@ -55,8 +101,17 @@ def userPortal(request, user_hash): # Distribute connections evenly among links if we can't track specific usage if user_links_on_server > 0: link_connections = server_connections // user_links_on_server + recent_connections = server_recent_connections // user_links_on_server + # Distribute daily usage as well + if sum(daily_usage) == 0: # If no activity, create empty chart + daily_usage = [0] * 7 + else: + avg_daily = max(1, sum(daily_usage) // (user_links_on_server * 7)) + daily_usage = [avg_daily if sum(daily_usage) > 0 else 0 for _ in range(7)] connection_stats[acl_link.link] = link_connections + recent_connection_stats[acl_link.link] = recent_connections + usage_frequency[acl_link.link] = daily_usage total_connections += link_connections # Group links by server @@ -92,24 +147,23 @@ def userPortal(request, user_hash): # Add link information with connection stats link_url = f"{EXTERNAL_ADDRESS}/ss/{link.link}#{server_name}" connection_count = connection_stats.get(link.link, 0) + recent_count = recent_connection_stats.get(link.link, 0) + daily_usage = usage_frequency.get(link.link, [0] * 7) servers_data[server_name]['links'].append({ 'link': link, 'url': link_url, 'comment': link.comment or 'Default', 'connections': connection_count, + 'recent_connections': recent_count, + 'daily_usage': daily_usage, + 'max_daily': max(daily_usage) if daily_usage else 0, }) servers_data[server_name]['total_connections'] += connection_count total_links += 1 - # Get recent connection activity (last 30 days) - from datetime import datetime, timedelta - recent_date = datetime.now() - timedelta(days=30) - recent_connections = AccessLog.objects.filter( - user=user.username, - action='Success', - timestamp__gte=recent_date - ).count() + # Calculate total recent connections from all links + total_recent_connections = sum(recent_connection_stats.values()) context = { 'user': user, @@ -117,7 +171,7 @@ def userPortal(request, user_hash): 'total_servers': len(servers_data), 'total_links': total_links, 'total_connections': total_connections, - 'recent_connections': recent_connections, + 'recent_connections': total_recent_connections, 'external_address': EXTERNAL_ADDRESS, }