JRNET / web /templates /dashboard.html
Factor Studios
Upload 96 files
6a5b8d8 verified
{% 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 %}