Преглед на файлове

receiving server status data is done in the background, after loading the status page

root преди 8 месеца
родител
ревизия
16e7ed4fb4
променени са 5 файла, в които са добавени 538 реда и са изтрити 371 реда
  1. 5 0
      config.php
  2. 248 0
      functions.php
  3. 141 0
      get_server_data.php
  4. 47 0
      handle_action.php
  5. 97 371
      index.php

+ 5 - 0
config.php

@@ -1,4 +1,9 @@
 <?php
+
+defined('CONFIG') or die('Direct access not allowed');
+
+define('REQUEST_INTERVAL', 60);
+
 // config.php - конфигурация OpenVPN серверов
 return [
     'server1' => [

+ 248 - 0
functions.php

@@ -0,0 +1,248 @@
+<?php
+
+defined('CONFIG') or die('Direct access not allowed');
+
+function canRequestStatus($server) {
+    if (!isset($_SESSION['last_request_time'][$server['name']])) { return true; }
+    if (time() - $_SESSION['last_request_time'][$server['name']] >= REQUEST_INTERVAL) { return true; }
+    return false;
+}
+
+function updateLastRequestTime($server) {
+    $_SESSION['last_request_time'][$server['name']] = time();
+}
+
+function openvpnManagementCommand($server, $command) {
+    $mgmt_host = $server['host'];
+    $mgmt_port = $server['port'];
+    $mgmt_pass = $server['password'];
+
+    $timeout = 5;
+    $socket = @fsockopen($mgmt_host, $mgmt_port, $errno, $errstr, $timeout);
+    
+    if (!$socket) {
+        error_log("OpenVPN management connection failed to {$server['name']}: $errstr ($errno)");
+        return false;
+    }
+
+    stream_set_timeout($socket, $timeout);
+    
+    try {
+        // Читаем приветственное сообщение
+        $welcome = '';
+        while (!feof($socket)) {
+            $line = fgets($socket);
+            if ($line === false) break;
+            $welcome .= $line;
+            if (strpos($welcome, 'ENTER PASSWORD:') !== false) break;
+        }
+
+        // Отправляем пароль
+        if (@fwrite($socket, "$mgmt_pass\n") === false) {
+            throw new Exception("Failed to send password");
+        }
+
+        // Ждем подтверждения аутентификации
+        $authResponse = '';
+        while (!feof($socket)) {
+            $line = fgets($socket);
+            if ($line === false) break;
+            $authResponse .= $line;
+            if (strpos($authResponse, 'SUCCESS:') !== false || strpos($authResponse, '>INFO:') !== false) break;
+        }
+
+        // Отправляем команду
+        if (@fwrite($socket, "$command\n") === false) {
+            throw new Exception("Failed to send command");
+        }
+
+        // Читаем ответ
+        $response = '';
+        $expectedEnd = strpos($command, 'status') !== false ? "END\r\n" : ">";
+        while (!feof($socket)) {
+            $line = fgets($socket);
+            if ($line === false) break;
+            $response .= $line;
+            if (strpos($response, $expectedEnd) !== false) break;
+        }
+
+        return $response;
+
+    } catch (Exception $e) {
+        error_log("OpenVPN management error ({$server['name']}): " . $e->getMessage());
+        return false;
+    } finally {
+        @fwrite($socket, "quit\n");
+        @fclose($socket);
+    }
+}
+
+function getOpenVPNStatus($server) {
+    // Проверяем, можно ли делать запрос
+    if (!canRequestStatus($server)) {
+        // Возвращаем кэшированные данные или пустой массив
+        return $_SESSION['cached_status'][$server['name']] ?? [];
+    }
+
+    // Обновляем время последнего запроса
+    updateLastRequestTime($server);
+
+    $response = openvpnManagementCommand($server, "status 2");
+    if (!$response) return $_SESSION['cached_status'][$server['name']] ?? [];
+
+    $clients = [];
+    $lines = explode("\n", $response);
+    $in_client_list = false;
+
+    foreach ($lines as $line) {
+        $line = trim($line);
+
+        if (strpos($line, 'HEADER,CLIENT_LIST') === 0) {
+            $in_client_list = true;
+            continue;
+        }
+
+        if (strpos($line, 'HEADER,ROUTING_TABLE') === 0) {
+            $in_client_list = false;
+            continue;
+        }
+
+        if ($in_client_list && strpos($line, 'CLIENT_LIST') === 0) {
+            $parts = explode(',', $line);
+            if (count($parts) >= 9) {
+                $clients[] = [
+                    'name' => $parts[1],
+                    'real_ip' => $parts[2],
+                    'virtual_ip' => $parts[3],
+                    'bytes_received' => formatBytes($parts[5]),
+                    'bytes_sent' => formatBytes($parts[6]),
+                    'connected_since' => $parts[7],
+                    'username' => $parts[8] ?? $parts[1],
+                    'banned' => isClientBanned($server, $parts[1])
+                ];
+            }
+        }
+    }
+
+    // Кэшируем результат
+    $_SESSION['cached_status'][$server['name']] = $clients;
+
+    return $clients;
+}
+
+function getAccountList($server) {
+    $accounts = [];
+    
+    // Получаем список из index.txt (неотозванные сертификаты)
+    if (!empty($server['cert_index']) && file_exists($server['cert_index'])) {
+        $index_content = file_get_contents($server['cert_index']);
+        $lines = explode("\n", $index_content);
+        
+        foreach ($lines as $line) {
+            if (empty(trim($line))) continue;
+            
+            $parts = preg_split('/\s+/', $line);
+            if (count($parts) >= 6 && $parts[0] === 'V') { // Только валидные сертификаты
+                $username = $parts[5];
+                $accounts[$username]["username"] = $username;
+                $accounts[$username]["ip"] = $ip;
+                $accounts[$username]["banned"] = false;
+                if (isClientBanned($server,$username)) { 
+                    $accounts[$username]["banned"] = true;
+                    }
+            }
+        }
+    }
+
+    // Получаем список выданных IP из ipp.txt
+    $assigned_ips = [];
+    if (!empty($server['ipp_file']) && file_exists($server['ipp_file'])) {
+        $ipp_content = file_get_contents($server['ipp_file']);
+        $lines = explode("\n", $ipp_content);
+
+        foreach ($lines as $line) {
+            if (empty(trim($line))) continue;
+
+            $parts = explode(',', $line);
+            if (count($parts) >= 2) {
+                $username = $parts[0];
+                $ip = $parts[1];
+                $accounts[$username]["username"] = $username;
+                $accounts[$username]["ip"] = $ip;
+                $accounts[$username]["banned"] = false;
+                if (isClientBanned($server,$username)) { 
+                    $accounts[$username]["banned"] = true;
+                    }
+            }
+        }
+    }
+
+    return $accounts;
+}
+
+function isClientBanned($server, $client_name) {
+    $ccd_file = "{$server['ccd']}/$client_name";
+    return file_exists($ccd_file) && 
+           preg_match('/^disable$/m', file_get_contents($ccd_file));
+}
+
+function kickClient($server, $client_name) {
+    return openvpnManagementCommand($server, "kill $client_name");
+}
+
+function banClient($server, $client_name) {
+    $ccd_file = "{$server['ccd']}/$client_name";
+    
+    // Добавляем директиву disable
+    $content = file_exists($ccd_file) ? file_get_contents($ccd_file) : '';
+    if (!preg_match('/^disable$/m', $content)) {
+        file_put_contents($ccd_file, $content . "\ndisable\n");
+    }
+
+    // Кикаем клиента
+    kickClient($server, $client_name);
+    return true;
+}
+
+function unbanClient($server, $client_name) {
+    $ccd_file = "{$server['ccd']}/$client_name";
+    if (file_exists($ccd_file)) {
+        $content = file_get_contents($ccd_file);
+        $new_content = preg_replace('/^disable$\n?/m', '', $content);
+        file_put_contents($ccd_file, $new_content);
+        return true;
+    }
+    return false;
+}
+
+function formatBytes($bytes) {
+    $bytes = (int)$bytes;
+    if ($bytes <= 0) return '0 B';
+    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
+    $pow = floor(log($bytes)/log(1024));
+    return round($bytes/pow(1024,$pow),2).' '.$units[$pow];
+}
+
+function getBannedClients($server, $active_clients) {
+    $banned = [];
+    $active_names = array_column($active_clients, 'name');
+    
+    if (is_dir($server['ccd'])) {
+        foreach (scandir($server['ccd']) as $file) {
+            if ($file !== '.' && $file !== '..' && is_file("{$server['ccd']}/$file")) {
+                if (isClientBanned($server, $file) && !in_array($file, $active_names)) {
+                    $banned[] = $file;
+                }
+            }
+        }
+    }
+    
+    return $banned;
+}
+
+function isClientActive($active_clients,$username) {
+    $active_names = array_column($active_clients, 'name');
+    if (!empty($active_names[$username])) { return true; }
+    return false;
+}
+

+ 141 - 0
get_server_data.php

@@ -0,0 +1,141 @@
+<?php
+
+session_start();
+
+// 1. Проверяем AJAX-запрос
+if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || strtolower($_SERVER['HTTP_X_REQUESTED_WITH']) !== 'xmlhttprequest') {
+    dieAjaxError('Direct access not allowed');
+}
+
+// 2. Проверяем CSRF-токен
+if (empty($_GET['csrf']) || $_GET['csrf'] !== $_SESSION['csrf_token']) {
+    dieAjaxError('Invalid CSRF token');
+}
+
+// Если все проверки пройдены, выполняем основной код
+function dieAjaxError($message) {
+    header('HTTP/1.0 403 Forbidden');
+    header('Content-Type: application/json');
+    die(json_encode(['error' => $message]));
+}
+
+define("CONFIG", 1);
+
+require_once 'functions.php';
+
+$config_file = __DIR__ . '/config.php';
+if (!file_exists($config_file)) {
+    die("Configuration file not found: $config_file");
+}
+
+$servers = require_once $config_file;
+
+$server_name = $_GET['server'] ?? '';
+
+if (!isset($servers[$server_name])) {
+    die("Invalid server name");
+}
+
+$server = $servers[$server_name];
+$clients = getOpenVPNStatus($server);
+$banned_clients = getBannedClients($server, $clients);
+$accounts = getAccountList($server);
+
+// Генерируем HTML для этого сервера
+ob_start();
+?>
+<h2><?= htmlspecialchars($server['title']) ?></h2>
+
+<div class="section">
+    <h3>Active Connections</h3>
+    <?php if (!empty($clients)): ?>
+    <table>
+        <thead>
+            <tr>
+                <th>Client</th>
+                <th>Real IP</th>
+                <th>Virtual IP</th>
+                <th>Traffic</th>
+                <th>Connected</th>
+                <th>Status</th>
+                <th>Actions</th>
+            </tr>
+        </thead>
+        <tbody>
+            <?php foreach ($clients as $client): ?>
+            <tr class="<?= $client['banned'] ? 'banned' : '' ?>">
+                <td><?= htmlspecialchars($client['name']) ?></td>
+                <td><?= htmlspecialchars($client['real_ip']) ?></td>
+                <td><?= htmlspecialchars($client['virtual_ip']) ?></td>
+                <td>↓<?= $client['bytes_received'] ?> ↑<?= $client['bytes_sent'] ?></td>
+                <td><?= htmlspecialchars($client['connected_since']) ?></td>
+                <td>
+                    <span class="status-badge <?= $client['banned'] ? 'status-banned' : 'status-active' ?>">
+                        <?= $client['banned'] ? 'BANNED' : 'Active' ?>
+                    </span>
+                </td>
+                <td class="actions">
+                    <?php if ($client['banned']): ?>
+                        <button onclick="handleAction('<?= $server_name ?>', 'unban', '<?= htmlspecialchars($client['name']) ?>')" 
+                                class="btn unban-btn">Unban</button>
+                    <?php else: ?>
+                        <button onclick="handleAction('<?= $server_name ?>', 'ban', '<?= htmlspecialchars($client['name']) ?>')" 
+                                class="btn ban-btn">Ban</button>
+                    <?php endif; ?>
+                </td>
+            </tr>
+            <?php endforeach; ?>
+        </tbody>
+    </table>
+    <?php else: ?>
+    <p>No active connections</p>
+    <?php endif; ?>
+</div>
+
+<div class="section">
+    <div class="spoiler">
+        <div class="spoiler-title collapsed" onclick="toggleSpoiler(this)">
+            Account List (<?= count($accounts) ?>)
+        </div>
+        <div class="spoiler-content">
+            <table>
+                <thead>
+                    <tr>
+                        <th>Account</th>
+                        <th>Assigned IP</th>
+                        <th>Status</th>
+                        <th>Actions</th>
+                    </tr>
+                </thead>
+                <tbody>
+                    <?php foreach ($accounts as $account): ?>
+                    <tr>
+                        <td><?= htmlspecialchars($account["username"]) ?></td>
+                        <td><?= htmlspecialchars($account['ip'] ?? 'N/A') ?></td>
+                        <td>
+                            <span class="status-badge <?= $account['banned'] ? 'status-banned' : 'status-active' ?>">
+                                <?= $account['banned'] ? 'BANNED' : 'ACTIVE' ?>
+                            </span>
+                        </td>
+                        <td class="actions">
+                            <?php if ($account['banned']): ?>
+                                <button onclick="handleAction('<?= $server_name ?>', 'unban', '<?= htmlspecialchars($account['username']) ?>')" 
+                                        class="btn unban-btn">Unban</button>
+                            <?php else: ?>
+                                <button onclick="handleAction('<?= $server_name ?>', 'ban', '<?= htmlspecialchars($account['username']) ?>')" 
+                                        class="btn ban-btn">Ban</button>
+                            <?php endif; ?>
+                        </td>
+                    </tr>
+                    <?php endforeach; ?>
+                </tbody>
+            </table>
+        </div>
+    </div>
+</div>
+
+<div class="last-update">
+    Last update: <?= date('Y-m-d H:i:s') ?>
+</div>
+<?php
+echo ob_get_clean();

+ 47 - 0
handle_action.php

@@ -0,0 +1,47 @@
+<?php
+
+define("CONFIG", 1);
+
+require_once 'functions.php';
+
+// Подключаем конфигурационный файл
+$config_file = __DIR__ . '/config.php';
+if (!file_exists($config_file)) {
+    die("Configuration file not found: $config_file");
+}
+$servers = require_once $config_file;
+
+// Проверяем AJAX-запрос
+if (empty($_SERVER['HTTP_X_REQUESTED_WITH']) || $_SERVER['HTTP_X_REQUESTED_WITH'] !== 'XMLHttpRequest') {
+    http_response_code(403);
+    die(json_encode(['success' => false, 'message' => 'Direct access not allowed']));
+}
+
+// Обработка POST-данных
+$server_name = $_POST['server'] ?? null;
+$action = $_POST['action'] ?? null;
+$client_name = $_POST['client'] ?? null;
+
+if (!isset($servers[$server_name])) {
+    die(json_encode(['success' => false, 'message' => 'Invalid server']));
+}
+
+$server = $servers[$server_name];
+$result = false;
+
+try {
+    switch ($action) {
+        case 'ban':
+            $result = banClient($server, $client_name);
+            break;
+        case 'unban':
+            $result = unbanClient($server, $client_name);
+            break;
+        default:
+            throw new Exception('Invalid action');
+    }
+    
+    echo json_encode(['success' => $result]);
+} catch (Exception $e) {
+    echo json_encode(['success' => false, 'message' => $e->getMessage()]);
+}

+ 97 - 371
index.php

@@ -1,287 +1,32 @@
 <?php
+
+define("CONFIG", 1);
+
 // Настройки
 $page_title = 'OpenVPN Status';
 
-// Ограничение частоты запросов (в секундах)
-define('REQUEST_INTERVAL', 60);
-
 // Подключаем конфигурационный файл
 $config_file = __DIR__ . '/config.php';
 if (!file_exists($config_file)) {
     die("Configuration file not found: $config_file");
 }
-$servers = require $config_file;
+$servers = require_once $config_file;
 
 session_start();
 
+$_SESSION['csrf_token'] = bin2hex(random_bytes(32));
+
 // Проверяем и инициализируем массив, если его нет
 if (!isset($_SESSION['last_request_time']) || !is_array($_SESSION['last_request_time'])) {
     $_SESSION['last_request_time'] = []; // Создаем пустой массив
 }
 
-function canRequestStatus($server) {
-    if (!isset($_SESSION['last_request_time'][$server['name']])) { return true; }
-    if (time() - $_SESSION['last_request_time'][$server['name']] >= REQUEST_INTERVAL) { return true; }
-    return false;
-}
-
-function updateLastRequestTime($server) {
-    $_SESSION['last_request_time'][$server['name']] = time();
-}
-
-function openvpnManagementCommand($server, $command) {
-    $mgmt_host = $server['host'];
-    $mgmt_port = $server['port'];
-    $mgmt_pass = $server['password'];
-
-    $timeout = 5;
-    $socket = @fsockopen($mgmt_host, $mgmt_port, $errno, $errstr, $timeout);
-    
-    if (!$socket) {
-        error_log("OpenVPN management connection failed to {$server['name']}: $errstr ($errno)");
-        return false;
-    }
-
-    stream_set_timeout($socket, $timeout);
-    
-    try {
-        // Читаем приветственное сообщение
-        $welcome = '';
-        while (!feof($socket)) {
-            $line = fgets($socket);
-            if ($line === false) break;
-            $welcome .= $line;
-            if (strpos($welcome, 'ENTER PASSWORD:') !== false) break;
-        }
-
-        // Отправляем пароль
-        if (@fwrite($socket, "$mgmt_pass\n") === false) {
-            throw new Exception("Failed to send password");
-        }
-
-        // Ждем подтверждения аутентификации
-        $authResponse = '';
-        while (!feof($socket)) {
-            $line = fgets($socket);
-            if ($line === false) break;
-            $authResponse .= $line;
-            if (strpos($authResponse, 'SUCCESS:') !== false || strpos($authResponse, '>INFO:') !== false) break;
-        }
-
-        // Отправляем команду
-        if (@fwrite($socket, "$command\n") === false) {
-            throw new Exception("Failed to send command");
-        }
-
-        // Читаем ответ
-        $response = '';
-        $expectedEnd = strpos($command, 'status') !== false ? "END\r\n" : ">";
-        while (!feof($socket)) {
-            $line = fgets($socket);
-            if ($line === false) break;
-            $response .= $line;
-            if (strpos($response, $expectedEnd) !== false) break;
-        }
-
-        return $response;
-
-    } catch (Exception $e) {
-        error_log("OpenVPN management error ({$server['name']}): " . $e->getMessage());
-        return false;
-    } finally {
-        @fwrite($socket, "quit\n");
-        @fclose($socket);
-    }
-}
-
-function getOpenVPNStatus($server) {
-    // Проверяем, можно ли делать запрос
-    if (!canRequestStatus($server)) {
-        // Возвращаем кэшированные данные или пустой массив
-        return $_SESSION['cached_status'][$server['name']] ?? [];
-    }
-
-    // Обновляем время последнего запроса
-    updateLastRequestTime($server);
-
-    $response = openvpnManagementCommand($server, "status 2");
-    if (!$response) return $_SESSION['cached_status'][$server['name']] ?? [];
-
-    $clients = [];
-    $lines = explode("\n", $response);
-    $in_client_list = false;
-
-    foreach ($lines as $line) {
-        $line = trim($line);
-
-        if (strpos($line, 'HEADER,CLIENT_LIST') === 0) {
-            $in_client_list = true;
-            continue;
-        }
-
-        if (strpos($line, 'HEADER,ROUTING_TABLE') === 0) {
-            $in_client_list = false;
-            continue;
-        }
-
-        if ($in_client_list && strpos($line, 'CLIENT_LIST') === 0) {
-            $parts = explode(',', $line);
-            if (count($parts) >= 9) {
-                $clients[] = [
-                    'name' => $parts[1],
-                    'real_ip' => $parts[2],
-                    'virtual_ip' => $parts[3],
-                    'bytes_received' => formatBytes($parts[5]),
-                    'bytes_sent' => formatBytes($parts[6]),
-                    'connected_since' => $parts[7],
-                    'username' => $parts[8] ?? $parts[1],
-                    'banned' => isClientBanned($server, $parts[1])
-                ];
-            }
-        }
-    }
-
-    // Кэшируем результат
-    $_SESSION['cached_status'][$server['name']] = $clients;
-
-    return $clients;
-}
-
-function getAccountList($server) {
-    $accounts = [];
-    
-    // Получаем список из index.txt (неотозванные сертификаты)
-    if (!empty($server['cert_index']) && file_exists($server['cert_index'])) {
-        $index_content = file_get_contents($server['cert_index']);
-        $lines = explode("\n", $index_content);
-        
-        foreach ($lines as $line) {
-            if (empty(trim($line))) continue;
-            
-            $parts = preg_split('/\s+/', $line);
-            if (count($parts) >= 6 && $parts[0] === 'V') { // Только валидные сертификаты
-                $username = $parts[5];
-                $accounts[$username]["username"] = $username;
-                $accounts[$username]["ip"] = $ip;
-                $accounts[$username]["banned"] = false;
-                if (isClientBanned($server,$username)) { 
-                    $accounts[$username]["banned"] = true;
-                    }
-            }
-        }
-    }
-
-    // Получаем список выданных IP из ipp.txt
-    $assigned_ips = [];
-    if (!empty($server['ipp_file']) && file_exists($server['ipp_file'])) {
-        $ipp_content = file_get_contents($server['ipp_file']);
-        $lines = explode("\n", $ipp_content);
-
-        foreach ($lines as $line) {
-            if (empty(trim($line))) continue;
-
-            $parts = explode(',', $line);
-            if (count($parts) >= 2) {
-                $username = $parts[0];
-                $ip = $parts[1];
-                $accounts[$username]["username"] = $username;
-                $accounts[$username]["ip"] = $ip;
-                $accounts[$username]["banned"] = false;
-                if (isClientBanned($server,$username)) { 
-                    $accounts[$username]["banned"] = true;
-                    }
-            }
-        }
-    }
-
-    return $accounts;
-}
-
-function isClientBanned($server, $client_name) {
-    $ccd_file = "{$server['ccd']}/$client_name";
-    return file_exists($ccd_file) && 
-           preg_match('/^disable$/m', file_get_contents($ccd_file));
-}
-
-function kickClient($server, $client_name) {
-    return openvpnManagementCommand($server, "kill $client_name");
-}
-
-function banClient($server, $client_name) {
-    $ccd_file = "{$server['ccd']}/$client_name";
-    
-    // Добавляем директиву disable
-    $content = file_exists($ccd_file) ? file_get_contents($ccd_file) : '';
-    if (!preg_match('/^disable$/m', $content)) {
-        file_put_contents($ccd_file, $content . "\ndisable\n");
-    }
-
-    // Кикаем клиента
-    kickClient($server, $client_name);
-    return true;
-}
-
-function unbanClient($server, $client_name) {
-    $ccd_file = "{$server['ccd']}/$client_name";
-    if (file_exists($ccd_file)) {
-        $content = file_get_contents($ccd_file);
-        $new_content = preg_replace('/^disable$\n?/m', '', $content);
-        file_put_contents($ccd_file, $new_content);
-        return true;
-    }
-    return false;
-}
-
-function formatBytes($bytes) {
-    $bytes = (int)$bytes;
-    if ($bytes <= 0) return '0 B';
-    $units = ['B', 'KB', 'MB', 'GB', 'TB'];
-    $pow = floor(log($bytes)/log(1024));
-    return round($bytes/pow(1024,$pow),2).' '.$units[$pow];
-}
-
-function getBannedClients($server, $active_clients) {
-    $banned = [];
-    $active_names = array_column($active_clients, 'name');
-    
-    if (is_dir($server['ccd'])) {
-        foreach (scandir($server['ccd']) as $file) {
-            if ($file !== '.' && $file !== '..' && is_file("{$server['ccd']}/$file")) {
-                if (isClientBanned($server, $file) && !in_array($file, $active_names)) {
-                    $banned[] = $file;
-                }
-            }
-        }
-    }
-    
-    return $banned;
-}
-
-function isClientActive($active_clients,$username) {
-    $active_names = array_column($active_clients, 'name');
-    if (!empty($active_names[$username])) { return true; }
-    return false;
-}
-
-// Обработка POST-запросов
-if ($_SERVER['REQUEST_METHOD'] === 'POST') {
-    foreach ($servers as $server_name => $server) {
-        if (isset($_POST["ban-$server_name"])) {
-            banClient($server, $_POST["ban-$server_name"]);
-        } elseif (isset($_POST["unban-$server_name"])) {
-            unbanClient($server, $_POST["unban-$server_name"]);
-        }
-    }
-    header("Location: ".$_SERVER['PHP_SELF']);
-    exit();
-}
 ?>
 
 <!DOCTYPE html>
 <html>
 <head>
     <title><?= htmlspecialchars($page_title) ?></title>
-    <meta http-equiv="refresh" content="<?= REQUEST_INTERVAL ?>">
     <style>
         body { font-family: Arial, sans-serif; margin: 20px; }
         table { border-collapse: collapse; width: 100%; margin-bottom: 20px; }
@@ -309,12 +54,8 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
             display: inline-block;
             margin-bottom: 5px;
         }
-        .spoiler-title:after {
-            content: " ▼";
-        }
-        .spoiler-title.collapsed:after {
-            content: " ►";
-        }
+        .spoiler-title:after { content: " ▼"; }
+        .spoiler-title.collapsed:after { content: " ►"; }
         .spoiler-content { 
             display: none; 
             padding: 10px; 
@@ -323,10 +64,98 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
             background-color: #f9f9f9; 
             border-radius: 3px;
         }
+        .loading { color: #666; font-style: italic; }
+        .last-update { font-size: 0.8em; color: #666; margin-top: 5px; }
     </style>
+</head>
+<body>
+    <h1><?= htmlspecialchars($page_title) ?></h1>
+    
+    <div id="server-container">
+        <?php foreach ($servers as $server_name => $server): ?>
+        <div class="server-section" id="server-<?= htmlspecialchars($server_name) ?>">
+            <h2><?= htmlspecialchars($server['title']) ?></h2>
+            <div class="loading">Loading data...</div>
+        </div>
+        <?php endforeach; ?>
+    </div>
+
     <script>
+        // Функция для загрузки данных сервера
+        function loadServerData(serverName) {
+            const serverElement = document.getElementById(`server-${serverName}`);
+            
+            fetch(`get_server_data.php?server=${serverName}&csrf=<?= $_SESSION['csrf_token'] ?>`,{
+		    headers: {
+			'X-Requested-With': 'XMLHttpRequest'
+		    }
+		})
+                .then(response => response.text())
+                .then(html => {
+                    serverElement.innerHTML = html;
+                    // Обновляем данные каждые 60 секунд
+                    setTimeout(() => loadServerData(serverName), 60000);
+                })
+                .catch(error => {
+                    serverElement.querySelector('.loading').textContent = 'Error loading data';
+                    console.error('Error:', error);
+                    // Повторяем попытку через 10 секунд при ошибке
+                    setTimeout(() => loadServerData(serverName), 10000);
+                });
+        }
+
+        // Загружаем данные для всех серверов
+        document.addEventListener('DOMContentLoaded', function() {
+            <?php foreach ($servers as $server_name => $server): ?>
+                loadServerData('<?= $server_name ?>');
+            <?php endforeach; ?>
+        });
+
+        // Функция для обработки действий (ban/unban)
+        function handleAction(serverName, action, clientName) {
+
+            const params = new URLSearchParams();
+            params.append('server', serverName);
+            params.append('action', action);
+            params.append('client', clientName);
+
+            fetch('handle_action.php', {
+                method: 'POST',
+                headers: {
+                    'Content-Type': 'application/x-www-form-urlencoded',
+                    'X-Requested-With': 'XMLHttpRequest'
+                },
+                body: params
+            })
+            .then(response => {
+                // 2. Проверяем статус ответа
+                if (!response.ok) {
+                    throw new Error(`Server returned ${response.status} status`);
+                }
+                return response.json();
+            })
+            .then(data => {
+                // 3. Проверяем структуру ответа
+                if (!data || typeof data.success === 'undefined') {
+                    throw new Error('Invalid server response');
+                }
+                if (data.success) {
+                    loadServerData(serverName);
+                } else {
+                    console.error('Server error:', data.message);
+                    alert(`Error: ${data.message || 'Operation failed'}`);
+                }
+            })
+            .catch(error => {
+                // 4. Правильное отображение ошибки
+                console.error('Request failed:', error);
+                alert(`Request failed: ${error.message}`);
+            });
+        }
+
+        // Функция для переключения спойлера
         function toggleSpoiler(button) {
-            var content = button.nextElementSibling;
+            const content = button.nextElementSibling;
             if (content.style.display === "block") {
                 content.style.display = "none";
                 button.classList.add('collapsed');
@@ -336,108 +165,5 @@ if ($_SERVER['REQUEST_METHOD'] === 'POST') {
             }
         }
     </script>
-</head>
-<body>
-    <h1><?= htmlspecialchars($page_title) ?></h1>
-    
-    <form method="post">
-    <?php foreach ($servers as $server_name => $server): 
-        $clients = getOpenVPNStatus($server);
-        $banned_clients = getBannedClients($server, $clients);
-        $accounts = getAccountList($server);
-    ?>
-    <div class="server-section">
-        <h2><?= htmlspecialchars($server['title']) ?></h2>
-        
-        <div class="section">
-            <h3>Active Connections</h3>
-            <table>
-                <thead>
-                    <tr>
-                        <th>Client</th>
-                        <th>Real IP</th>
-                        <th>Virtual IP</th>
-                        <th>Traffic</th>
-                        <th>Connected</th>
-                        <th>Status</th>
-                        <th>Actions</th>
-                    </tr>
-                </thead>
-                <tbody>
-                    <?php foreach ($clients as $client): ?>
-                    <tr class="<?= $client['banned'] ? 'banned' : '' ?>">
-                        <td><?= htmlspecialchars($client['name']) ?></td>
-                        <td><?= htmlspecialchars($client['real_ip']) ?></td>
-                        <td><?= htmlspecialchars($client['virtual_ip']) ?></td>
-                        <td>↓<?= $client['bytes_received'] ?> ↑<?= $client['bytes_sent'] ?></td>
-                        <td><?= htmlspecialchars($client['connected_since']) ?></td>
-                        <td>
-                            <span class="status-badge <?= $client['banned'] ? 'status-banned' : 'status-active' ?>">
-                                <?= $client['banned'] ? 'BANNED' : 'Active' ?>
-                            </span>
-                        </td>
-                        <td class="actions">
-                            <?php if ($client['banned']): ?>
-                                <button type="submit" name="unban-<?= $server_name ?>" value="<?= htmlspecialchars($client['name']) ?>" 
-                                        class="btn unban-btn">Unban</button>
-                            <?php else: ?>
-                                <button type="submit" name="ban-<?= $server_name ?>" value="<?= htmlspecialchars($client['name']) ?>" 
-                                        class="btn ban-btn">Ban</button>
-                            <?php endif; ?>
-                        </td>
-                    </tr>
-                    <?php endforeach; ?>
-                </tbody>
-            </table>
-        </div>
-
-        <div class="section">
-            <div class="spoiler">
-                <div class="spoiler-title collapsed" onclick="toggleSpoiler(this)">
-                    Account List (<?= count($accounts) ?>)
-                </div>
-                <div class="spoiler-content">
-                    <table>
-                        <thead>
-                            <tr>
-                                <th>Account</th>
-                                <th>Assigned IP</th>
-                                <th>Status</th>
-                                <th>Actions</th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            <?php foreach ($accounts as $account): ?>
-                            <tr>
-                                <td><?= htmlspecialchars($account["username"]) ?></td>
-                                <td><?= htmlspecialchars($account['ip'] ?? 'N/A') ?></td>
-                                <td>
-                                    <span class="status-badge <?= $account['banned'] ? 'status-banned' : 'status-active' ?>">
-                                        <?= $account['banned'] ? 'BANNED' : 'ENABLED' ?>
-                                    </span>
-                                </td>
-                                <td class="actions">
-                                    <?php if ($account['banned']): ?>
-                                        <button type="submit" name="unban-<?= $server_name ?>" value="<?= htmlspecialchars($account['username']) ?>" 
-                                                class="btn unban-btn">Unban</button>
-                                    <?php else: ?>
-                                        <button type="submit" name="ban-<?= $server_name ?>" value="<?= htmlspecialchars($account['username']) ?>" 
-                                                class="btn ban-btn">Ban</button>
-                                    <?php endif; ?>
-                                </td>
-                            </tr>
-                            <?php endforeach; ?>
-                        </tbody>
-                    </table>
-                </div>
-            </div>
-        </div>
-    </div>
-
-    <p>Next update in: <?= REQUEST_INTERVAL - (time() - $_SESSION['last_request_time'][$server['name']]) ?> seconds</p>
-
-    <?php endforeach; ?>
-    </form>
 </body>
 </html>
-