| |
| |
|
|
|
|
|
|
| document.addEventListener('DOMContentLoaded', () => {
|
| initializeAnimations();
|
| initializeCharts();
|
| initializeEventListeners();
|
| });
|
|
|
|
|
| const animate = {
|
| fadeIn: (element, delay = 0) => {
|
| element.style.opacity = '0';
|
| element.style.transform = 'translateY(20px)';
|
|
|
| setTimeout(() => {
|
| element.style.transition = 'all 0.5s ease';
|
| element.style.opacity = '1';
|
| element.style.transform = 'translateY(0)';
|
| }, delay);
|
| },
|
|
|
| slideIn: (element, direction = 'left', delay = 0) => {
|
| const transforms = {
|
| left: 'translateX(-50px)',
|
| right: 'translateX(50px)',
|
| up: 'translateY(50px)',
|
| down: 'translateY(-50px)'
|
| };
|
|
|
| element.style.opacity = '0';
|
| element.style.transform = transforms[direction];
|
|
|
| setTimeout(() => {
|
| element.style.transition = 'all 0.5s ease';
|
| element.style.opacity = '1';
|
| element.style.transform = 'translate(0)';
|
| }, delay);
|
| },
|
|
|
| pulse: (element) => {
|
| element.style.transform = 'scale(1)';
|
| element.style.transition = 'transform 0.2s ease';
|
|
|
| element.style.transform = 'scale(1.05)';
|
| setTimeout(() => {
|
| element.style.transform = 'scale(1)';
|
| }, 200);
|
| }
|
| };
|
|
|
|
|
| function initializeAnimations() {
|
|
|
| const statCards = document.querySelectorAll('.stat-card');
|
| statCards.forEach((card, index) => {
|
| animate.fadeIn(card, index * 100);
|
| });
|
|
|
|
|
| const charts = document.querySelectorAll('.chart-container');
|
| charts.forEach((chart, index) => {
|
| animate.slideIn(chart, 'up', 300 + index * 100);
|
| });
|
|
|
|
|
| const sessionCards = document.querySelectorAll('.session-card');
|
| sessionCards.forEach((card, index) => {
|
| animate.slideIn(card, 'right', 500 + index * 100);
|
| });
|
| }
|
|
|
|
|
| function initializeCharts() {
|
|
|
| const trafficChart = document.getElementById('traffic-chart');
|
| if (trafficChart) {
|
| new Chart(trafficChart, {
|
| type: 'line',
|
| data: {
|
| labels: getLastNHours(24),
|
| datasets: [{
|
| label: 'Upload',
|
| data: generateRandomData(24),
|
| borderColor: '#4F46E5',
|
| tension: 0.4
|
| }, {
|
| label: 'Download',
|
| data: generateRandomData(24),
|
| borderColor: '#10B981',
|
| tension: 0.4
|
| }]
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: false,
|
| plugins: {
|
| legend: {
|
| position: 'top'
|
| }
|
| },
|
| scales: {
|
| y: {
|
| beginAtZero: true
|
| }
|
| }
|
| }
|
| });
|
| }
|
|
|
|
|
| const protocolChart = document.getElementById('protocol-chart');
|
| if (protocolChart) {
|
| new Chart(protocolChart, {
|
| type: 'doughnut',
|
| data: {
|
| labels: ['Shadowsocks', 'IKEv2', 'L2TP', 'PPTP'],
|
| datasets: [{
|
| data: [40, 30, 20, 10],
|
| backgroundColor: [
|
| '#4F46E5',
|
| '#10B981',
|
| '#F59E0B',
|
| '#EF4444'
|
| ]
|
| }]
|
| },
|
| options: {
|
| responsive: true,
|
| maintainAspectRatio: false,
|
| plugins: {
|
| legend: {
|
| position: 'bottom'
|
| }
|
| }
|
| }
|
| });
|
| }
|
| }
|
|
|
|
|
| function initializeEventListeners() {
|
|
|
| const quickActions = document.querySelectorAll('.quick-action');
|
| quickActions.forEach(action => {
|
| action.addEventListener('mouseenter', () => {
|
| animate.pulse(action);
|
| });
|
| });
|
|
|
|
|
| const sidebarToggle = document.getElementById('sidebar-toggle');
|
| const sidebar = document.querySelector('.dashboard-sidebar');
|
| if (sidebarToggle && sidebar) {
|
| sidebarToggle.addEventListener('click', () => {
|
| sidebar.classList.toggle('active');
|
| });
|
| }
|
|
|
|
|
| setInterval(updateLiveStats, 5000);
|
| }
|
|
|
|
|
| function getLastNHours(n) {
|
| const hours = [];
|
| for (let i = n - 1; i >= 0; i--) {
|
| const d = new Date();
|
| d.setHours(d.getHours() - i);
|
| hours.push(d.getHours() + ':00');
|
| }
|
| return hours;
|
| }
|
|
|
| function generateRandomData(n) {
|
| return Array.from({length: n}, () => Math.floor(Math.random() * 100));
|
| }
|
|
|
| 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 updateLiveStats() {
|
| fetch('/api/stats')
|
| .then(response => response.json())
|
| .then(data => {
|
|
|
| const statusIndicator = document.querySelector('.status-indicator');
|
| if (statusIndicator) {
|
| statusIndicator.className = `status-indicator ${data.isConnected ? 'online' : 'offline'}`;
|
| statusIndicator.textContent = data.isConnected ? 'Connected' : 'Disconnected';
|
| }
|
|
|
|
|
| const uploadStat = document.getElementById('upload-stat');
|
| const downloadStat = document.getElementById('download-stat');
|
| if (uploadStat) uploadStat.textContent = formatBytes(data.uploadTotal);
|
| if (downloadStat) downloadStat.textContent = formatBytes(data.downloadTotal);
|
|
|
|
|
| updateSessionsList(data.sessions);
|
| })
|
| .catch(console.error);
|
| }
|
|
|
| function updateSessionsList(sessions) {
|
| const sessionsList = document.getElementById('active-sessions');
|
| if (!sessionsList) return;
|
|
|
| sessions.forEach(session => {
|
| const existingSession = document.getElementById(`session-${session.id}`);
|
| if (!existingSession) {
|
|
|
| const sessionCard = createSessionCard(session);
|
| animate.slideIn(sessionCard, 'right');
|
| sessionsList.appendChild(sessionCard);
|
| } else {
|
|
|
| updateSessionCard(existingSession, session);
|
| }
|
| });
|
| }
|
|
|
| function createSessionCard(session) {
|
| const card = document.createElement('div');
|
| card.id = `session-${session.id}`;
|
| card.className = 'session-card dashboard-card';
|
| card.innerHTML = `
|
| <div class="session-info">
|
| <h6 class="mb-1">${session.protocol}</h6>
|
| <small class="text-muted">${session.ip}</small>
|
| </div>
|
| <div class="session-stats">
|
| <div class="mb-1">↑ ${formatBytes(session.upload)}</div>
|
| <div>↓ ${formatBytes(session.download)}</div>
|
| </div>
|
| `;
|
| return card;
|
| }
|
|
|
| function updateSessionCard(card, session) {
|
| const uploadEl = card.querySelector('.session-stats div:first-child');
|
| const downloadEl = card.querySelector('.session-stats div:last-child');
|
|
|
| uploadEl.textContent = `↑ ${formatBytes(session.upload)}`;
|
| downloadEl.textContent = `↓ ${formatBytes(session.download)}`;
|
| }
|
|
|