216 lines
		
	
	
		
			No EOL
		
	
	
		
			9 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
			
		
		
	
	
			216 lines
		
	
	
		
			No EOL
		
	
	
		
			9 KiB
		
	
	
	
		
			HTML
		
	
	
	
	
	
| <!DOCTYPE html>
 | |
| <html lang="en">
 | |
| <head>
 | |
|     <meta charset="UTF-8">
 | |
|     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 | |
|     <title>Traffic Analytics - Admin</title>
 | |
|     <script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
 | |
|     <style>
 | |
|         body { font-family: Arial, sans-serif; margin: 0; padding: 20px; background: #f5f5f5; }
 | |
|         .container { max-width: 1200px; margin: 0 auto; }
 | |
|         .stats-grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 20px; margin-bottom: 30px; }
 | |
|         .stat-card { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); }
 | |
|         .stat-number { font-size: 2em; font-weight: bold; color: #2563eb; }
 | |
|         .stat-label { color: #666; margin-top: 5px; }
 | |
|         .chart-container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 30px; }
 | |
|         .table-container { background: white; padding: 20px; border-radius: 8px; box-shadow: 0 2px 4px rgba(0,0,0,0.1); margin-bottom: 20px; }
 | |
|         table { width: 100%; border-collapse: collapse; }
 | |
|         th, td { padding: 12px; text-align: left; border-bottom: 1px solid #eee; }
 | |
|         th { background: #f8f9fa; font-weight: 600; }
 | |
|         h1 { color: #333; margin-bottom: 30px; }
 | |
|         h2 { color: #333; margin-bottom: 15px; }
 | |
|         .controls { margin-bottom: 20px; }
 | |
|         .controls select { padding: 8px 12px; border: 1px solid #ddd; border-radius: 4px; }
 | |
|         .real-time-btn { background: #10b981; color: white; padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
 | |
|         .real-time-btn:hover { background: #059669; }
 | |
|         .activity-item { padding: 8px; margin: 4px 0; background: #f8f9fa; border-radius: 4px; font-size: 0.9em; }
 | |
|         .activity-time { color: #666; font-size: 0.8em; }
 | |
|         .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
 | |
|         @media (max-width: 768px) { .grid-2 { grid-template-columns: 1fr; } }
 | |
|     </style>
 | |
| </head>
 | |
| <body>
 | |
|     <div class="container">
 | |
|         <div style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 30px;">
 | |
|             <h1>Traffic Analytics Dashboard</h1>
 | |
|             <div class="controls">
 | |
|                 <select id="dateRange">
 | |
|                     <option value="7" {% if days == 7 %}selected{% endif %}>Last 7 days</option>
 | |
|                     <option value="30" {% if days == 30 %}selected{% endif %}>Last 30 days</option>
 | |
|                     <option value="90" {% if days == 90 %}selected{% endif %}>Last 90 days</option>
 | |
|                 </select>
 | |
|                 <button id="realTimeBtn" class="real-time-btn">
 | |
|                     Real-time: <span id="activeUsers">0</span> active
 | |
|                 </button>
 | |
|             </div>
 | |
|         </div>
 | |
| 
 | |
|         <!-- Stats Cards -->
 | |
|         <div class="stats-grid">
 | |
|             <div class="stat-card">
 | |
|                 <div class="stat-number">{{ "{:,}".format(stats.total_views) }}</div>
 | |
|                 <div class="stat-label">Total Page Views</div>
 | |
|             </div>
 | |
|             <div class="stat-card">
 | |
|                 <div class="stat-number">{{ "{:,}".format(stats.unique_visitors) }}</div>
 | |
|                 <div class="stat-label">Unique Visitors</div>
 | |
|             </div>
 | |
|             <div class="stat-card">
 | |
|                 <div class="stat-number">{{ stats.avg_response_time }}ms</div>
 | |
|                 <div class="stat-label">Avg Response Time</div>
 | |
|             </div>
 | |
|             <div class="stat-card">
 | |
|                 <div class="stat-number">{{ stats.bounce_rate }}%</div>
 | |
|                 <div class="stat-label">Bounce Rate</div>
 | |
|             </div>
 | |
|         </div>
 | |
| 
 | |
|         <!-- Daily Traffic Chart -->
 | |
|         <div class="chart-container">
 | |
|             <h2>Daily Traffic (Last {{ days }} Days)</h2>
 | |
|             <canvas id="dailyTrafficChart" width="400" height="100"></canvas>
 | |
|         </div>
 | |
| 
 | |
|         <!-- Tables -->
 | |
|         <div class="grid-2">
 | |
|             <div class="table-container">
 | |
|                 <h2>Top Pages</h2>
 | |
|                 <table>
 | |
|                     <thead>
 | |
|                         <tr>
 | |
|                             <th>Page</th>
 | |
|                             <th>Views</th>
 | |
|                         </tr>
 | |
|                     </thead>
 | |
|                     <tbody>
 | |
|                         {% for page, views in top_pages %}
 | |
|                         <tr>
 | |
|                             <td>{{ page }}</td>
 | |
|                             <td>{{ views }}</td>
 | |
|                         </tr>
 | |
|                         {% endfor %}
 | |
|                     </tbody>
 | |
|                 </table>
 | |
|             </div>
 | |
| 
 | |
|             <div class="table-container">
 | |
|                 <h2>Top Referrers</h2>
 | |
|                 <table>
 | |
|                     <thead>
 | |
|                         <tr>
 | |
|                             <th>Referrer</th>
 | |
|                             <th>Views</th>
 | |
|                         </tr>
 | |
|                     </thead>
 | |
|                     <tbody>
 | |
|                         {% for referrer, views in top_referrers %}
 | |
|                         <tr>
 | |
|                             <td>{{ referrer[:50] }}{% if referrer|length > 50 %}...{% endif %}</td>
 | |
|                             <td>{{ views }}</td>
 | |
|                         </tr>
 | |
|                         {% endfor %}
 | |
|                     </tbody>
 | |
|                 </table>
 | |
|             </div>
 | |
|         </div>
 | |
| 
 | |
|         <!-- Recent Activity -->
 | |
|         <div class="table-container">
 | |
|             <h2>Recent Activity (Last 24 Hours)</h2>
 | |
|             <div id="recentActivity" style="max-height: 300px; overflow-y: auto;">
 | |
|                 {% for activity in recent_activity %}
 | |
|                 <div class="activity-item">
 | |
|                     <strong>{{ activity.path }}</strong>
 | |
|                     {% if activity.referrer %}
 | |
|                         from {{ activity.referrer[:30] }}{% if activity.referrer|length > 30 %}...{% endif %}
 | |
|                     {% endif %}
 | |
|                     <div class="activity-time">{{ activity.timestamp.strftime('%Y-%m-%d %H:%M:%S') }} - {{ activity.ip_address[:8] }}...</div>
 | |
|                 </div>
 | |
|                 {% endfor %}
 | |
|             </div>
 | |
|         </div>
 | |
|     </div>
 | |
| 
 | |
|     <script>
 | |
|         // Chart.js configurations - Fixed tojsonfilter to tojson
 | |
|         const dailyData = {{ daily_views | tojson | safe }};
 | |
| 
 | |
|         // Daily Traffic Chart
 | |
|         const ctx = document.getElementById('dailyTrafficChart').getContext('2d');
 | |
| 
 | |
|         new Chart(ctx, {
 | |
|             type: 'line',
 | |
|             data: {
 | |
|                 labels: dailyData.map(d => d.date),
 | |
|                 datasets: [{
 | |
|                     label: 'Page Views',
 | |
|                     data: dailyData.map(d => d.views),
 | |
|                     borderColor: 'rgb(37, 99, 235)',
 | |
|                     backgroundColor: 'rgba(37, 99, 235, 0.1)',
 | |
|                     tension: 0.1,
 | |
|                     fill: true
 | |
|                 }, {
 | |
|                     label: 'Unique Visitors',
 | |
|                     data: dailyData.map(d => d.unique_visitors),
 | |
|                     borderColor: 'rgb(16, 185, 129)',
 | |
|                     backgroundColor: 'rgba(16, 185, 129, 0.1)',
 | |
|                     tension: 0.1,
 | |
|                     fill: false
 | |
|                 }]
 | |
|             },
 | |
|             options: {
 | |
|                 responsive: true,
 | |
|                 scales: {
 | |
|                     y: {
 | |
|                         beginAtZero: true
 | |
|                     }
 | |
|                 },
 | |
|                 plugins: {
 | |
|                     legend: {
 | |
|                         display: true
 | |
|                     }
 | |
|                 }
 | |
|             }
 | |
|         });
 | |
| 
 | |
|         // Real-time updates
 | |
|         function updateRealTime() {
 | |
|             fetch('/admin/traffic/api/realtime')
 | |
|                 .then(response => response.json())
 | |
|                 .then(data => {
 | |
|                     document.getElementById('activeUsers').textContent = data.active_users;
 | |
| 
 | |
|                     const activityDiv = document.getElementById('recentActivity');
 | |
|                     if (data.recent_views && data.recent_views.length > 0) {
 | |
|                         activityDiv.innerHTML = data.recent_views.map(view => `
 | |
|                             <div class="activity-item">
 | |
|                                 <strong>${view.path}</strong>
 | |
|                                 <div class="activity-time">
 | |
|                                     ${new Date(view.timestamp).toLocaleString()} - ${view.ip_address}
 | |
|                                     ${view.country ? ' (' + view.country + ')' : ''}
 | |
|                                 </div>
 | |
|                             </div>
 | |
|                         `).join('');
 | |
|                     }
 | |
|                 })
 | |
|                 .catch(error => {
 | |
|                     console.log('Real-time update failed:', error);
 | |
|                 });
 | |
|         }
 | |
| 
 | |
|         // Update every 30 seconds
 | |
|         setInterval(updateRealTime, 30000);
 | |
|         updateRealTime(); // Initial load
 | |
| 
 | |
|         // Date range selector
 | |
|         document.getElementById('dateRange').addEventListener('change', function() {
 | |
|             window.location.href = `?days=${this.value}`;
 | |
|         });
 | |
| 
 | |
|         // Real-time button click
 | |
|         document.getElementById('realTimeBtn').addEventListener('click', function() {
 | |
|             updateRealTime();
 | |
|         });
 | |
|     </script>
 | |
| </body>
 | |
| </html> | 
