diff --git a/bin/collect_metrics.php b/bin/collect_metrics.php new file mode 100644 index 0000000..0f37455 --- /dev/null +++ b/bin/collect_metrics.php @@ -0,0 +1,85 @@ +#!/usr/bin/env php +collectMetrics(); + echo " Server: CPU={$serverMetrics['cpu_percent']}% RAM={$serverMetrics['ram_used_mb']}/{$serverMetrics['ram_total_mb']}MB "; + echo "Disk={$serverMetrics['disk_used_gb']}/{$serverMetrics['disk_total_gb']}GB "; + echo "Net RX={$serverMetrics['network_rx_mbps']}Mbps TX={$serverMetrics['network_tx_mbps']}Mbps\n"; + + // Collect client metrics + $clientMetrics = $monitoring->collectClientMetrics(); + + if (!empty($clientMetrics)) { + foreach ($clientMetrics as $cm) { + echo " Client #{$cm['client_id']} ({$cm['client_name']}): UP={$cm['speed_up_kbps']}Kbps DOWN={$cm['speed_down_kbps']}Kbps\n"; + } + } else { + echo " No active clients\n"; + } + + } catch (Exception $e) { + echo " ERROR: " . $e->getMessage() . "\n"; + } + } + + // Clean old metrics + ServerMonitoring::cleanOldMetrics(); + + // Calculate sleep time + $executionTime = microtime(true) - $startTime; + $sleepTime = max(0, 30 - $executionTime); + + echo "[" . date('Y-m-d H:i:s') . "] Collection completed in " . round($executionTime, 2) . "s, sleeping for " . round($sleepTime, 2) . "s\n\n"; + + if ($sleepTime > 0) { + sleep((int)$sleepTime); + } + + } catch (Exception $e) { + echo "[" . date('Y-m-d H:i:s') . "] FATAL ERROR: " . $e->getMessage() . "\n"; + echo "Retrying in 30 seconds...\n\n"; + sleep(30); + } +} diff --git a/inc/ServerMonitoring.php b/inc/ServerMonitoring.php new file mode 100644 index 0000000..af05a74 --- /dev/null +++ b/inc/ServerMonitoring.php @@ -0,0 +1,349 @@ +server = new VpnServer($serverId); + $this->serverData = $this->server->getData(); + } + + /** + * Collect all server metrics + */ + public function collectMetrics(): array + { + $metrics = [ + 'cpu_percent' => $this->getCpuUsage(), + 'ram_used_mb' => $this->getRamUsed(), + 'ram_total_mb' => $this->getRamTotal(), + 'disk_used_gb' => $this->getDiskUsed(), + 'disk_total_gb' => $this->getDiskTotal(), + 'network_rx_mbps' => $this->getNetworkRxSpeed(), + 'network_tx_mbps' => $this->getNetworkTxSpeed(), + ]; + + $this->saveServerMetrics($metrics); + + return $metrics; + } + + /** + * Collect client traffic metrics + */ + public function collectClientMetrics(): array + { + $clients = VpnClient::listByServer($this->serverData['id']); + $results = []; + + foreach ($clients as $client) { + if ($client['status'] !== 'active') continue; + + $stats = $this->getClientStats($client); + if ($stats) { + $this->saveClientMetrics($client['id'], $stats); + $results[] = [ + 'client_id' => $client['id'], + 'client_name' => $client['name'], + 'speed_up_kbps' => $stats['speed_up_kbps'], + 'speed_down_kbps' => $stats['speed_down_kbps'], + ]; + } + } + + return $results; + } + + /** + * Get CPU usage percentage + */ + private function getCpuUsage(): ?float + { + $cmd = "top -bn1 | grep 'Cpu(s)' | sed 's/.*, *\\([0-9.]*\\)%* id.*/\\1/' | awk '{print 100 - \$1}'"; + $result = $this->execSSH($cmd); + + return $result ? (float)trim($result) : null; + } + + /** + * Get RAM used in MB + */ + private function getRamUsed(): ?int + { + $cmd = "free -m | grep Mem | awk '{print \$3}'"; + $result = $this->execSSH($cmd); + + return $result ? (int)trim($result) : null; + } + + /** + * Get total RAM in MB + */ + private function getRamTotal(): ?int + { + $cmd = "free -m | grep Mem | awk '{print \$2}'"; + $result = $this->execSSH($cmd); + + return $result ? (int)trim($result) : null; + } + + /** + * Get disk used in GB + */ + private function getDiskUsed(): ?float + { + $cmd = "df -BG / | tail -1 | awk '{print \$3}' | sed 's/G//'"; + $result = $this->execSSH($cmd); + + return $result ? (float)trim($result) : null; + } + + /** + * Get total disk in GB + */ + private function getDiskTotal(): ?float + { + $cmd = "df -BG / | tail -1 | awk '{print \$2}' | sed 's/G//'"; + $result = $this->execSSH($cmd); + + return $result ? (float)trim($result) : null; + } + + /** + * Get network RX speed in Mbps + */ + private function getNetworkRxSpeed(): ?float + { + // Get bytes received on main interface + $cmd = "cat /sys/class/net/\$(ip route | grep default | awk '{print \$5}' | head -1)/statistics/rx_bytes"; + $bytes1 = $this->execSSH($cmd); + + if (!$bytes1) return null; + + sleep(1); // Wait 1 second + + $bytes2 = $this->execSSH($cmd); + + if (!$bytes2) return null; + + // Calculate speed in Mbps + $bytesPerSec = (int)$bytes2 - (int)$bytes1; + $mbps = ($bytesPerSec * 8) / 1000000; + + return round($mbps, 2); + } + + /** + * Get network TX speed in Mbps + */ + private function getNetworkTxSpeed(): ?float + { + // Get bytes transmitted on main interface + $cmd = "cat /sys/class/net/\$(ip route | grep default | awk '{print \$5}' | head -1)/statistics/tx_bytes"; + $bytes1 = $this->execSSH($cmd); + + if (!$bytes1) return null; + + sleep(1); // Wait 1 second + + $bytes2 = $this->execSSH($cmd); + + if (!$bytes2) return null; + + // Calculate speed in Mbps + $bytesPerSec = (int)$bytes2 - (int)$bytes1; + $mbps = ($bytesPerSec * 8) / 1000000; + + return round($mbps, 2); + } + + /** + * Get client current stats and calculate speed + */ + private function getClientStats(array $client): ?array + { + $db = DB::conn(); + + // Get current stats from server + $containerName = $this->serverData['container_name']; + $publicKey = $client['public_key']; + + $cmd = "docker exec {$containerName} wg show all dump | grep '{$publicKey}' | awk '{print \$6, \$7}'"; + $result = $this->execSSH($cmd); + + if (!$result) return null; + + list($bytesReceived, $bytesSent) = explode(' ', trim($result)); + + // Get previous metrics (30 seconds ago) + $stmt = $db->prepare(" + SELECT bytes_sent, bytes_received, collected_at + FROM client_metrics + WHERE client_id = ? + ORDER BY collected_at DESC + LIMIT 1 + "); + $stmt->execute([$client['id']]); + $previous = $stmt->fetch(PDO::FETCH_ASSOC); + + $speedUp = 0; + $speedDown = 0; + + if ($previous) { + $timeDiff = time() - strtotime($previous['collected_at']); + if ($timeDiff > 0) { + // Calculate speed in Kbps + $bytesDiffSent = (int)$bytesSent - (int)$previous['bytes_sent']; + $bytesDiffReceived = (int)$bytesReceived - (int)$previous['bytes_received']; + + $speedUp = round(($bytesDiffSent * 8) / $timeDiff / 1000, 2); + $speedDown = round(($bytesDiffReceived * 8) / $timeDiff / 1000, 2); + } + } + + return [ + 'bytes_sent' => (int)$bytesSent, + 'bytes_received' => (int)$bytesReceived, + 'speed_up_kbps' => $speedUp, + 'speed_down_kbps' => $speedDown, + ]; + } + + /** + * Save server metrics to database + */ + private function saveServerMetrics(array $metrics): void + { + $db = DB::conn(); + + $stmt = $db->prepare(" + INSERT INTO server_metrics + (server_id, cpu_percent, ram_used_mb, ram_total_mb, disk_used_gb, disk_total_gb, network_rx_mbps, network_tx_mbps) + VALUES (?, ?, ?, ?, ?, ?, ?, ?) + "); + + $stmt->execute([ + $this->serverData['id'], + $metrics['cpu_percent'], + $metrics['ram_used_mb'], + $metrics['ram_total_mb'], + $metrics['disk_used_gb'], + $metrics['disk_total_gb'], + $metrics['network_rx_mbps'], + $metrics['network_tx_mbps'], + ]); + } + + /** + * Save client metrics to database + */ + private function saveClientMetrics(int $clientId, array $stats): void + { + $db = DB::conn(); + + $stmt = $db->prepare(" + INSERT INTO client_metrics + (client_id, bytes_sent, bytes_received, speed_up_kbps, speed_down_kbps) + VALUES (?, ?, ?, ?, ?) + "); + + $stmt->execute([ + $clientId, + $stats['bytes_sent'], + $stats['bytes_received'], + $stats['speed_up_kbps'], + $stats['speed_down_kbps'], + ]); + } + + /** + * Get server metrics for last 24 hours + */ + public static function getServerMetrics(int $serverId, int $hours = 24): array + { + $db = DB::conn(); + + $stmt = $db->prepare(" + SELECT * + FROM server_metrics + WHERE server_id = ? + AND collected_at >= DATE_SUB(NOW(), INTERVAL ? HOUR) + ORDER BY collected_at ASC + "); + + $stmt->execute([$serverId, $hours]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Get client metrics for last 24 hours + */ + public static function getClientMetrics(int $clientId, int $hours = 24): array + { + $db = DB::conn(); + + $stmt = $db->prepare(" + SELECT * + FROM client_metrics + WHERE client_id = ? + AND collected_at >= DATE_SUB(NOW(), INTERVAL ? HOUR) + ORDER BY collected_at ASC + "); + + $stmt->execute([$clientId, $hours]); + + return $stmt->fetchAll(PDO::FETCH_ASSOC); + } + + /** + * Clean old metrics (older than 24 hours) + */ + public static function cleanOldMetrics(): void + { + $db = DB::conn(); + + // Clean server metrics + $db->exec("DELETE FROM server_metrics WHERE collected_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)"); + + // Clean client metrics + $db->exec("DELETE FROM client_metrics WHERE collected_at < DATE_SUB(NOW(), INTERVAL 24 HOUR)"); + } + + /** + * Execute SSH command on server + */ + private function execSSH(string $cmd): ?string + { + $host = $this->serverData['host']; + $port = $this->serverData['port']; + $username = $this->serverData['username']; + $password = $this->serverData['password']; + + $sshCmd = sprintf( + 'sshpass -p %s ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p %d %s@%s %s 2>/dev/null', + escapeshellarg($password), + $port, + escapeshellarg($username), + escapeshellarg($host), + escapeshellarg($cmd) + ); + + $output = shell_exec($sshCmd); + + return $output ?: null; + } +} diff --git a/inc/VpnClient.php b/inc/VpnClient.php index bca4a9d..1cf7732 100644 --- a/inc/VpnClient.php +++ b/inc/VpnClient.php @@ -689,15 +689,11 @@ class VpnClient { } /** - * Format bytes to human-readable string + * Format bytes to human-readable string (always in MB) */ private function formatBytes(int $bytes): string { - if ($bytes === 0) return '0 B'; - - $units = ['B', 'KB', 'MB', 'GB', 'TB']; - $i = floor(log($bytes) / log(1024)); - - return round($bytes / pow(1024, $i), 2) . ' ' . $units[$i]; + $mb = $bytes / 1048576; // 1024 * 1024 + return number_format($mb, 2) . ' MB'; } /** diff --git a/migrations/009_add_server_metrics.sql b/migrations/009_add_server_metrics.sql new file mode 100644 index 0000000..d06511c --- /dev/null +++ b/migrations/009_add_server_metrics.sql @@ -0,0 +1,31 @@ +-- Add server metrics tables +-- This migration adds functionality to store and display server monitoring data + +-- Server metrics (CPU, RAM, Disk, Network) +CREATE TABLE IF NOT EXISTS server_metrics ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + server_id INT UNSIGNED NOT NULL, + cpu_percent DECIMAL(5,2) NULL COMMENT 'CPU usage percentage', + ram_used_mb INT UNSIGNED NULL COMMENT 'RAM used in MB', + ram_total_mb INT UNSIGNED NULL COMMENT 'Total RAM in MB', + disk_used_gb DECIMAL(10,2) NULL COMMENT 'Disk used in GB', + disk_total_gb DECIMAL(10,2) NULL COMMENT 'Total disk in GB', + network_rx_mbps DECIMAL(10,2) NULL COMMENT 'Network receive speed in Mbps', + network_tx_mbps DECIMAL(10,2) NULL COMMENT 'Network transmit speed in Mbps', + collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_server_time (server_id, collected_at), + FOREIGN KEY (server_id) REFERENCES vpn_servers(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; + +-- Client traffic metrics (speed tracking) +CREATE TABLE IF NOT EXISTS client_metrics ( + id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY, + client_id INT UNSIGNED NOT NULL, + bytes_sent BIGINT UNSIGNED DEFAULT 0 COMMENT 'Bytes sent at this moment', + bytes_received BIGINT UNSIGNED DEFAULT 0 COMMENT 'Bytes received at this moment', + speed_up_kbps DECIMAL(10,2) NULL COMMENT 'Upload speed in Kbps', + speed_down_kbps DECIMAL(10,2) NULL COMMENT 'Download speed in Kbps', + collected_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + INDEX idx_client_time (client_id, collected_at), + FOREIGN KEY (client_id) REFERENCES vpn_clients(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/migrations/010_add_monitoring_translations.sql b/migrations/010_add_monitoring_translations.sql new file mode 100644 index 0000000..24dbba2 --- /dev/null +++ b/migrations/010_add_monitoring_translations.sql @@ -0,0 +1,92 @@ +-- Add translations for monitoring UI elements + +INSERT INTO translations (translation_key, language_code, translation_value) VALUES +-- Speed +('common.speed', 'en', 'Speed'), +('common.speed', 'ru', 'Скорость'), +('common.speed', 'es', 'Velocidad'), +('common.speed', 'de', 'Geschwindigkeit'), +('common.speed', 'fr', 'Vitesse'), +('common.speed', 'zh', '速度'), + +-- Metrics +('common.metrics', 'en', 'Metrics'), +('common.metrics', 'ru', 'Метрики'), +('common.metrics', 'es', 'Métricas'), +('common.metrics', 'de', 'Metriken'), +('common.metrics', 'fr', 'Métriques'), +('common.metrics', 'zh', '指标'), + +-- Server Info +('servers.server_info', 'en', 'Server Info'), +('servers.server_info', 'ru', 'Информация о сервере'), +('servers.server_info', 'es', 'Información del servidor'), +('servers.server_info', 'de', 'Serverinformationen'), +('servers.server_info', 'fr', 'Informations sur le serveur'), +('servers.server_info', 'zh', '服务器信息'), + +-- Status +('common.status', 'en', 'Status'), +('common.status', 'ru', 'Статус'), +('common.status', 'es', 'Estado'), +('common.status', 'de', 'Status'), +('common.status', 'fr', 'Statut'), +('common.status', 'zh', '状态'), + +-- Client Configuration +('clients.configuration', 'en', 'Client Configuration'), +('clients.configuration', 'ru', 'Конфигурация клиента'), +('clients.configuration', 'es', 'Configuración del cliente'), +('clients.configuration', 'de', 'Client-Konfiguration'), +('clients.configuration', 'fr', 'Configuration du client'), +('clients.configuration', 'zh', '客户端配置'), + +-- Traffic Statistics +('clients.traffic_stats', 'en', 'Traffic Statistics'), +('clients.traffic_stats', 'ru', 'Статистика трафика'), +('clients.traffic_stats', 'es', 'Estadísticas de tráfico'), +('clients.traffic_stats', 'de', 'Traffic-Statistiken'), +('clients.traffic_stats', 'fr', 'Statistiques de trafic'), +('clients.traffic_stats', 'zh', '流量统计'), + +-- Uploaded +('common.uploaded', 'en', 'Uploaded'), +('common.uploaded', 'ru', 'Отправлено'), +('common.uploaded', 'es', 'Subido'), +('common.uploaded', 'de', 'Hochgeladen'), +('common.uploaded', 'fr', 'Envoyé'), +('common.uploaded', 'zh', '上传'), + +-- Downloaded +('common.downloaded', 'en', 'Downloaded'), +('common.downloaded', 'ru', 'Получено'), +('common.downloaded', 'es', 'Descargado'), +('common.downloaded', 'de', 'Heruntergeladen'), +('common.downloaded', 'fr', 'Reçu'), +('common.downloaded', 'zh', '下载'), + +-- Total +('common.total', 'en', 'Total'), +('common.total', 'ru', 'Всего'), +('common.total', 'es', 'Total'), +('common.total', 'de', 'Gesamt'), +('common.total', 'fr', 'Total'), +('common.total', 'zh', '总计'), + +-- Created +('common.created', 'en', 'Created'), +('common.created', 'ru', 'Создан'), +('common.created', 'es', 'Creado'), +('common.created', 'de', 'Erstellt'), +('common.created', 'fr', 'Créé'), +('common.created', 'zh', '创建时间'), + +-- IP Address +('common.ip_address', 'en', 'IP Address'), +('common.ip_address', 'ru', 'IP-адрес'), +('common.ip_address', 'es', 'Dirección IP'), +('common.ip_address', 'de', 'IP-Adresse'), +('common.ip_address', 'fr', 'Adresse IP'), +('common.ip_address', 'zh', 'IP地址') + +ON DUPLICATE KEY UPDATE translation_value=VALUES(translation_value); diff --git a/public/index.php b/public/index.php index 4adc895..e7335d1 100644 --- a/public/index.php +++ b/public/index.php @@ -19,6 +19,7 @@ require_once __DIR__ . '/../inc/VpnClient.php'; require_once __DIR__ . '/../inc/Translator.php'; require_once __DIR__ . '/../inc/JWT.php'; require_once __DIR__ . '/../inc/PanelImporter.php'; +require_once __DIR__ . '/../inc/ServerMonitoring.php'; // Load environment configuration Config::load(__DIR__ . '/../.env'); @@ -48,6 +49,29 @@ Translator::init(); $user = Auth::user(); $appName = Config::get('APP_NAME', 'Amnezia VPN Panel'); +/** + * Helper function to authenticate user from JWT or session + * Returns user array or null if unauthorized + */ +function authenticateRequest(): ?array { + // Check JWT token in Authorization header + $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; + if ($authHeader && preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { + $token = $matches[1]; + $user = JWT::verify($token); + if ($user) { + return $user; + } + } + + // Fallback to session + if (isset($_SESSION['user_id'])) { + return Auth::user(); + } + + return null; +} + View::init(__DIR__ . '/../templates', [ 'app_name' => $appName, 'user' => $user, @@ -421,6 +445,36 @@ Router::get('/servers/{id}', function ($params) { } }); +// Server monitoring page +Router::get('/servers/{id}/monitoring', function ($params) { + requireAuth(); + $serverId = (int)$params['id']; + + try { + $server = new VpnServer($serverId); + $serverData = $server->getData(); + + // Check ownership + $user = Auth::user(); + if ($serverData['user_id'] != $user['id'] && !Auth::isAdmin()) { + http_response_code(403); + echo 'Forbidden'; + return; + } + + // Get clients for this server + $clients = VpnClient::listByServer($serverId); + + View::render('servers/monitoring.twig', [ + 'server' => $serverData, + 'clients' => $clients, + ]); + } catch (Exception $e) { + http_response_code(404); + echo 'Server not found'; + } +}); + // Delete server Router::post('/servers/{id}/delete', function ($params) { requireAuth(); @@ -1272,12 +1326,112 @@ Router::post('/api/clients/{id}/restore', function ($params) { } }); +// API: Get server metrics +Router::get('/api/servers/{id}/metrics', function ($params) { + header('Content-Type: application/json'); + + // Check authentication - either JWT or session + $user = null; + $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; + + if ($authHeader && preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { + // JWT authentication + $token = $matches[1]; + $user = JWT::verify($token); + } else if (isset($_SESSION['user_id'])) { + // Session authentication + $user = Auth::user(); + } + + if (!$user) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + return; + } + + $serverId = (int)$params['id']; + $hours = isset($_GET['hours']) ? (float)$_GET['hours'] : 24; + + try { + $server = new VpnServer($serverId); + $serverData = $server->getData(); + + // Check ownership + if ($serverData['user_id'] != $user['id'] && $user['role'] !== 'admin') { + http_response_code(403); + echo json_encode(['error' => 'Forbidden']); + return; + } + + $metrics = ServerMonitoring::getServerMetrics($serverId, $hours); + + echo json_encode(['success' => true, 'metrics' => $metrics]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); + } +}); + +// API: Get client metrics +Router::get('/api/clients/{id}/metrics', function ($params) { + header('Content-Type: application/json'); + + // Check authentication - either JWT or session + $user = null; + $authHeader = $_SERVER['HTTP_AUTHORIZATION'] ?? ''; + + if ($authHeader && preg_match('/Bearer\s+(.*)$/i', $authHeader, $matches)) { + // JWT authentication + $token = $matches[1]; + $user = JWT::verify($token); + } else if (isset($_SESSION['user_id'])) { + // Session authentication + $user = Auth::user(); + } + + if (!$user) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + return; + } + + $clientId = (int)$params['id']; + $hours = isset($_GET['hours']) ? (float)$_GET['hours'] : 24; + + try { + $client = new VpnClient($clientId); + $clientData = $client->getData(); + + // Get server to check ownership + $server = new VpnServer($clientData['server_id']); + $serverData = $server->getData(); + + // Check ownership + if ($serverData['user_id'] != $user['id'] && $user['role'] !== 'admin') { + http_response_code(403); + echo json_encode(['error' => 'Forbidden']); + return; + } + + $metrics = ServerMonitoring::getClientMetrics($clientId, $hours); + + echo json_encode(['success' => true, 'metrics' => $metrics]); + } catch (Exception $e) { + http_response_code(500); + echo json_encode(['error' => $e->getMessage()]); + } +}); + // API: Get server clients Router::get('/api/servers/{id}/clients', function ($params) { header('Content-Type: application/json'); - $user = JWT::requireAuth(); - if (!$user) return; + $user = authenticateRequest(); + if (!$user) { + http_response_code(401); + echo json_encode(['error' => 'Unauthorized']); + return; + } $serverId = (int)$params['id']; @@ -1286,8 +1440,7 @@ Router::get('/api/servers/{id}/clients', function ($params) { $serverData = $server->getData(); // Check ownership - $user = Auth::user(); - if ($serverData['user_id'] != $user['id'] && !Auth::isAdmin()) { + if ($serverData['user_id'] != $user['id'] && $user['role'] !== 'admin') { http_response_code(403); echo json_encode(['error' => 'Forbidden']); return; diff --git a/templates/clients/view.twig b/templates/clients/view.twig index 770ae78..90a055b 100644 --- a/templates/clients/view.twig +++ b/templates/clients/view.twig @@ -6,19 +6,19 @@
-

Client Configuration

+

{{ t('clients.configuration') }}

-
IP Address
{{ client.client_ip }}
-
Status
+
{{ t('common.ip_address') }}
{{ client.client_ip }}
+
{{ t('common.status') }}
{% if client.status == 'active' %} - Active + {{ t('status.active') }} {% else %} - Disabled + {{ t('status.disabled') }} {% endif %}
-
Created
{{ client.created_at }}
+
{{ t('common.created') }}
{{ client.created_at }}
@@ -42,16 +42,16 @@
-

Traffic Statistics

+

{{ t('clients.traffic_stats') }}

-
Uploaded
{{ client.bytes_sent|default(0)|number_format }} B
-
Downloaded
{{ client.bytes_received|default(0)|number_format }} B
-
Total
{{ (client.bytes_sent|default(0) + client.bytes_received|default(0))|number_format }} B
-
Last Handshake
+
{{ t('common.uploaded') }}
{{ (client.bytes_sent|default(0) / 1048576)|number_format(2) }} MB
+
{{ t('common.downloaded') }}
{{ (client.bytes_received|default(0) / 1048576)|number_format(2) }} MB
+
{{ t('common.total') }}
{{ ((client.bytes_sent|default(0) + client.bytes_received|default(0)) / 1048576)|number_format(2) }} MB
+
{{ t('clients.last_handshake') }}
{% if client.last_handshake %} {{ client.last_handshake }} diff --git a/templates/layout.twig b/templates/layout.twig index 0a44255..8b6db1d 100644 --- a/templates/layout.twig +++ b/templates/layout.twig @@ -6,6 +6,8 @@ {% block title %}{{ app_name }}{% endblock %} + + {% block styles %}{% endblock %} +{% endblock %} + {% block content %}
{% endblock %} + +{% block scripts %} + +{% endblock %} diff --git a/templates/servers/monitoring.twig b/templates/servers/monitoring.twig new file mode 100644 index 0000000..a6c5374 --- /dev/null +++ b/templates/servers/monitoring.twig @@ -0,0 +1,421 @@ +{% extends "layout.twig" %} + +{% block title %}{{ server.name }} - Monitoring{% endblock %} + +{% block styles %} + +{% endblock %} + +{% block content %} +
+ ← Back to Server + +

{{ server.name }} - Monitoring

+ +
+ Auto-refresh every 30 seconds +
+ +
+
+

+ CPU Usage + -- +

+
+ +
+
+ +
+

+ RAM Usage + -- +

+
+ +
+
+ +
+

+ Disk Usage + -- +

+
+ +
+
+ +
+

+ Network Speed + -- +

+
+ +
+
+
+ +
+

Client Speeds

+
+
Loading...
+
+
+
+{% endblock %} + +{% block scripts %} + + +{% endblock %} diff --git a/templates/servers/view.twig b/templates/servers/view.twig index a95ba29..d1a8a8d 100644 --- a/templates/servers/view.twig +++ b/templates/servers/view.twig @@ -1,8 +1,50 @@ {% extends "layout.twig" %} {% block title %}{{ server.name }}{% endblock %} + +{% block styles %} + +{% endblock %} + {% block content %}
-

{{ server.name }}

{{ server.host }}

+
+

{{ server.name }}

+

{{ server.host }}

+
{% if import_message %}
@@ -13,12 +55,41 @@
-

Server Info

+

{{ t('servers.server_info') }}

-
Status
{{ server.status }}
+
{{ t('common.status') }}
{{ server.status }}
VPN Port
{{ server.vpn_port }}
Subnet
{{ server.vpn_subnet }}
+ + {% if server.status == 'active' %} +
+
+ + CPU + +
--
+
+
+ + RAM + +
--
+
+
+ + Disk + +
--
+
+
+ + Network + +
--
+
+
+ {% endif %}

{{ t('clients.create') }}

@@ -97,6 +168,7 @@ {{ t('clients.expiration') }} {{ t('clients.traffic') }} {{ t('clients.traffic_limit') }} + {{ t('common.speed') }} {{ t('clients.last_handshake') }} {{ t('clients.actions') }} @@ -164,6 +236,9 @@ {{ t('clients.unlimited') }} {% endif %} + +
-
+ {% if client.last_handshake %} {{ client.last_handshake }} @@ -431,5 +506,204 @@ async function deleteBackup(backupId) { alert('Error: ' + error.message); } } + +// Server metrics sparklines +{% if server.status == 'active' %} +const serverId = {{ server.id }}; +let sparklineCharts = {}; + +function initSparklines() { + const chartConfig = { + type: 'line', + options: { + responsive: true, + maintainAspectRatio: false, + plugins: { + legend: { display: false }, + tooltip: { enabled: false } + }, + scales: { + x: { display: false }, + y: { display: false, beginAtZero: true } + }, + elements: { + line: { borderWidth: 2, tension: 0.4 }, + point: { radius: 0 } + } + } + }; + + sparklineCharts.cpu = new Chart(document.getElementById('cpuSparkline'), { + ...chartConfig, + data: { + labels: [], + datasets: [{ + data: [], + borderColor: '#dc3545', + backgroundColor: 'rgba(220, 53, 69, 0.1)', + fill: true + }] + }, + options: { + ...chartConfig.options, + scales: { + ...chartConfig.options.scales, + y: { display: false, beginAtZero: true, max: 100 } + } + } + }); + + sparklineCharts.ram = new Chart(document.getElementById('ramSparkline'), { + ...chartConfig, + data: { + labels: [], + datasets: [{ + data: [], + borderColor: '#ffc107', + backgroundColor: 'rgba(255, 193, 7, 0.1)', + fill: true + }] + } + }); + + sparklineCharts.disk = new Chart(document.getElementById('diskSparkline'), { + ...chartConfig, + data: { + labels: [], + datasets: [{ + data: [], + borderColor: '#17a2b8', + backgroundColor: 'rgba(23, 162, 184, 0.1)', + fill: true + }] + } + }); + + sparklineCharts.network = new Chart(document.getElementById('networkSparkline'), { + ...chartConfig, + data: { + labels: [], + datasets: [{ + data: [], + borderColor: '#28a745', + backgroundColor: 'rgba(40, 167, 69, 0.1)', + fill: true + }] + } + }); +} + +async function updateServerMetrics() { + try { + const response = await fetch(`/api/servers/${serverId}/metrics?hours=1`, { + credentials: 'same-origin' + }); + + const data = await response.json(); + + console.log('Metrics response:', data); + + if (data.success && data.metrics.length > 0) { + const metrics = data.metrics.slice(-20); // Last 20 points + const latest = metrics[metrics.length - 1]; + + // Update values with more details + const cpuVal = parseFloat(latest.cpu_percent).toFixed(1); + document.getElementById('cpuValue').textContent = `${cpuVal}%`; + + const ramUsed = Math.round(latest.ram_used_mb / 1024 * 10) / 10; + const ramTotal = Math.round(latest.ram_total_mb / 1024 * 10) / 10; + const ramPercent = (latest.ram_used_mb / latest.ram_total_mb * 100).toFixed(0); + document.getElementById('ramValue').textContent = `${ramPercent}% (${ramUsed}G)`; + + const diskUsed = parseFloat(latest.disk_used_gb).toFixed(0); + const diskTotal = parseFloat(latest.disk_total_gb).toFixed(0); + const diskPercent = (latest.disk_used_gb / latest.disk_total_gb * 100).toFixed(0); + document.getElementById('diskValue').textContent = `${diskPercent}% (${diskUsed}G)`; + + const netRx = parseFloat(latest.network_rx_mbps).toFixed(1); + const netTx = parseFloat(latest.network_tx_mbps).toFixed(1); + document.getElementById('networkValue').textContent = `↓${netRx} ↑${netTx}`; + + // Update sparklines + const labels = metrics.map((_, i) => i); + + sparklineCharts.cpu.data.labels = labels; + sparklineCharts.cpu.data.datasets[0].data = metrics.map(m => parseFloat(m.cpu_percent)); + sparklineCharts.cpu.update('none'); + + sparklineCharts.ram.data.labels = labels; + sparklineCharts.ram.data.datasets[0].data = metrics.map(m => (m.ram_used_mb / m.ram_total_mb * 100)); + sparklineCharts.ram.update('none'); + + sparklineCharts.disk.data.labels = labels; + sparklineCharts.disk.data.datasets[0].data = metrics.map(m => (m.disk_used_gb / m.disk_total_gb * 100)); + sparklineCharts.disk.update('none'); + + sparklineCharts.network.data.labels = labels; + sparklineCharts.network.data.datasets[0].data = metrics.map(m => parseFloat(m.network_rx_mbps) + parseFloat(m.network_tx_mbps)); + sparklineCharts.network.update('none'); + } + } catch (error) { + console.error('Failed to fetch metrics:', error); + // Show error in UI + document.getElementById('cpuValue').textContent = 'Error'; + document.getElementById('ramValue').textContent = 'Error'; + document.getElementById('diskValue').textContent = 'Error'; + document.getElementById('networkValue').textContent = 'Error'; + } +} + +// Initialize sparklines if active +if (document.getElementById('cpuSparkline')) { + initSparklines(); + updateServerMetrics(); + setInterval(updateServerMetrics, 30000); // Update every 30 seconds +} + +// Update client speeds +async function updateClientSpeeds() { + const clientRows = document.querySelectorAll('[id^="client-speed-"]'); + + console.log('Found client speed rows:', clientRows.length); + + for (const row of clientRows) { + const clientId = row.id.replace('client-speed-', ''); + + console.log(`Fetching metrics for client ${clientId}`); + + try { + const response = await fetch(`/api/clients/${clientId}/metrics?hours=1`, { + credentials: 'same-origin' + }); + + const data = await response.json(); + + console.log(`Client ${clientId} metrics:`, data); + + if (data.success && data.metrics && data.metrics.length > 0) { + const latest = data.metrics[data.metrics.length - 1]; + const speedUp = parseFloat(latest.speed_up_kbps).toFixed(1); + const speedDown = parseFloat(latest.speed_down_kbps).toFixed(1); + + // Format as compact badge + row.innerHTML = `↑${speedUp} ↓${speedDown} KB/s`; + } else { + console.log(`No metrics for client ${clientId}`); + row.innerHTML = '-'; + } + } catch (error) { + console.error(`Failed to fetch metrics for client ${clientId}:`, error); + row.innerHTML = '-'; + } + } +} + +// Update client speeds on load and every 30 seconds +if (document.querySelector('[id^="client-speed-"]')) { + updateClientSpeeds(); + setInterval(updateClientSpeeds, 30000); +} +{% endif %} {% endblock %}