feat: Enhance online client tracking by including recent handshake counts for WireGuard/AWG
This commit is contained in:
@@ -67,3 +67,4 @@ 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
|
scripts/test_online.php
|
||||||
|
API_AWG_DOCS.md
|
||||||
|
|||||||
+144
-93
@@ -27,19 +27,11 @@ class ServerMonitoring
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
$containerName = $this->serverData['container_name'];
|
// Always try to fetch from amnezia-xray container
|
||||||
if (strpos($containerName, 'xray') === false) {
|
// Even if server's container_name is different, there may be xray clients
|
||||||
$this->xrayStatsFetched = true;
|
$xrayContainer = $this->getXrayContainerName() ?? 'amnezia-xray';
|
||||||
return true;
|
|
||||||
}
|
$cmd = "docker exec $xrayContainer xray api statsquery --pattern 'user>>>' --reset=true --server=127.0.0.1:10085 2>/dev/null";
|
||||||
|
|
||||||
// Use --reset=true to get delta since last check and prevent counter reset on restart
|
|
||||||
$xrayContainer = $this->getXrayContainerName();
|
|
||||||
if (!$xrayContainer) {
|
|
||||||
$this->xrayStatsFetched = true;
|
|
||||||
return true; // Not an Xray server
|
|
||||||
}
|
|
||||||
$cmd = "docker exec $xrayContainer xray api statsquery --pattern 'user>>>' --reset=true --server=127.0.0.1:10085";
|
|
||||||
$json = $this->execSSH($cmd);
|
$json = $this->execSSH($cmd);
|
||||||
|
|
||||||
if (!$json || trim($json) === '') {
|
if (!$json || trim($json) === '') {
|
||||||
@@ -274,24 +266,40 @@ class ServerMonitoring
|
|||||||
$containerName = $this->serverData['container_name'];
|
$containerName = $this->serverData['container_name'];
|
||||||
$bytesReceived = 0;
|
$bytesReceived = 0;
|
||||||
$bytesSent = 0;
|
$bytesSent = 0;
|
||||||
|
$speedUp = 0;
|
||||||
|
$speedDown = 0;
|
||||||
|
|
||||||
$protocol = $this->serverData['install_protocol'] ?? '';
|
// Determine if this client is XRay based on protocol_id
|
||||||
|
$isXrayClient = false;
|
||||||
|
if (!empty($client['protocol_id'])) {
|
||||||
|
$stmtProto = $db->prepare('SELECT slug FROM protocols WHERE id = ?');
|
||||||
|
$stmtProto->execute([$client['protocol_id']]);
|
||||||
|
$protoData = $stmtProto->fetch();
|
||||||
|
if ($protoData && stripos($protoData['slug'], 'xray') !== false) {
|
||||||
|
$isXrayClient = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check config for vless URI
|
||||||
|
if (!$isXrayClient && !empty($client['config']) && strpos($client['config'], 'vless://') !== false) {
|
||||||
|
$isXrayClient = true;
|
||||||
|
}
|
||||||
|
|
||||||
if (strpos($protocol, 'xray') !== false || strpos($protocol, 'vless') !== false) {
|
if ($isXrayClient) {
|
||||||
// Retrieve DELTA from cache
|
// Retrieve DELTA from cache
|
||||||
if ($this->xrayStatsFetched) {
|
if ($this->xrayStatsFetched) {
|
||||||
// Try to find by UUID first (if we tracked it) or Email/Name
|
// Try name first (matches email in xray config), then UUID from config
|
||||||
// Our cache is keyed by "email" from the stats query "user>>>email>>>..."
|
$key = $client['name'];
|
||||||
// In VpnClient.php, the X-ray config uses client 'id' (uuid) as 'id' and 'email' as 'email'.
|
|
||||||
// Usually Amnezia sets email = uuid or name.
|
|
||||||
// Let's try keys: client['id'], client['name'], client['email'] (if exists)
|
|
||||||
|
|
||||||
// In our previous fetchXrayStats, we keyed by $parts[1].
|
|
||||||
|
|
||||||
$key = $client['id']; // UUID
|
|
||||||
if (!isset($this->xrayStatsCache[$key])) {
|
if (!isset($this->xrayStatsCache[$key])) {
|
||||||
// Try name
|
// Try UUID from config
|
||||||
$key = $client['name'];
|
if (!empty($client['config']) && preg_match('/vless:\/\/([0-9a-fA-F-]{36})@/i', $client['config'], $m)) {
|
||||||
|
$key = $m[1];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!isset($this->xrayStatsCache[$key])) {
|
||||||
|
// Try client['id'] as last resort
|
||||||
|
$key = $client['id'];
|
||||||
}
|
}
|
||||||
|
|
||||||
if (isset($this->xrayStatsCache[$key])) {
|
if (isset($this->xrayStatsCache[$key])) {
|
||||||
@@ -307,32 +315,42 @@ class ServerMonitoring
|
|||||||
$bytesReceived = ($currentDbStats['bytes_received'] ?? 0) + (int) $xStats['down'];
|
$bytesReceived = ($currentDbStats['bytes_received'] ?? 0) + (int) $xStats['down'];
|
||||||
|
|
||||||
// Calculate speed based on DELTA (since Reset=true, value IS the delta since last check)
|
// Calculate speed based on DELTA (since Reset=true, value IS the delta since last check)
|
||||||
// If we check every 60s, speed = delta / 60.
|
// Assuming cron runs every minute (60s):
|
||||||
// But exact interval varies.
|
|
||||||
// For now, let's trust the delta.
|
|
||||||
|
|
||||||
// Simple speed aproximation: Delta / (Now - LastCheck)
|
|
||||||
// But we don't have exact LastCheck time per client easily here.
|
|
||||||
// However, sparklines use a separate API.
|
|
||||||
// The 'speed_up'/'speed_down' columns in DB are usually "Current Speed".
|
|
||||||
// If we just gathered a delta over X seconds...
|
|
||||||
// Let's approximate: X-ray stats delta.
|
|
||||||
// We can just store the 'current speed' as calculated by (Delta Bytes / Interval).
|
|
||||||
// But we don't know the exact interval since the LAST fetch was run by the cron.
|
|
||||||
// Assuming cron runs every minute?
|
|
||||||
// If we assume 1 minute (60s):
|
|
||||||
$speedUp = round($xStats['up'] / 60);
|
$speedUp = round($xStats['up'] / 60);
|
||||||
$speedDown = round($xStats['down'] / 60);
|
$speedDown = round($xStats['down'] / 60);
|
||||||
|
} else {
|
||||||
|
// No stats in cache, use current DB values
|
||||||
|
$stmt = $db->prepare("SELECT bytes_sent, bytes_received FROM vpn_clients WHERE id = ?");
|
||||||
|
$stmt->execute([$client['id']]);
|
||||||
|
$currentDbStats = $stmt->fetch(PDO::FETCH_ASSOC);
|
||||||
|
$bytesSent = $currentDbStats['bytes_sent'] ?? 0;
|
||||||
|
$bytesReceived = $currentDbStats['bytes_received'] ?? 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// WireGuard Logic
|
// WireGuard Logic - get bytes and handshake timestamp
|
||||||
$publicKey = $client['public_key'];
|
$publicKey = $client['public_key'];
|
||||||
$cmd = "docker exec {$containerName} wg show all dump | grep '{$publicKey}' | awk '{print \$6, \$7}'";
|
// wg show all dump format (tab-separated):
|
||||||
|
// $1=interface $2=pubkey $3=psk $4=endpoint $5=allowed-ips $6=latest-handshake $7=rx-bytes $8=tx-bytes $9=keepalive
|
||||||
|
// rx-bytes = bytes received by server = client's upload (bytes_sent)
|
||||||
|
// tx-bytes = bytes transmitted by server = client's download (bytes_received)
|
||||||
|
$cmd = "docker exec {$containerName} wg show all dump | grep '{$publicKey}' | awk '{print \$6, \$7, \$8}'";
|
||||||
$result = $this->execSSH($cmd);
|
$result = $this->execSSH($cmd);
|
||||||
|
|
||||||
if ($result) {
|
if ($result) {
|
||||||
list($bytesReceived, $bytesSent) = explode(' ', trim($result));
|
$parts = explode(' ', trim($result));
|
||||||
|
if (count($parts) >= 3) {
|
||||||
|
$handshakeTs = (int)$parts[0];
|
||||||
|
$bytesSent = (int)$parts[1]; // server's rx = client's upload
|
||||||
|
$bytesReceived = (int)$parts[2]; // server's tx = client's download
|
||||||
|
|
||||||
|
// Update last_handshake if there was a recent handshake
|
||||||
|
if ($handshakeTs > 0) {
|
||||||
|
$handshakeDate = date('Y-m-d H:i:s', $handshakeTs);
|
||||||
|
$stmtHs = $db->prepare("UPDATE vpn_clients SET last_handshake = ? WHERE id = ?");
|
||||||
|
$stmtHs->execute([$handshakeDate, $client['id']]);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -425,10 +443,10 @@ class ServerMonitoring
|
|||||||
$stats['speed_down_kbps'],
|
$stats['speed_down_kbps'],
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Update vpn_clients table with latest stats
|
// Update vpn_clients table with latest stats (don't touch last_handshake - it's set separately for WG/AWG)
|
||||||
$stmt = $db->prepare("
|
$stmt = $db->prepare("
|
||||||
UPDATE vpn_clients
|
UPDATE vpn_clients
|
||||||
SET bytes_sent = ?, bytes_received = ?, speed_up = ?, speed_down = ?, current_speed = ?, last_handshake = NOW(), last_sync_at = NOW()
|
SET bytes_sent = ?, bytes_received = ?, speed_up = ?, speed_down = ?, current_speed = ?, last_sync_at = NOW()
|
||||||
WHERE id = ?
|
WHERE id = ?
|
||||||
");
|
");
|
||||||
|
|
||||||
@@ -785,11 +803,23 @@ class ServerMonitoring
|
|||||||
|
|
||||||
// Get all active servers
|
// Get all active servers
|
||||||
$servers = VpnServer::listAll();
|
$servers = VpnServer::listAll();
|
||||||
|
$db = DB::conn();
|
||||||
|
|
||||||
foreach ($servers as $serverData) {
|
foreach ($servers as $serverData) {
|
||||||
// Check if this is an Xray server
|
// Check if this server has any XRay clients
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT COUNT(*) as cnt FROM vpn_clients vc
|
||||||
|
JOIN protocols p ON vc.protocol_id = p.id
|
||||||
|
WHERE vc.server_id = ? AND p.slug LIKE '%xray%'
|
||||||
|
");
|
||||||
|
$stmt->execute([$serverData['id']]);
|
||||||
|
$hasXrayClients = (int)$stmt->fetchColumn() > 0;
|
||||||
|
|
||||||
|
// Also check container_name as fallback
|
||||||
$containerName = $serverData['container_name'] ?? '';
|
$containerName = $serverData['container_name'] ?? '';
|
||||||
if (strpos($containerName, 'xray') === false) {
|
$isXrayServer = strpos($containerName, 'xray') !== false;
|
||||||
|
|
||||||
|
if (!$hasXrayClients && !$isXrayServer) {
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -799,7 +829,7 @@ class ServerMonitoring
|
|||||||
$username = $serverData['username'] ?? 'root';
|
$username = $serverData['username'] ?? 'root';
|
||||||
$password = $serverData['password'] ?? '';
|
$password = $serverData['password'] ?? '';
|
||||||
|
|
||||||
$xrayContainer = $containerName ?: 'amnezia-xray';
|
$xrayContainer = $isXrayServer ? $containerName : 'amnezia-xray';
|
||||||
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
|
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
|
||||||
|
|
||||||
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5';
|
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5';
|
||||||
@@ -861,58 +891,79 @@ class ServerMonitoring
|
|||||||
public static function getOnlineClientsForServer(array $serverData): array
|
public static function getOnlineClientsForServer(array $serverData): array
|
||||||
{
|
{
|
||||||
$result = [];
|
$result = [];
|
||||||
|
$db = DB::conn();
|
||||||
|
|
||||||
|
// 1. Get XRay online clients from Xray API
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT COUNT(*) as cnt FROM vpn_clients vc
|
||||||
|
JOIN protocols p ON vc.protocol_id = p.id
|
||||||
|
WHERE vc.server_id = ? AND p.slug LIKE '%xray%'
|
||||||
|
");
|
||||||
|
$stmt->execute([$serverData['id']]);
|
||||||
|
$hasXrayClients = (int)$stmt->fetchColumn() > 0;
|
||||||
|
|
||||||
// Check if this is an Xray server
|
|
||||||
$containerName = $serverData['container_name'] ?? '';
|
$containerName = $serverData['container_name'] ?? '';
|
||||||
if (strpos($containerName, 'xray') === false) {
|
$isXrayServer = strpos($containerName, 'xray') !== false;
|
||||||
return $result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build SSH command
|
if ($hasXrayClients || $isXrayServer) {
|
||||||
$host = $serverData['host'];
|
$host = $serverData['host'];
|
||||||
$port = (int)($serverData['port'] ?? 22);
|
$port = (int)($serverData['port'] ?? 22);
|
||||||
$username = $serverData['username'] ?? 'root';
|
$username = $serverData['username'] ?? 'root';
|
||||||
$password = $serverData['password'] ?? '';
|
$password = $serverData['password'] ?? '';
|
||||||
|
|
||||||
$xrayContainer = $containerName ?: 'amnezia-xray';
|
$xrayContainer = $isXrayServer ? $containerName : 'amnezia-xray';
|
||||||
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
|
$cmd = "docker exec $xrayContainer xray api statsgetallonlineusers --server=127.0.0.1:10085";
|
||||||
|
|
||||||
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5';
|
$sshOptions = '-o StrictHostKeyChecking=no -o UserKnownHostsFile=/dev/null -o ConnectTimeout=5';
|
||||||
$sshCmd = sprintf(
|
$sshCmd = sprintf(
|
||||||
"sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null",
|
"sshpass -p '%s' ssh -p %d %s %s@%s %s 2>/dev/null",
|
||||||
$password,
|
$password,
|
||||||
$port,
|
$port,
|
||||||
$sshOptions,
|
$sshOptions,
|
||||||
$username,
|
$username,
|
||||||
$host,
|
$host,
|
||||||
escapeshellarg($cmd)
|
escapeshellarg($cmd)
|
||||||
);
|
);
|
||||||
|
|
||||||
$output = shell_exec($sshCmd);
|
$output = shell_exec($sshCmd);
|
||||||
if (!$output) {
|
if ($output) {
|
||||||
return $result;
|
$data = json_decode($output, true);
|
||||||
}
|
if (isset($data['users']) && is_array($data['users'])) {
|
||||||
|
foreach ($data['users'] as $user) {
|
||||||
$data = json_decode($output, true);
|
if (is_string($user)) {
|
||||||
if (!isset($data['users']) || !is_array($data['users'])) {
|
$parts = explode('>>>', $user);
|
||||||
return $result;
|
if (count($parts) >= 2) {
|
||||||
}
|
$result[] = $parts[1];
|
||||||
|
}
|
||||||
foreach ($data['users'] as $user) {
|
} else {
|
||||||
// Parse format: "user>>>email>>>online"
|
$email = $user['email'] ?? null;
|
||||||
if (is_string($user)) {
|
if ($email) {
|
||||||
$parts = explode('>>>', $user);
|
$result[] = $email;
|
||||||
if (count($parts) >= 2) {
|
}
|
||||||
$result[] = $parts[1];
|
}
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
$email = $user['email'] ?? null;
|
|
||||||
if ($email) {
|
|
||||||
$result[] = $email;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 2. Add WireGuard/AWG clients with recent handshake (< 5 minutes)
|
||||||
|
// Exclude XRay clients - they use Xray API for online status
|
||||||
|
$stmt = $db->prepare("
|
||||||
|
SELECT vc.name FROM vpn_clients vc
|
||||||
|
LEFT JOIN protocols p ON vc.protocol_id = p.id
|
||||||
|
WHERE vc.server_id = ?
|
||||||
|
AND vc.status = 'active'
|
||||||
|
AND vc.last_handshake IS NOT NULL
|
||||||
|
AND vc.last_handshake >= DATE_SUB(NOW(), INTERVAL 300 SECOND)
|
||||||
|
AND (p.slug IS NULL OR p.slug NOT LIKE '%xray%')
|
||||||
|
");
|
||||||
|
$stmt->execute([$serverData['id']]);
|
||||||
|
while ($row = $stmt->fetch(PDO::FETCH_ASSOC)) {
|
||||||
|
if (!in_array($row['name'], $result)) {
|
||||||
|
$result[] = $row['name'];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return $result;
|
return $result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+46
-9
@@ -1551,21 +1551,58 @@ class VpnClient
|
|||||||
// XRay stats logic
|
// XRay stats logic
|
||||||
$stats = [];
|
$stats = [];
|
||||||
|
|
||||||
// Heuristic: if container name contains 'xray' or protocol slug suggests xray
|
// Determine protocol by client's protocol_id
|
||||||
$containerName = $serverData['container_name'] ?? '';
|
$isXray = false;
|
||||||
// Or better: try to detect protocol from config if container name is vague (but usually amnezia-xray)
|
$xrayContainerName = 'amnezia-xray'; // Default XRay container name
|
||||||
|
|
||||||
|
if (!empty($this->data['protocol_id'])) {
|
||||||
|
$stmtProto = $pdo->prepare('SELECT slug FROM protocols WHERE id = ?');
|
||||||
|
$stmtProto->execute([$this->data['protocol_id']]);
|
||||||
|
$protoData = $stmtProto->fetch();
|
||||||
|
if ($protoData && stripos($protoData['slug'], 'xray') !== false) {
|
||||||
|
$isXray = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: check container_name or config for xray indicators
|
||||||
|
if (!$isXray) {
|
||||||
|
$containerName = $serverData['container_name'] ?? '';
|
||||||
|
if (strpos($containerName, 'xray') !== false) {
|
||||||
|
$isXray = true;
|
||||||
|
$xrayContainerName = $containerName;
|
||||||
|
} elseif (!empty($this->data['config']) && strpos($this->data['config'], 'vless://') !== false) {
|
||||||
|
$isXray = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (strpos($containerName, 'xray') !== false) {
|
if ($isXray) {
|
||||||
// Extract UUID from config for XRay (vless://UUID@...)
|
// XRay stats are tracked by email field in xray config
|
||||||
|
// Try client name first (typically used as email), then UUID from config as fallback
|
||||||
$identifier = null;
|
$identifier = null;
|
||||||
|
$uuid = null;
|
||||||
|
|
||||||
|
// Extract UUID from config
|
||||||
if (!empty($this->data['config']) && preg_match('/vless:\/\/([0-9a-fA-F-]{36})@/i', $this->data['config'], $m)) {
|
if (!empty($this->data['config']) && preg_match('/vless:\/\/([0-9a-fA-F-]{36})@/i', $this->data['config'], $m)) {
|
||||||
$identifier = $m[1];
|
$uuid = $m[1];
|
||||||
} elseif (!empty($this->data['name'])) {
|
}
|
||||||
|
|
||||||
|
// Override container_name for XRay stats
|
||||||
|
$xrayServerData = $serverData;
|
||||||
|
$xrayServerData['container_name'] = $xrayContainerName;
|
||||||
|
|
||||||
|
// Try name first (typically matches email in xray config)
|
||||||
|
if (!empty($this->data['name'])) {
|
||||||
$identifier = $this->data['name'];
|
$identifier = $this->data['name'];
|
||||||
|
$stats = self::getXrayStats($xrayServerData, $identifier);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If no stats found by name, try UUID
|
||||||
|
if ((empty($stats) || ($stats['bytes_sent'] == 0 && $stats['bytes_received'] == 0)) && $uuid) {
|
||||||
|
$identifier = $uuid;
|
||||||
|
$stats = self::getXrayStats($xrayServerData, $identifier);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($identifier) {
|
if ($identifier && !empty($stats)) {
|
||||||
$stats = self::getXrayStats($serverData, $identifier);
|
|
||||||
// Infer online status for XRay: if traffic increased, they are online.
|
// Infer online status for XRay: if traffic increased, they are online.
|
||||||
// Update last_handshake to NOW() if activity detected.
|
// Update last_handshake to NOW() if activity detected.
|
||||||
if ($stats['bytes_sent'] > $prevSent || $stats['bytes_received'] > $prevReceived) {
|
if ($stats['bytes_sent'] > $prevSent || $stats['bytes_received'] > $prevReceived) {
|
||||||
|
|||||||
+14
-1
@@ -267,11 +267,24 @@ Router::get('/dashboard', function () {
|
|||||||
|
|
||||||
// Get real-time online clients count from Xray API
|
// Get real-time online clients count from Xray API
|
||||||
$onlineData = ServerMonitoring::countOnlineClients();
|
$onlineData = ServerMonitoring::countOnlineClients();
|
||||||
|
|
||||||
|
// Also count clients with recent handshake (within 5 minutes) for WireGuard/AWG
|
||||||
|
$pdo = DB::conn();
|
||||||
|
$stmt = $pdo->query("
|
||||||
|
SELECT COUNT(*) as cnt FROM vpn_clients
|
||||||
|
WHERE last_handshake IS NOT NULL
|
||||||
|
AND last_handshake > DATE_SUB(NOW(), INTERVAL 5 MINUTE)
|
||||||
|
AND status = 'active'
|
||||||
|
");
|
||||||
|
$recentHandshakeCount = (int)$stmt->fetchColumn();
|
||||||
|
|
||||||
|
// Combine both counts (XRay online + recent handshake), avoiding duplicates
|
||||||
|
$totalOnline = max($onlineData['total'], $recentHandshakeCount);
|
||||||
|
|
||||||
View::render('dashboard.twig', [
|
View::render('dashboard.twig', [
|
||||||
'servers' => $servers,
|
'servers' => $servers,
|
||||||
'clients' => $clients,
|
'clients' => $clients,
|
||||||
'online_count' => $onlineData['total'],
|
'online_count' => $totalOnline,
|
||||||
'online_users' => $onlineData['users'],
|
'online_users' => $onlineData['users'],
|
||||||
]);
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -276,8 +276,9 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</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" data-client-name="{{ client.name }}" data-client-status="{{ client.status }}">
|
<td class="px-6 py-4" data-client-name="{{ client.name }}" data-client-status="{{ client.status }}" data-last-handshake="{{ client.last_handshake }}">
|
||||||
{% if client.name in online_logins %}
|
{% set is_online_by_handshake = client.last_handshake and (("now"|date('U') - client.last_handshake|date('U')) < 300) %}
|
||||||
|
{% if client.name in online_logins or is_online_by_handshake %}
|
||||||
<span class="online-badge px-2 py-1 bg-green-100 text-green-800 rounded text-xs"><i class="fas fa-wifi mr-1"></i>Online</span>
|
<span class="online-badge 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' %}
|
{% elseif client.status == 'active' %}
|
||||||
<span class="status-badge px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{{ t('status.active') }}</span>
|
<span class="status-badge px-2 py-1 bg-gray-100 text-gray-600 rounded text-xs">{{ t('status.active') }}</span>
|
||||||
|
|||||||
Reference in New Issue
Block a user