Spaces:
Runtime error
Runtime error
| {% extends "base.html" %} | |
| {% block title %}Dashboard - Outline VPN{% endblock %} | |
| {% block extra_css %} | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.css"> | |
| <link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.11.3/css/dataTables.bootstrap5.min.css"> | |
| <link rel="stylesheet" href="{{ url_for('static', filename='css/dashboard.css') }}"> | |
| {% endblock %} | |
| {% block content %} | |
| <div class="dashboard-container"> | |
| <!-- Sidebar --> | |
| <aside class="sidebar"> | |
| <div class="logo-container"> | |
| <img src="{{ url_for('static', filename='img/logo.png') }}" alt="Outline VPN" class="logo"> | |
| <span class="logo-text">Outline VPN</span> | |
| </div> | |
| <nav class="sidebar-nav"> | |
| <ul class="nav flex-column"> | |
| <li class="nav-item"> | |
| <a href="#" class="nav-link active"> | |
| <i class="bi bi-speedometer2"></i> | |
| <span>Dashboard</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="#" class="nav-link"> | |
| <i class="bi bi-person"></i> | |
| <span>Profile</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="#" class="nav-link"> | |
| <i class="bi bi-gear"></i> | |
| <span>Settings</span> | |
| </a> | |
| </li> | |
| <li class="nav-item"> | |
| <a href="{{ url_for('logout') }}" class="nav-link text-danger"> | |
| <i class="bi bi-box-arrow-right"></i> | |
| <span>Logout</span> | |
| </a> | |
| </li> | |
| </ul> | |
| </nav> | |
| </aside> | |
| <!-- Main Content --> | |
| <main class="main-content"> | |
| <div class="d-flex justify-content-between align-items-center mb-4"> | |
| <h1 class="h3">Welcome back, {{ current_user.username }}</h1> | |
| <button id="sidebar-toggle" class="btn btn-light"> | |
| <i class="bi bi-list"></i> | |
| </button> | |
| </div> | |
| <!-- Stats Grid --> | |
| <div class="stats-grid"> | |
| <div class="stat-card" data-aos="fade-up"> | |
| <h3 class="stat-title">Connection Status</h3> | |
| <div class="stat-value"> | |
| <span class="status-indicator status-active"></span> | |
| Connected | |
| </div> | |
| <div class="stat-change"> | |
| <i class="bi bi-arrow-up"></i> | |
| Uptime: 12h 30m | |
| </div> | |
| </div> | |
| <div class="stat-card" data-aos="fade-up" data-aos-delay="100"> | |
| <h3 class="stat-title">Data Usage</h3> | |
| <div class="stat-value" id="data-usage"> | |
| 450.5 GB | |
| </div> | |
| <div class="stat-change"> | |
| <i class="bi bi-arrow-up-right"></i> | |
| +2.3% from last week | |
| </div> | |
| </div> | |
| <div class="stat-card" data-aos="fade-up" data-aos-delay="200"> | |
| <h3 class="stat-title">Active Sessions</h3> | |
| <div class="stat-value" id="active-sessions-count"> | |
| 3 | |
| </div> | |
| <div class="stat-change"> | |
| <i class="bi bi-people"></i> | |
| Across all devices | |
| </div> | |
| </div> | |
| <div class="stat-card" data-aos="fade-up" data-aos-delay="300"> | |
| <h3 class="stat-title">Network Speed</h3> | |
| <div class="stat-value" id="network-speed"> | |
| 125 Mbps | |
| </div> | |
| <div class="stat-change"> | |
| <i class="bi bi-speedometer2"></i> | |
| Average speed | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Traffic Chart --> | |
| <div class="chart-container" data-aos="fade-up"> | |
| <h2 class="h5 mb-4">Traffic Overview</h2> | |
| <canvas id="traffic-chart"></canvas> | |
| </div> | |
| <!-- Active Sessions Table --> | |
| <div class="dashboard-card" data-aos="fade-up"> | |
| <div class="d-flex justify-content-between align-items-center mb-4"> | |
| <h2 class="h5 mb-0">Active Sessions</h2> | |
| <button class="btn btn-outline-primary btn-sm"> | |
| <i class="bi bi-download me-2"></i>Export | |
| </button> | |
| </div> | |
| <div class="table-responsive"> | |
| <table class="data-table table"> | |
| <thead> | |
| <tr> | |
| <th>Device</th> | |
| <th>IP Address</th> | |
| <th>Location</th> | |
| <th>Connected Since</th> | |
| <th>Status</th> | |
| <th>Actions</th> | |
| </tr> | |
| </thead> | |
| <tbody> | |
| {% for session in active_sessions %} | |
| <tr class="fade-in"> | |
| <td> | |
| <div class="d-flex align-items-center"> | |
| <i class="bi {{ session.device_icon }} me-2"></i> | |
| {{ session.device_name }} | |
| </div> | |
| </td> | |
| <td>{{ session.ip_address }}</td> | |
| <td>{{ session.location }}</td> | |
| <td>{{ session.connected_since }}</td> | |
| <td> | |
| <span class="status-indicator status-{{ session.status }}"></span> | |
| {{ session.status|title }} | |
| </td> | |
| <td> | |
| <button class="btn btn-sm btn-outline-danger"> | |
| <i class="bi bi-x-circle"></i> | |
| Disconnect | |
| </button> | |
| </td> | |
| </tr> | |
| {% endfor %} | |
| </tbody> | |
| </table> | |
| </div> | |
| </div> | |
| <!-- Quick Actions --> | |
| <div class="row mt-4"> | |
| <div class="col-md-6"> | |
| <div class="dashboard-card" data-aos="fade-up"> | |
| <h2 class="h5 mb-4">Quick Actions</h2> | |
| <div class="d-grid gap-3"> | |
| <button class="btn btn-primary"> | |
| <i class="bi bi-cloud-download me-2"></i> | |
| Download Configuration | |
| </button> | |
| <button class="btn btn-outline-primary"> | |
| <i class="bi bi-gear me-2"></i> | |
| Server Settings | |
| </button> | |
| <button class="btn btn-outline-secondary"> | |
| <i class="bi bi-shield-check me-2"></i> | |
| Security Scan | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <div class="dashboard-card" data-aos="fade-up"> | |
| <h2 class="h5 mb-4">System Status</h2> | |
| <div class="system-stats"> | |
| <div class="mb-3"> | |
| <label class="form-label d-flex justify-content-between"> | |
| CPU Usage | |
| <span class="text-primary">45%</span> | |
| </label> | |
| <div class="progress"> | |
| <div class="progress-bar" role="progressbar" style="width: 45%"></div> | |
| </div> | |
| </div> | |
| <div class="mb-3"> | |
| <label class="form-label d-flex justify-content-between"> | |
| Memory Usage | |
| <span class="text-primary">60%</span> | |
| </label> | |
| <div class="progress"> | |
| <div class="progress-bar" role="progressbar" style="width: 60%"></div> | |
| </div> | |
| </div> | |
| <div> | |
| <label class="form-label d-flex justify-content-between"> | |
| Disk Usage | |
| <span class="text-primary">25%</span> | |
| </label> | |
| <div class="progress"> | |
| <div class="progress-bar" role="progressbar" style="width: 25%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </main> | |
| </div> | |
| {% endblock %} | |
| {% block extra_js %} | |
| <script src="https://cdn.jsdelivr.net/npm/chart.js@3.7.0/dist/chart.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/datatables.net@1.11.3/js/jquery.dataTables.min.js"></script> | |
| <script src="https://cdn.jsdelivr.net/npm/datatables.net-bs5@1.11.3/js/dataTables.bootstrap5.min.js"></script> | |
| <script src="{{ url_for('static', filename='js/dashboard.js') }}"></script> | |
| {% endblock %} | |
| <div class="col"> | |
| <div class="card h-100"> | |
| <div class="card-body"> | |
| <h6 class="card-title text-muted">Data Upload</h6> | |
| <h3 class="card-text mb-0" id="data-sent">0 B</h3> | |
| <div class="progress mt-2" style="height: 4px;"> | |
| <div id="upload-progress" class="progress-bar bg-success" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col"> | |
| <div class="card h-100"> | |
| <div class="card-body"> | |
| <h6 class="card-title text-muted">Data Download</h6> | |
| <h3 class="card-text mb-0" id="data-received">0 B</h3> | |
| <div class="progress mt-2" style="height: 4px;"> | |
| <div id="download-progress" class="progress-bar bg-info" style="width: 0%"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col"> | |
| <div class="card h-100"> | |
| <div class="card-body"> | |
| <h6 class="card-title text-muted">Connected Since</h6> | |
| <h3 class="card-text" id="connected-time">-</h3> | |
| <small id="session-duration" class="text-muted d-block mt-2"></small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Active Sessions --> | |
| <div class="card shadow-sm mb-4"> | |
| <div class="card-header"> | |
| <h5 class="card-title mb-0">Active Sessions</h5> | |
| </div> | |
| <div class="card-body"> | |
| <div id="active-sessions" class="row g-3"> | |
| <!-- Sessions will be populated dynamically --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-lg-4"> | |
| <!-- Quick Actions --> | |
| <div class="card shadow-sm mb-4"> | |
| <div class="card-header"> | |
| <h5 class="card-title mb-0">Quick Actions</h5> | |
| </div> | |
| <div class="card-body"> | |
| <div class="d-grid gap-2"> | |
| <button class="btn btn-primary" onclick="downloadConfig()"> | |
| <i class="bi bi-cloud-download me-2"></i>Download Configuration | |
| </button> | |
| <button id="offline-toggle" class="btn btn-outline-primary" onclick="toggleOfflineAccess()"> | |
| <i class="bi bi-wifi-off me-2"></i> | |
| <span>Enable Offline Access</span> | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Active Protocols --> | |
| <div class="card shadow-sm mb-4"> | |
| <div class="card-header"> | |
| <h5 class="card-title mb-0">Active Protocols</h5> | |
| </div> | |
| <div class="card-body"> | |
| <div id="protocol-badges"> | |
| <!-- Protocol badges will be populated dynamically --> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <!-- Session Card Template --> | |
| <template id="session-template"> | |
| <div class="col-md-6"> | |
| <div class="card h-100"> | |
| <div class="card-body"> | |
| <div class="d-flex justify-content-between align-items-start mb-3"> | |
| <div> | |
| <h6 class="session-protocol mb-1"></h6> | |
| <small class="session-ip text-muted"></small> | |
| </div> | |
| <span class="session-status badge"></span> | |
| </div> | |
| <div class="session-stats small"> | |
| <div class="row g-2"> | |
| <div class="col-6"> | |
| <div class="p-2 border rounded"> | |
| <div class="text-muted">Upload</div> | |
| <div class="session-upload fw-bold"></div> | |
| </div> | |
| </div> | |
| <div class="col-6"> | |
| <div class="p-2 border rounded"> | |
| <div class="text-muted">Download</div> | |
| <div class="session-download fw-bold"></div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="mt-3"> | |
| <small class="session-time text-muted"></small> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </template> | |
| {% endblock %} | |
| {% block scripts %} | |
| <script> | |
| function formatBytes(bytes) { | |
| if (bytes === 0) return '0 B'; | |
| const k = 1024; | |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; | |
| const i = Math.floor(Math.log(bytes) / Math.log(k)); | |
| return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; | |
| } | |
| function formatDuration(ms) { | |
| const seconds = Math.floor(ms / 1000); | |
| const minutes = Math.floor(seconds / 60); | |
| const hours = Math.floor(minutes / 60); | |
| const days = Math.floor(hours / 24); | |
| if (days > 0) return `${days}d ${hours % 24}h`; | |
| if (hours > 0) return `${hours}h ${minutes % 60}m`; | |
| if (minutes > 0) return `${minutes}m ${seconds % 60}s`; | |
| return `${seconds}s`; | |
| } | |
| function formatDate(isoString) { | |
| return new Date(isoString).toLocaleString(); | |
| } | |
| function updateDashboard(stats) { | |
| // Update status | |
| const statusBadge = document.querySelector('#connection-status .badge'); | |
| statusBadge.className = 'badge ' + ( | |
| stats.status === 'active' ? 'bg-success' : | |
| stats.status === 'offline_available' ? 'bg-warning' : | |
| 'bg-secondary' | |
| ); | |
| statusBadge.textContent = stats.status.replace('_', ' ').toUpperCase(); | |
| // Update data transfer | |
| document.getElementById('data-sent').textContent = formatBytes(stats.bytes_sent); | |
| document.getElementById('data-received').textContent = formatBytes(stats.bytes_received); | |
| // Calculate progress percentages (assuming 1GB as max for visualization) | |
| const maxBytes = 1024 * 1024 * 1024; | |
| document.getElementById('upload-progress').style.width = | |
| Math.min((stats.bytes_sent / maxBytes) * 100, 100) + '%'; | |
| document.getElementById('download-progress').style.width = | |
| Math.min((stats.bytes_received / maxBytes) * 100, 100) + '%'; | |
| // Update connection time | |
| if (stats.connected_since) { | |
| document.getElementById('connected-time').textContent = formatDate(stats.connected_since); | |
| const duration = Date.now() - new Date(stats.connected_since); | |
| document.getElementById('session-duration').textContent = 'Duration: ' + formatDuration(duration); | |
| } | |
| // Update last seen | |
| if (stats.last_seen) { | |
| document.getElementById('last-seen').textContent = 'Last seen: ' + formatDate(stats.last_seen); | |
| } | |
| // Update active sessions | |
| const sessionsContainer = document.getElementById('active-sessions'); | |
| sessionsContainer.innerHTML = ''; | |
| const sessionTemplate = document.getElementById('session-template'); | |
| stats.active_sessions.forEach(session => { | |
| const sessionElement = document.importNode(sessionTemplate.content, true); | |
| sessionElement.querySelector('.session-protocol').textContent = session.protocol.toUpperCase(); | |
| sessionElement.querySelector('.session-ip').textContent = session.assigned_ip; | |
| const statusBadge = sessionElement.querySelector('.session-status'); | |
| statusBadge.className = 'badge ' + (session.is_offline ? 'bg-warning' : 'bg-success'); | |
| statusBadge.textContent = session.is_offline ? 'OFFLINE READY' : 'CONNECTED'; | |
| sessionElement.querySelector('.session-upload').textContent = formatBytes(session.bytes_sent); | |
| sessionElement.querySelector('.session-download').textContent = formatBytes(session.bytes_received); | |
| sessionElement.querySelector('.session-time').textContent = | |
| 'Connected: ' + formatDate(session.connected_since); | |
| sessionsContainer.appendChild(sessionElement); | |
| }); | |
| // Update protocols | |
| const protocolsContainer = document.getElementById('protocol-badges'); | |
| protocolsContainer.innerHTML = stats.protocols.map(protocol => | |
| `<span class="badge bg-primary me-2 mb-2">${protocol.toUpperCase()}</span>` | |
| ).join(''); | |
| // Update offline access button | |
| const offlineButton = document.getElementById('offline-toggle'); | |
| const hasOfflineSession = stats.active_sessions.some(s => s.is_offline); | |
| offlineButton.classList.toggle('btn-outline-primary', !hasOfflineSession); | |
| offlineButton.classList.toggle('btn-primary', hasOfflineSession); | |
| offlineButton.querySelector('span').textContent = | |
| hasOfflineSession ? 'Disable Offline Access' : 'Enable Offline Access'; | |
| } | |
| async function downloadConfig() { | |
| try { | |
| const response = await fetch('/download_config'); | |
| const config = await response.json(); | |
| // Create config file | |
| const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); | |
| const url = window.URL.createObjectURL(blob); | |
| // Trigger download | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'vpn-config.json'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| document.body.removeChild(a); | |
| window.URL.revokeObjectURL(url); | |
| } catch (error) { | |
| console.error('Error downloading config:', error); | |
| alert('Failed to download configuration. Please try again.'); | |
| } | |
| } | |
| async function toggleOfflineAccess() { | |
| try { | |
| const response = await fetch('/api/toggle_offline', { | |
| method: 'POST', | |
| headers: { 'Content-Type': 'application/json' } | |
| }); | |
| const result = await response.json(); | |
| if (result.success) { | |
| refreshStats(); | |
| } else { | |
| throw new Error(result.message || 'Failed to toggle offline access'); | |
| } | |
| } catch (error) { | |
| console.error('Error:', error); | |
| alert(error.message); | |
| } | |
| } | |
| function refreshStats() { | |
| fetch('/api/stats') | |
| .then(response => response.json()) | |
| .then(stats => updateDashboard(stats)) | |
| .catch(error => console.error('Error fetching stats:', error)); | |
| } | |
| // Initial load and periodic refresh | |
| refreshStats(); | |
| setInterval(refreshStats, 5000); | |
| </script> | |
| {% endblock %} | |
| <div class="col-md-12"> | |
| <div class="card"> | |
| <div class="card-body"> | |
| <h5 class="card-title">Usage History</h5> | |
| <canvas id="usageChart"></canvas> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="row"> | |
| <div class="col-md-6"> | |
| <div class="card"> | |
| <div class="card-body"> | |
| <h5 class="card-title">VPN Configuration</h5> | |
| <div class="mb-3"> | |
| <label class="form-label">Server Address</label> | |
| <div class="input-group"> | |
| <input type="text" class="form-control" id="server-address" readonly> | |
| <button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('server-address')"> | |
| Copy | |
| </button> | |
| </div> | |
| </div> | |
| <div class="mb-3"> | |
| <label class="form-label">Access Key</label> | |
| <div class="input-group"> | |
| <input type="text" class="form-control" id="access-key" readonly> | |
| <button class="btn btn-outline-secondary" type="button" onclick="copyToClipboard('access-key')"> | |
| Copy | |
| </button> | |
| </div> | |
| </div> | |
| <button class="btn btn-primary" onclick="downloadConfig()"> | |
| Download Configuration | |
| </button> | |
| </div> | |
| </div> | |
| </div> | |
| <div class="col-md-6"> | |
| <div class="card"> | |
| <div class="card-body"> | |
| <h5 class="card-title">Quick Setup Guide</h5> | |
| <ol class="list-group list-group-numbered"> | |
| <li class="list-group-item">Download the Outline Client for your device</li> | |
| <li class="list-group-item">Copy your access key from this dashboard</li> | |
| <li class="list-group-item">Open the Outline Client and paste your access key</li> | |
| <li class="list-group-item">Click "Connect" to start using the VPN</li> | |
| </ol> | |
| <div class="mt-3"> | |
| <a href="https://getoutline.org/get-started/" class="btn btn-outline-primary" target="_blank"> | |
| Download Outline Client | |
| </a> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| </div> | |
| {% endblock %} | |
| {% block extra_js %} | |
| <script> | |
| let usageChart; | |
| function formatBytes(bytes) { | |
| const sizes = ['B', 'KB', 'MB', 'GB', 'TB']; | |
| if (bytes === 0) return '0 B'; | |
| const i = parseInt(Math.floor(Math.log(bytes) / Math.log(1024))); | |
| return Math.round(bytes / Math.pow(1024, i), 2) + ' ' + sizes[i]; | |
| } | |
| function copyToClipboard(elementId) { | |
| const element = document.getElementById(elementId); | |
| element.select(); | |
| document.execCommand('copy'); | |
| alert('Copied to clipboard!'); | |
| } | |
| function downloadConfig() { | |
| fetch('/download_config') | |
| .then(response => response.json()) | |
| .then(config => { | |
| const blob = new Blob([JSON.stringify(config, null, 2)], { type: 'application/json' }); | |
| const url = window.URL.createObjectURL(blob); | |
| const a = document.createElement('a'); | |
| a.href = url; | |
| a.download = 'outline-config.json'; | |
| document.body.appendChild(a); | |
| a.click(); | |
| window.URL.revokeObjectURL(url); | |
| document.body.removeChild(a); | |
| }) | |
| .catch(error => console.error('Error downloading config:', error)); | |
| } | |
| function updateStats() { | |
| fetch('/api/stats') | |
| .then(response => response.json()) | |
| .then(stats => { | |
| document.getElementById('data-sent').textContent = formatBytes(stats.bytes_sent); | |
| document.getElementById('data-received').textContent = formatBytes(stats.bytes_received); | |
| document.getElementById('connection-status').innerHTML = | |
| `<span class="badge bg-${stats.status === 'active' ? 'success' : 'warning'}">${ | |
| stats.status.charAt(0).toUpperCase() + stats.status.slice(1) | |
| }</span>`; | |
| const connectedDate = new Date(stats.connected_since); | |
| document.getElementById('connected-time').textContent = | |
| connectedDate.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' }); | |
| // Update chart data | |
| if (usageChart) { | |
| // Add new data point | |
| const now = new Date(); | |
| usageChart.data.labels.push(now.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' })); | |
| usageChart.data.datasets[0].data.push(stats.bytes_sent / 1024 / 1024); | |
| usageChart.data.datasets[1].data.push(stats.bytes_received / 1024 / 1024); | |
| // Remove old data points if more than 10 | |
| if (usageChart.data.labels.length > 10) { | |
| usageChart.data.labels.shift(); | |
| usageChart.data.datasets[0].data.shift(); | |
| usageChart.data.datasets[1].data.shift(); | |
| } | |
| usageChart.update(); | |
| } | |
| }) | |
| .catch(error => console.error('Error fetching stats:', error)); | |
| } | |
| // Initialize usage chart | |
| document.addEventListener('DOMContentLoaded', function() { | |
| const ctx = document.getElementById('usageChart').getContext('2d'); | |
| usageChart = new Chart(ctx, { | |
| type: 'line', | |
| data: { | |
| labels: [], | |
| datasets: [{ | |
| label: 'Data Sent (MB)', | |
| borderColor: 'rgb(75, 192, 192)', | |
| data: [] | |
| }, { | |
| label: 'Data Received (MB)', | |
| borderColor: 'rgb(255, 99, 132)', | |
| data: [] | |
| }] | |
| }, | |
| options: { | |
| responsive: true, | |
| scales: { | |
| y: { | |
| beginAtZero: true, | |
| title: { | |
| display: true, | |
| text: 'Data (MB)' | |
| } | |
| } | |
| } | |
| } | |
| }); | |
| // Update stats initially and every 5 seconds | |
| updateStats(); | |
| setInterval(updateStats, 5000); | |
| }); | |
| </script> | |
| {% endblock %} | |