feat: Add online clients tracking and display on dashboard and server views

This commit is contained in:
infosave2007
2026-01-30 21:07:30 +03:00
parent 28a6de5697
commit 66bd218aec
6 changed files with 182 additions and 13 deletions
+1
View File
@@ -66,3 +66,4 @@ restore_local.php
test_protocols.php test_protocols.php
scripts/regen_qr.php scripts/regen_qr.php
scripts/test_xray_install.sh scripts/test_xray_install.sh
scripts/test_online.php
+154 -6
View File
@@ -525,16 +525,18 @@ class ServerMonitoring
private function execSSH(string $cmd): ?string private function execSSH(string $cmd): ?string
{ {
$host = $this->serverData['host']; $host = $this->serverData['host'];
$port = $this->serverData['port']; $port = (int)$this->serverData['port'];
$username = $this->serverData['username']; $username = $this->serverData['username'];
$password = $this->serverData['password']; $password = $this->serverData['password'];
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null';
$sshCmd = sprintf( $sshCmd = sprintf(
'sshpass -p %s ssh -o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -p %d %s@%s %s 2>/dev/null', "sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null",
escapeshellarg($password), $password,
$port, $port,
escapeshellarg($username), $sshOptions,
escapeshellarg($host), $username,
$host,
escapeshellarg($cmd) escapeshellarg($cmd)
); );
@@ -628,12 +630,158 @@ class ServerMonitoring
} }
} }
// Block collected IPs // Update blocking rules
if (!empty($ipsToBlock)) { if (!empty($ipsToBlock)) {
// Block collected IPs (with -reset to replace existing rule)
$ipList = implode(' ', array_unique($ipsToBlock)); $ipList = implode(' ', array_unique($ipsToBlock));
$blockCmd = "docker exec $xrayContainer xray api sib --server=127.0.0.1:10085 -outbound=blocked -inbound=vless-in -reset $ipList"; $blockCmd = "docker exec $xrayContainer xray api sib --server=127.0.0.1:10085 -outbound=blocked -inbound=vless-in -reset $ipList";
$this->execSSH($blockCmd); $this->execSSH($blockCmd);
error_log("[Xray Enforcement] Blocked IPs: $ipList"); error_log("[Xray Enforcement] Blocked IPs: $ipList");
} else {
// No IPs to block - remove the blocking rule if it exists
$rmCmd = "docker exec $xrayContainer xray api rmrules --server=127.0.0.1:10085 sourceIpBlock 2>/dev/null || true";
$this->execSSH($rmCmd);
} }
} }
/**
* Count total online clients across all Xray servers
* Returns array with 'total' count and 'users' list
*/
public static function countOnlineClients(): array
{
$result = ['total' => 0, 'users' => []];
// Get all active servers
$servers = VpnServer::listAll();
foreach ($servers as $serverData) {
// Check if this is an Xray server
$containerName = $serverData['container_name'] ?? '';
if (strpos($containerName, 'xray') === false) {
continue;
}
// Build SSH command
$host = $serverData['host'];
$port = (int)($serverData['port'] ?? 22);
$username = $serverData['username'] ?? 'root';
$password = $serverData['password'] ?? '';
$xrayContainer = $containerName ?: 'amnezia-xray';
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5';
$sshCmd = sprintf(
"sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null",
$password,
$port,
$sshOptions,
$username,
$host,
escapeshellarg($cmd)
);
$output = shell_exec($sshCmd);
if (!$output) {
continue;
}
$data = json_decode($output, true);
if (!isset($data['users']) || !is_array($data['users'])) {
continue;
}
foreach ($data['users'] as $user) {
// Parse format: "user>>>email>>>online" or object with email/count
if (is_string($user)) {
// Format: "user>>>olegtest3>>>online"
$parts = explode('>>>', $user);
if (count($parts) >= 2) {
$email = $parts[1];
$result['total'] += 1;
$result['users'][] = [
'server_id' => $serverData['id'],
'email' => $email,
'count' => 1
];
}
} else {
// Object format
$email = $user['email'] ?? 'unknown';
$count = (int)($user['count'] ?? 1);
$result['total'] += $count;
$result['users'][] = [
'server_id' => $serverData['id'],
'email' => $email,
'count' => $count
];
}
}
}
return $result;
}
/**
* Get online clients for a specific server
* Returns array of online client logins/emails
*/
public static function getOnlineClientsForServer(array $serverData): array
{
$result = [];
// Check if this is an Xray server
$containerName = $serverData['container_name'] ?? '';
if (strpos($containerName, 'xray') === false) {
return $result;
}
// Build SSH command
$host = $serverData['host'];
$port = (int)($serverData['port'] ?? 22);
$username = $serverData['username'] ?? 'root';
$password = $serverData['password'] ?? '';
$xrayContainer = $containerName ?: 'amnezia-xray';
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5';
$sshCmd = sprintf(
"sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null",
$password,
$port,
$sshOptions,
$username,
$host,
escapeshellarg($cmd)
);
$output = shell_exec($sshCmd);
if (!$output) {
return $result;
}
$data = json_decode($output, true);
if (!isset($data['users']) || !is_array($data['users'])) {
return $result;
}
foreach ($data['users'] as $user) {
// Parse format: "user>>>email>>>online"
if (is_string($user)) {
$parts = explode('>>>', $user);
if (count($parts) >= 2) {
$result[] = $parts[1];
}
} else {
$email = $user['email'] ?? null;
if ($email) {
$result[] = $email;
}
}
}
return $result;
}
} }
@@ -0,0 +1,9 @@
-- Add translation for dashboard.online_now
INSERT INTO translations (`locale`, `category`, `key_name`, `translation`) VALUES
('en', 'dashboard', 'online_now', 'Online Now'),
('ru', 'dashboard', 'online_now', 'Сейчас онлайн'),
('es', 'dashboard', 'online_now', 'En línea ahora'),
('de', 'dashboard', 'online_now', 'Jetzt online'),
('fr', 'dashboard', 'online_now', 'En ligne maintenant'),
('zh', 'dashboard', 'online_now', '当前在线')
ON DUPLICATE KEY UPDATE `translation` = VALUES(`translation`);
+9
View File
@@ -265,9 +265,14 @@ Router::get('/dashboard', function () {
// Get user's clients // Get user's clients
$clients = VpnClient::listByUser($user['id']); $clients = VpnClient::listByUser($user['id']);
// Get real-time online clients count from Xray API
$onlineData = ServerMonitoring::countOnlineClients();
View::render('dashboard.twig', [ View::render('dashboard.twig', [
'servers' => $servers, 'servers' => $servers,
'clients' => $clients, 'clients' => $clients,
'online_count' => $onlineData['total'],
'online_users' => $onlineData['users'],
]); ]);
}); });
@@ -845,6 +850,9 @@ Router::get('/servers/{id}', function ($params) {
} }
} }
// Get online clients for this server (Xray)
$onlineLogins = ServerMonitoring::getOnlineClientsForServer($serverData);
View::render('servers/view.twig', [ View::render('servers/view.twig', [
'server' => $serverData, 'server' => $serverData,
'clients' => $clients, 'clients' => $clients,
@@ -852,6 +860,7 @@ Router::get('/servers/{id}', function ($params) {
'server_protocols' => $serverProtocols, 'server_protocols' => $serverProtocols,
'selected_protocol_id' => $selectedProtocolId, 'selected_protocol_id' => $selectedProtocolId,
'available_protocols' => $availableProtocols, 'available_protocols' => $availableProtocols,
'online_logins' => $onlineLogins,
]); ]);
} catch (Exception $e) { } catch (Exception $e) {
error_log('Server view error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine()); error_log('Server view error: ' . $e->getMessage() . ' at ' . $e->getFile() . ':' . $e->getLine());
+4 -4
View File
@@ -40,13 +40,13 @@
<div class="bg-white rounded-lg shadow p-6"> <div class="bg-white rounded-lg shadow p-6">
<div class="flex items-center"> <div class="flex items-center">
<div class="p-3 rounded-full bg-purple-100 text-purple-600"> <div class="p-3 rounded-full bg-green-100 text-green-600">
<i class="fas fa-check-circle text-2xl"></i> <i class="fas fa-wifi text-2xl"></i>
</div> </div>
<div class="ml-4"> <div class="ml-4">
<p class="text-sm font-medium text-gray-600">{{ t('dashboard.active_clients') }}</p> <p class="text-sm font-medium text-gray-600">{{ t('dashboard.online_now') }}</p>
<p class="text-2xl font-bold text-gray-900"> <p class="text-2xl font-bold text-gray-900">
{{ servers|filter(s => s.status == 'active')|length }} {{ online_count|default(0) }}
</p> </p>
</div> </div>
</div> </div>
+5 -3
View File
@@ -267,7 +267,7 @@
{% for client in clients %} {% for client in clients %}
<tr class="border-t"> <tr class="border-t">
<td class="px-6 py-4">{{ client.name }}</td> <td class="px-6 py-4">{{ client.name }}</td>
<td class="px-6 py-4">{{ client.login|default('-') }}</td> <td class="px-6 py-4">{{ client.name }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{% if client.protocol_name %} {% if client.protocol_name %}
<span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{{ client.protocol_name }}</span> <span class="px-2 py-1 bg-blue-100 text-blue-800 rounded text-xs">{{ client.protocol_name }}</span>
@@ -277,8 +277,10 @@
</td> </td>
<td class="px-6 py-4">{{ client.client_ip }}</td> <td class="px-6 py-4">{{ client.client_ip }}</td>
<td class="px-6 py-4"> <td class="px-6 py-4">
{% if client.status == 'active' %} {% if client.name in online_logins %}
<span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs">{{ t('status.active') }}</span> <span class="px-2 py-1 bg-green-100 text-green-800 rounded text-xs"><i class="fas fa-wifi mr-1"></i>Online</span>
{% elseif client.status == 'active' %}
<span class="px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{{ t('status.active') }}</span>
{% else %} {% else %}
<span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span> <span class="px-2 py-1 bg-red-100 text-red-800 rounded text-xs">{{ t('status.disabled') }}</span>
{% endif %} {% endif %}