| {% 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">
|
|
|
| <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 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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <div class="chart-container" data-aos="fade-up">
|
| <h2 class="h5 mb-4">Traffic Overview</h2>
|
| <canvas id="traffic-chart"></canvas>
|
| </div>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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>
|
|
|
|
|
| <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">
|
|
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
| <div class="col-lg-4">
|
|
|
| <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>
|
|
|
|
|
| <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">
|
|
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
| </div>
|
|
|
|
|
| <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) {
|
|
|
| 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();
|
|
|
|
|
| document.getElementById('data-sent').textContent = formatBytes(stats.bytes_sent);
|
| document.getElementById('data-received').textContent = formatBytes(stats.bytes_received);
|
|
|
|
|
| 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) + '%';
|
|
|
|
|
| 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);
|
| }
|
|
|
|
|
| if (stats.last_seen) {
|
| document.getElementById('last-seen').textContent = 'Last seen: ' + formatDate(stats.last_seen);
|
| }
|
|
|
|
|
| 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);
|
| });
|
|
|
|
|
| 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('');
|
|
|
|
|
| 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();
|
|
|
|
|
| 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 = '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));
|
| }
|
|
|
|
|
| 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' });
|
|
|
|
|
| if (usageChart) {
|
|
|
| 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);
|
|
|
|
|
| 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));
|
| }
|
|
|
|
|
| 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)'
|
| }
|
| }
|
| }
|
| }
|
| });
|
|
|
|
|
| updateStats();
|
| setInterval(updateStats, 5000);
|
| });
|
| </script>
|
| {% endblock %}
|
|
|