Files
2026-09-16 23:20:08 +07:00

1315 lines
77 KiB
PHP

<?php $pageTitle = __('Agents Overview'); ob_start(); ?>
<!-- Stats Section -->
<div class="grid grid-cols-1 md:grid-cols-4 gap-5 mb-8">
<div class="corp-card p-5 border-t-4 border-t-slate-800 flex flex-col justify-center">
<div class="flex justify-between items-center mb-1">
<h3 class="text-slate-500 text-xs font-bold uppercase tracking-wide"><?= __('Total Agents') ?></h3>
<i class="fas fa-server text-slate-600 text-lg"></i>
</div>
<p class="text-3xl font-semibold text-slate-800 transition-all duration-300"><?= number_format($stats['total']) ?></p>
</div>
<div class="corp-card p-5 border-t-4 border-t-emerald-500 flex flex-col justify-center">
<div class="flex justify-between items-center mb-1">
<h3 class="text-slate-500 text-xs font-bold uppercase tracking-wide"><?= __('Active (Online)') ?></h3>
<i class="fas fa-check-circle text-emerald-200 text-lg"></i>
</div>
<p class="text-3xl font-semibold text-emerald-600 transition-all duration-300"><?= number_format($stats['active']) ?></p>
</div>
<div class="corp-card p-5 border-t-4 border-t-rose-500 flex flex-col justify-center">
<div class="flex justify-between items-center mb-1">
<h3 class="text-slate-500 text-xs font-bold uppercase tracking-wide"><?= __('Disconnected (Offline)') ?></h3>
<i class="fas fa-times-circle text-rose-200 text-lg"></i>
</div>
<p class="text-3xl font-semibold text-rose-600 transition-all duration-300"><?= number_format($stats['disconnected']) ?></p>
</div>
<div class="corp-card p-5 border-t-4 border-t-slate-300 flex flex-col justify-center">
<div class="flex justify-between items-center mb-1">
<h3 class="text-slate-500 text-xs font-bold uppercase tracking-wide"><?= __('Never Connected') ?></h3>
<i class="fas fa-question-circle text-slate-800 text-lg"></i>
</div>
<p class="text-3xl font-semibold text-slate-500 transition-all duration-300"><?= number_format($stats['never_connected']) ?></p>
</div>
</div>
<!-- Resource Warnings Section -->
<div class="corp-card overflow-hidden mb-8">
<div class="px-6 py-4 border-b border-slate-200 bg-slate-50 flex justify-between items-center">
<div>
<h3 class="font-bold text-slate-800 flex items-center">
<i class="fas fa-exclamation-triangle text-amber-500 mr-2"></i> <?= __('Resource Warnings') ?>
</h3>
<p class="text-xs text-slate-500 mt-1"><?= __('Disk/RAM warnings from active alerts') ?></p>
</div>
</div>
<div class="p-4 bg-slate-50 border-b border-slate-100">
<div class="grid grid-cols-1 sm:grid-cols-2 gap-4">
<div class="bg-white border border-slate-200 rounded-lg p-4 flex items-center shadow-sm cursor-pointer hover:shadow-md hover:border-rose-200 transition-all" onclick="showResourceDetails('ram')">
<div class="rounded-full bg-rose-100 p-3 mr-4">
<i class="fas fa-memory text-rose-500 text-xl"></i>
</div>
<div>
<div class="text-xs text-slate-500 font-bold uppercase tracking-wider mb-1">RAM Usage > <?= $_ENV['ALERT_RAM_PERCENT'] ?? 85 ?>%</div>
<div class="text-2xl font-bold text-slate-800" id="stat-ram-count"><i class="fas fa-circle-notch fa-spin text-slate-600 text-lg"></i></div>
</div>
<div class="ml-auto text-slate-600"><i class="fas fa-chevron-right"></i></div>
</div>
<div class="bg-white border border-slate-200 rounded-lg p-4 flex items-center shadow-sm cursor-pointer hover:shadow-md hover:border-amber-200 transition-all" onclick="showResourceDetails('disk')">
<div class="rounded-full bg-amber-100 p-3 mr-4">
<i class="fas fa-hdd text-amber-500 text-xl"></i>
</div>
<div>
<div class="text-xs text-slate-500 font-bold uppercase tracking-wider mb-1">Disk Usage > <?= $_ENV['ALERT_DISK_PERCENT'] ?? 85 ?>%</div>
<div class="text-2xl font-bold text-slate-800" id="stat-disk-count"><i class="fas fa-circle-notch fa-spin text-slate-600 text-lg"></i></div>
</div>
<div class="ml-auto text-slate-600"><i class="fas fa-chevron-right"></i></div>
</div>
</div>
</div>
<div class="p-4">
<h4 class="font-bold text-slate-700 mb-3 text-sm"><i class="fas fa-history mr-1"></i> <?= __('Recent Resource Alerts (Last 24h)') ?></h4>
<?php if (empty($resourceAlerts)): ?>
<div class="text-center py-6 text-slate-500">
<i class="fas fa-check-circle text-emerald-400 text-3xl mb-3"></i>
<p><?= __('No resource warnings detected.') ?></p>
</div>
<?php else: ?>
<div class="space-y-3">
<?php foreach ($resourceAlerts as $alert): ?>
<div class="p-3 bg-amber-50 border border-amber-200 rounded-md flex flex-col sm:flex-row gap-3 items-start sm:items-center justify-between">
<div>
<span class="text-xs font-bold text-amber-800 bg-amber-200 px-2 py-0.5 rounded mr-2">Rule <?= htmlspecialchars($alert['rule_id']) ?></span>
<span class="font-semibold text-slate-800 text-sm"><?= htmlspecialchars($alert['agent_name']) ?></span>
<p class="text-sm text-slate-600 mt-1"><?= htmlspecialchars($alert['description']) ?></p>
</div>
<span class="text-xs text-slate-500 whitespace-nowrap"><i class="far fa-clock mr-1"></i> <?= date('d M Y H:i', strtotime($alert['created_at'])) ?></span>
</div>
<?php endforeach; ?>
</div>
<?php endif; ?>
</div>
</div>
<!-- Agents Table -->
<div class="corp-card overflow-hidden">
<div class="px-6 py-4 border-b border-slate-200 bg-slate-50">
<h3 class="font-bold text-slate-800 flex items-center">
<i class="fas fa-list text-slate-400 mr-2"></i> <?= __('Agents (Devices)') ?>
</h3>
</div>
<div class="p-0">
<div class="overflow-x-auto">
<table id="agentsTable" class="w-full text-left text-sm whitespace-nowrap">
<thead class="bg-slate-50 text-slate-500 text-xs uppercase">
<tr>
<th class="px-6 py-4 font-semibold text-center"><?= __('Status') ?></th>
<th class="px-6 py-4 font-semibold text-center"><?= __('ID') ?></th>
<th class="px-6 py-4 font-semibold text-center"><?= __('Agent Name') ?></th>
<th class="px-6 py-4 font-semibold text-center"><?= __('IP Address') ?></th>
<th class="px-6 py-4 font-semibold text-center"><?= __('OS') ?></th>
<th class="px-6 py-4 font-semibold text-center"><?= __('Action') ?></th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 bg-white">
<?php foreach ($agents as $agent): ?>
<?php
$status = strtolower($agent['status'] ?? '');
$statusClass = 'bg-slate-100 text-slate-600';
$statusIcon = 'fa-question-circle';
$sortOrder = 9;
if ($status === 'active') {
$statusClass = 'bg-emerald-100 text-emerald-700';
$statusIcon = 'fa-check-circle';
$sortOrder = 5;
} elseif ($status === 'disconnected') {
$statusClass = 'bg-rose-100 text-rose-700';
$statusIcon = 'fa-times-circle';
$sortOrder = 6;
}
$osName = $agent['os']['name'] ?? 'Unknown';
$osVersion = $agent['os']['version'] ?? '';
$isWindows = (stripos($osName, 'windows') !== false);
?>
<tr id="row-agent-<?= htmlspecialchars($agent['id']) ?>" class="hover:bg-slate-50 transition-colors">
<td class="px-6 py-4" id="status-col-<?= htmlspecialchars($agent['id']) ?>">
<div class="flex justify-center items-center space-x-2">
<span class="hidden sort-key"><?= $sortOrder ?></span>
<span class="inline-flex items-center px-2 py-0.5 rounded text-xs font-medium <?= $statusClass ?>">
<i class="fas <?= $statusIcon ?> mr-1.5"></i> <?= ucfirst($status) ?>
</span>
<!-- Warning icons will be injected here via JS -->
</div>
</td>
<td class="px-6 py-4 font-mono text-xs text-slate-500 text-center"><?= htmlspecialchars($agent['id']) ?></td>
<td class="px-6 py-4 font-semibold text-slate-700 text-center"><?= htmlspecialchars($agent['name']) ?></td>
<?php
$agentIp = $agent['ip'] ?? 'N/A';
if ($agentIp !== 'N/A' && strpos($agentIp, ',') !== false) {
$ipParts = explode(',', $agentIp);
$ipv4Parts = array_filter($ipParts, function($ip) {
return strpos($ip, ':') === false;
});
$agentIp = count($ipv4Parts) > 0 ? implode(', ', $ipv4Parts) : $agentIp;
} elseif (strpos($agentIp, ':') !== false && filter_var($agentIp, FILTER_VALIDATE_IP, FILTER_FLAG_IPV6)) {
// If it's only an IPv6 address, we keep it or show N/A based on preference. Let's just keep it if no IPv4.
}
?>
<td class="px-6 py-4 font-mono text-sm text-blue-600 text-center"><?= htmlspecialchars($agentIp) ?></td>
<td class="px-6 py-4 text-slate-600 text-center">
<?php if ($isWindows): ?>
<i class="fab fa-windows text-blue-500 mr-1 w-4 text-center"></i>
<?php elseif (stripos($osName, 'ubuntu') !== false || stripos($osName, 'debian') !== false): ?>
<i class="fab fa-ubuntu text-orange-500 mr-1 w-4 text-center"></i>
<?php elseif (stripos($osName, 'centos') !== false || stripos($osName, 'redhat') !== false): ?>
<i class="fab fa-redhat text-red-500 mr-1 w-4 text-center"></i>
<?php elseif (stripos($osName, 'mac') !== false): ?>
<i class="fab fa-apple text-slate-800 mr-1 w-4 text-center"></i>
<?php else: ?>
<i class="fab fa-linux text-slate-800 mr-1 w-4 text-center"></i>
<?php endif; ?>
<?= htmlspecialchars($osName . ' ' . $osVersion) ?>
</td>
<td class="px-6 py-4 text-center space-x-2">
<button onclick="viewSpecs('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['name']) ?>')" class="inline-flex items-center justify-center w-8 h-8 rounded-md bg-slate-100 text-slate-500 hover:bg-slate-200 hover:text-slate-700 transition" title="<?= __('View Specs') ?>">
<i class="fas fa-search"></i>
</button>
<button onclick="viewNetworkInspector('<?= htmlspecialchars($agent['id']) ?>', '<?= htmlspecialchars($agent['name']) ?>')" class="inline-flex items-center justify-center w-8 h-8 rounded-md bg-indigo-50 text-indigo-500 hover:bg-indigo-100 hover:text-indigo-700 transition" title="Network Inspector">
<i class="fas fa-network-wired"></i>
</button>
</td>
</tr>
<?php endforeach; ?>
</tbody>
</table>
</div>
</div>
</div>
<template id="specsTemplate">
<div class="text-left text-sm p-2">
<h4 class="font-bold text-slate-700 mb-2 border-b pb-2"><i class="fas fa-microchip text-slate-400 mr-2"></i> Hardware</h4>
<div class="mb-4 space-y-2">
<div class="flex justify-between border-b border-slate-100 pb-1">
<span class="text-slate-500"><?= __('CPU') ?></span>
<span class="font-mono text-slate-800" id="spec-cpu">-</span>
</div>
<div class="flex justify-between border-b border-slate-100 pb-1">
<span class="text-slate-500"><?= __('RAM Total') ?></span>
<span class="font-mono text-slate-800" id="spec-ram">-</span>
</div>
</div>
<h4 class="font-bold text-slate-700 mb-2 mt-4 border-b pb-2"><i class="fas fa-hdd text-slate-400 mr-2"></i> <?= __('Disk Storage (Drive C)') ?></h4>
<div id="spec-disk" class="space-y-3 mb-4">
<!-- Disk info will be injected here -->
</div>
<div class="flex justify-between items-end border-b pb-2 mb-2">
<h4 class="font-bold text-slate-700 m-0"><i class="fas fa-network-wired text-slate-400 mr-2"></i> <?= __('Network Interfaces') ?></h4>
<label class="flex items-center cursor-pointer" title="Show Virtual, Bluetooth, and Other Adapters">
<input type="checkbox" id="toggle-extra-net" class="sr-only peer" onchange="document.querySelectorAll('.iface-extra').forEach(e => e.classList.toggle('hidden', !this.checked));">
<div class="relative w-7 h-4 bg-slate-200 peer-focus:outline-none rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:start-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-3 after:w-3 after:transition-all peer-checked:bg-primary"></div>
<span class="ms-2 text-[10px] font-bold text-slate-500 uppercase tracking-wide">Show All</span>
</label>
</div>
<div id="spec-network" class="grid grid-cols-1 md:grid-cols-2 gap-3 max-h-48 overflow-y-auto mb-4">
<!-- Network interfaces will be injected here -->
</div>
<h4 class="font-bold text-slate-700 mb-2 border-b pb-2"><i class="fas fa-desktop text-slate-400 mr-2"></i> <?= __('Remote Access') ?></h4>
<div id="spec-remote" class="grid grid-cols-2 gap-4">
<!-- Remote ID will be injected here -->
</div>
</div>
</template>
<!-- Remote Panel Floating Button -->
<button onclick="toggleRemotePanel()" class="fixed top-1/2 right-0 transform -translate-y-1/2 bg-blue-600 hover:bg-blue-700 text-slate-800 p-3 rounded-l-lg shadow-[0_0_15px_rgba(0,0,0,0.2)] z-[9999] transition-colors flex flex-col items-center group">
<i class="fas fa-desktop mb-1"></i>
<span class="text-[9px] font-bold uppercase tracking-wider">Remote</span>
</button>
<!-- Remote Slide-out Panel -->
<div id="remoteBackdrop" onclick="toggleRemotePanel()" class="fixed inset-0 bg-white/20 backdrop-blur-sm z-[9998] hidden opacity-0 transition-opacity duration-300"></div>
<div id="remotePanel" class="fixed inset-y-0 right-0 w-80 md:w-96 bg-white shadow-2xl z-[9999] transform translate-x-full transition-transform duration-300 flex flex-col border-l border-slate-200">
<div class="p-4 bg-slate-50 border-b border-slate-200 flex justify-between items-center">
<h3 class="font-bold text-slate-800"><i class="fas fa-desktop text-blue-500 mr-2"></i> Remote Agents List</h3>
<button onclick="toggleRemotePanel()" class="text-slate-400 hover:text-rose-500 transition-colors">
<i class="fas fa-times text-lg"></i>
</button>
</div>
<div class="p-3 border-b border-slate-100 bg-white">
<div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-400"></i>
<input type="text" id="remoteSearch" onkeyup="filterRemoteList()" placeholder="Search agent or ID..." class="w-full text-sm border border-slate-300 rounded-lg pl-9 pr-3 py-2 focus:outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500">
</div>
<div class="mt-2.5 px-1 flex items-center">
<label class="flex items-center text-xs text-slate-500 cursor-pointer hover:text-slate-800 transition-colors">
<input type="checkbox" id="hideNoRemote" checked class="mr-2 rounded border-slate-300 text-blue-500 focus:ring-blue-500" onchange="filterRemoteList()">
<span>ซ่อนเครื่องที่ไม่มี Remote ID</span>
</label>
</div>
</div>
<div class="flex-1 overflow-y-auto" id="remoteListContainer">
<!-- JS will inject list here -->
<div class="text-center text-slate-400 mt-10">
<p>Click "Load Data" to fetch remote IDs.</p>
</div>
</div>
</div>
<script>
// Remote Panel Logic
let remoteDataLoaded = false;
function toggleRemotePanel() {
const panel = document.getElementById('remotePanel');
const backdrop = document.getElementById('remoteBackdrop');
if (panel.classList.contains('translate-x-full')) {
// Open
panel.classList.remove('translate-x-full');
backdrop.classList.remove('hidden');
setTimeout(() => backdrop.classList.remove('opacity-0'), 10);
if (!remoteDataLoaded) {
loadRemoteList();
}
} else {
// Close
panel.classList.add('translate-x-full');
backdrop.classList.add('opacity-0');
setTimeout(() => backdrop.classList.add('hidden'), 300);
}
}
function loadRemoteList() {
const container = document.getElementById('remoteListContainer');
container.innerHTML = '<div class="text-center text-slate-400 mt-10"><i class="fas fa-spinner fa-spin text-2xl mb-2"></i><br>Loading remote IDs...</div>';
fetch('<?= $baseUrl ?>/api/agents/remote-list')
.then(res => res.text())
.then(text => {
let res;
try {
res = JSON.parse(text);
} catch (e) {
console.error("JSON Parse Error:", e, "Response Text:", text);
container.innerHTML = '<div class="text-center text-rose-500 mt-10 text-xs text-left p-4 overflow-auto"><i class="fas fa-bug mb-2"></i><br>Parse error. Server returned:<br><code>' + text.substring(0, 200).replace(/</g, "&lt;") + '</code></div>';
return;
}
try {
if (res.status === 'success') {
remoteDataLoaded = true;
renderRemoteList(res.data);
} else {
container.innerHTML = '<div class="text-center text-rose-500 mt-10"><i class="fas fa-exclamation-triangle text-2xl mb-2"></i><br>' + (res.message || 'Failed to load data.') + '</div>';
}
} catch (e) {
console.error("Render Error:", e);
container.innerHTML = '<div class="text-center text-rose-500 mt-10"><i class="fas fa-bug mb-2"></i><br>UI Error: ' + e.message + '</div>';
}
})
.catch(err => {
console.error("Fetch Error:", err);
container.innerHTML = '<div class="text-center text-rose-500 mt-10"><i class="fas fa-exclamation-triangle text-2xl mb-2"></i><br>Connection error: ' + err.message + '</div>';
});
}
function copyToClipboard(text, el) {
if (text === '-' || !text) return;
navigator.clipboard.writeText(text).then(() => {
let originalText = el.innerText;
el.innerHTML = '<i class="fas fa-check text-emerald-500"></i> Copied';
el.classList.add('text-emerald-600');
setTimeout(() => {
el.innerText = originalText;
el.classList.remove('text-emerald-600');
}, 1500);
});
}
function toggleRemoteGroup(listId, iconId) {
const list = document.getElementById(listId);
const icon = document.getElementById(iconId);
if (list.classList.contains('hidden')) {
list.classList.remove('hidden');
icon.classList.remove('fa-chevron-down');
icon.classList.add('fa-chevron-up');
} else {
list.classList.add('hidden');
icon.classList.remove('fa-chevron-up');
icon.classList.add('fa-chevron-down');
}
}
function renderRemoteList(data) {
const container = document.getElementById('remoteListContainer');
container.innerHTML = '';
if (data.length === 0) {
container.innerHTML = '<div class="text-center text-slate-400 mt-10">No agents found.</div>';
return;
}
// Sort alphabetically by name
data.sort((a, b) => a.name.localeCompare(b.name));
const onlineAgents = data.filter(a => a.status === 'active');
const offlineAgents = data.filter(a => a.status !== 'active');
// Build HTML for Online Group (Expanded by default)
const onlineHtml = `
<div class="group-online border-b border-slate-200">
<button onclick="toggleRemoteGroup('online-list', 'online-icon')" class="w-full flex justify-between items-center bg-slate-50 p-3 text-xs font-bold text-slate-700 hover:bg-slate-100 transition-colors">
<div class="flex items-center">
<span class="w-2 h-2 rounded-full bg-emerald-500 mr-2 shadow-[0_0_5px_rgba(16,185,129,0.5)]"></span>
ออนไลน์ (<span id="online-count">${onlineAgents.length}</span>)
</div>
<i id="online-icon" class="fas fa-chevron-up text-slate-400 transition-transform text-[10px]"></i>
</button>
<div id="online-list" class="transition-all duration-300">
${onlineAgents.map(ag => createRemoteItemHtml(ag, true)).join('')}
</div>
</div>
`;
// Build HTML for Offline Group (Collapsed by default)
const offlineHtml = `
<div class="group-offline border-b border-slate-200">
<button onclick="toggleRemoteGroup('offline-list', 'offline-icon')" class="w-full flex justify-between items-center bg-slate-50 p-3 text-xs font-bold text-slate-700 hover:bg-slate-100 transition-colors">
<div class="flex items-center">
<span class="w-2 h-2 rounded-full bg-slate-400 mr-2"></span>
ออฟไลน์ (<span id="offline-count">${offlineAgents.length}</span>)
</div>
<i id="offline-icon" class="fas fa-chevron-down text-slate-400 transition-transform text-[10px]"></i>
</button>
<div id="offline-list" class="transition-all duration-300 hidden">
${offlineAgents.map(ag => createRemoteItemHtml(ag, false)).join('')}
</div>
</div>
`;
container.innerHTML = onlineHtml + offlineHtml;
filterRemoteList();
}
function createRemoteItemHtml(ag, isOnline) {
const dotColor = isOnline ? 'bg-emerald-500 shadow-[0_0_5px_rgba(16,185,129,0.5)]' : 'bg-slate-300';
let displayId = ag.remote_id ? ag.remote_id.toString() : '-';
let programIcon = '<i class="fas fa-desktop text-slate-600"></i>';
if (ag.remote_type === 'teamviewer') {
programIcon = '<i class="fas fa-network-wired text-blue-500" title="TeamViewer"></i>';
} else if (ag.remote_type === 'anydesk') {
programIcon = '<i class="fas fa-network-wired text-rose-500" title="AnyDesk"></i>';
}
return `
<div class="remote-item flex items-center justify-between px-3 py-2 border-b border-slate-50 hover:bg-slate-50 cursor-default bg-white transition-colors">
<div class="flex items-center space-x-2 w-5/12">
<div class="relative flex-shrink-0 text-center w-5">
${programIcon}
</div>
<span class="text-slate-700 text-xs font-medium truncate name-field" title="${ag.name}">${ag.name}</span>
</div>
<div class="flex items-center justify-end space-x-3 w-7/12">
<span class="font-mono text-slate-600 text-xs id-field whitespace-nowrap cursor-pointer hover:text-blue-600 transition-colors" title="คลิกเพื่อคัดลอก" onclick="copyToClipboard('${displayId}', this)">${displayId}</span>
<span class="w-2.5 h-2.5 rounded-full flex-shrink-0 ${dotColor}" title="${isOnline ? 'ออนไลน์' : 'ออฟไลน์'}"></span>
</div>
</div>
`;
}
function filterRemoteList() {
const input = document.getElementById('remoteSearch').value.toLowerCase();
const hideNoRemote = document.getElementById('hideNoRemote').checked;
const items = document.querySelectorAll('.remote-item');
let onlineVisible = 0;
let offlineVisible = 0;
items.forEach(item => {
const name = item.querySelector('.name-field').innerText.toLowerCase();
const id = item.querySelector('.id-field').innerText.trim().toLowerCase();
const isOnline = item.parentElement.id === 'online-list';
const hasNoId = (id === '-' || id === '');
const matchSearch = name.includes(input) || id.includes(input) || input.replace(/\s/g, '') === id.replace(/\s/g, '');
if (matchSearch) {
if (hideNoRemote && hasNoId) {
item.style.display = 'none';
} else {
item.style.display = 'flex';
if (isOnline) onlineVisible++; else offlineVisible++;
}
} else {
item.style.display = 'none';
}
});
document.getElementById('online-count').innerText = onlineVisible;
document.getElementById('offline-count').innerText = offlineVisible;
}
$(document).ready(function() {
window.agentsTableDt = $('#agentsTable').DataTable({
pageLength: 10,
language: {
search: "_INPUT_",
searchPlaceholder: "Search agents..."
},
dom: '<"flex flex-col md:flex-row justify-between items-center mb-4"lf>rt<"flex flex-col md:flex-row justify-between items-center mt-4"ip>'
});
});
window.viewNetworkInspector = function(agentId, agentName) {
// Load Chart.js if not available
if (typeof Chart === 'undefined') {
let script = document.createElement('script');
script.src = 'https://cdn.jsdelivr.net/npm/chart.js';
document.head.appendChild(script);
}
Swal.fire({
title: false,
background: '#ffffff', // Slate 900
color: '#1e293b',
html: '<div class="text-center py-16"><i class="fas fa-spinner fa-spin text-5xl text-blue-500"></i><div class="mt-4 text-slate-400 tracking-wider text-sm">ANALYZING NETWORK TRAFFIC...</div></div>',
showConfirmButton: false,
showCloseButton: true,
width: '1200px',
customClass: {
popup: 'rounded-xl border border-slate-200 shadow-xl',
closeButton: 'text-slate-400 hover:text-slate-800'
},
didOpen: () => {
$.ajax({
url: '<?= $baseUrl ?>/api/agent/network?agent_id=' + agentId,
success: function(res) {
if (res.status === 'success') {
let ip = res.netiface && res.netiface.length > 0 ? res.netiface[0].ipv4 : 'N/A';
let mac = res.netiface && res.netiface.length > 0 ? res.netiface[0].mac : 'N/A';
let html = `
<div class="text-left font-sans">
<!-- Header -->
<div class="flex flex-col md:flex-row justify-between items-start md:items-center mb-6">
<div class="flex items-center space-x-4">
<div class="bg-blue-600/20 text-blue-400 p-4 rounded-xl border border-blue-500/30">
<i class="fas fa-laptop text-3xl"></i>
</div>
<div>
<h2 class="text-2xl font-bold text-slate-800 mb-1 tracking-wide flex items-center">
${agentName}
<span class="ml-3 px-2.5 py-0.5 rounded-full border border-emerald-500/50 bg-emerald-500/10 text-emerald-400 text-xs font-medium"><i class="fas fa-circle text-[8px] mr-1"></i> Online</span>
</h2>
<div class="flex space-x-4 text-xs font-mono text-slate-400">
<span><i class="fas fa-plug text-slate-500 mr-1"></i> ${ip}</span>
<span><i class="fas fa-fingerprint text-slate-500 mr-1"></i> ${mac}</span>
<span><i class="fas fa-users text-slate-500 mr-1"></i> Corp_VLAN</span>
</div>
</div>
</div>
</div>
<!-- Top Grid: Stats, Donut, Bar -->
<div class="grid grid-cols-1 lg:grid-cols-4 gap-4 mb-6">
<!-- Left Column: Stats -->
<div class="lg:col-span-1 flex flex-col justify-between space-y-4">
<div class="bg-white border border-slate-200 rounded-xl p-4 flex items-center space-x-4">
<div class="bg-blue-500/10 text-blue-400 p-3 rounded-lg"><i class="fas fa-exchange-alt"></i></div>
<div>
<div class="text-slate-400 text-xs font-semibold tracking-wider uppercase mb-1">Total Connections (24h)</div>
<div class="text-2xl font-bold text-slate-800">${res.stats.total_connections_24h.toLocaleString()}</div>
</div>
</div>
<div class="bg-white border border-slate-200 rounded-xl p-4 flex items-center space-x-4">
<div class="bg-emerald-500/10 text-emerald-400 p-3 rounded-lg"><i class="fas fa-door-open"></i></div>
<div>
<div class="text-slate-400 text-xs font-semibold tracking-wider uppercase mb-1">Open Ports</div>
<div class="text-2xl font-bold text-slate-800">${res.stats.open_ports}</div>
</div>
</div>
<div class="bg-white border border-slate-200 rounded-xl p-4 flex items-center space-x-4">
<div class="bg-rose-500/10 text-rose-400 p-3 rounded-lg"><i class="fas fa-shield-alt"></i></div>
<div>
<div class="text-slate-400 text-xs font-semibold tracking-wider uppercase mb-1">Suspicious Events (24h)</div>
<div class="text-2xl font-bold text-rose-400">${res.stats.suspicious_alerts_24h}</div>
</div>
</div>
</div>
<!-- Middle Column: Donut Chart -->
<div class="lg:col-span-1.5 bg-white border border-slate-200 rounded-xl p-4 relative" style="grid-column: span 1.5 / span 1.5;">
<h3 class="text-slate-800 font-bold mb-4 tracking-wide text-sm">Top Processes (L7 Proxy)</h3>
<div class="relative h-48 w-full flex justify-center items-center"><canvas id="appChart"></canvas></div>
</div>
<!-- Center Column: Categories -->
<div class="lg:col-span-1 bg-white border border-slate-200 rounded-xl p-4">
<h3 class="text-slate-800 font-bold mb-4 tracking-wide text-sm">Traffic Categories</h3>
<div class="relative h-48 w-full flex justify-center items-center"><canvas id="categoryChart"></canvas></div>
</div>
<!-- Right Column: Bar Chart -->
<div class="lg:col-span-1 bg-white border border-slate-200 rounded-xl p-4">
<h3 class="text-slate-800 font-bold mb-4 tracking-wide text-sm">Connection Activity (Last 6h)</h3>
<div class="relative h-48 w-full">
<canvas id="activityChart"></canvas>
</div>
</div>
</div>
<!-- Bottom Table: Flow Logs -->
<div class="bg-white border border-slate-200 rounded-xl overflow-hidden flex flex-col">
<div class="p-4 border-b border-slate-200 flex justify-between items-center bg-slate-50">
<div>
<h3 class="text-slate-800 font-bold text-lg">Deep Packet Inspection Flow Logs</h3>
<p class="text-slate-400 text-xs mt-1">Showing Layer 7 identified traffic and Sysmon Event ID 3 actions.</p>
</div>
<div class="flex space-x-2">
<select onchange="window.filterFlowLogs(this.value)" class="bg-white border border-slate-200 text-slate-600 text-sm rounded-lg px-3 py-1.5 focus:outline-none focus:border-blue-500">
<option value="All">All Categories</option>
<option value="Web Surfing">Web Surfing</option>
<option value="Developer Tools">Developer Tools</option>
<option value="Infrastructure">Infrastructure</option>
<option value="P2P File Sharing">P2P File Sharing</option>
<option value="DNS Query">DNS Query</option>
<option value="Uncategorized">Uncategorized</option>
</select>
<div class="relative">
<i class="fas fa-search absolute left-3 top-1/2 transform -translate-y-1/2 text-slate-500 text-sm"></i>
<input type="text" onkeyup="window.searchFlowLogs(this.value)" placeholder="Search IP, App..." class="bg-white border border-slate-200 text-slate-600 text-sm rounded-lg pl-9 pr-3 py-1.5 focus:outline-none focus:border-blue-500 w-48">
</div>
</div>
</div>
<div class="overflow-x-auto overflow-y-auto max-h-[600px]">
<table class="w-full text-left border-collapse whitespace-nowrap">
<thead class="bg-slate-50 sticky top-0 z-10 border-b border-slate-200 shadow-sm text-xs text-slate-400 font-bold tracking-wider uppercase">
<tr>
<th class="px-6 py-4 font-semibold">Time (UTC)</th>
<th class="px-6 py-4 font-semibold">Destination</th>
<th class="px-6 py-4 font-semibold">Application / L7</th>
<th class="px-6 py-4 font-semibold">Category</th>
<th class="px-6 py-4 font-semibold">Payload Info</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100 text-sm">
`;
if (res.sysmon && res.sysmon.length > 0) {
console.log("SYSMON EVENTS:", res.sysmon); res.sysmon.forEach(s => {
// Mock category/payload based on process to match UI
let procLow = s.process.toLowerCase();
let cat = "Uncategorized";
let payload = s.payload;
let icon = "fa-brands fa-windows text-slate-400";
let action = "Pass";
// Colorized App Icons
if (procLow.includes("chrome")) icon = "fa-brands fa-chrome text-blue-500";
else if (procLow.includes("edge")) icon = "fa-brands fa-edge text-blue-600";
else if (procLow.includes("firefox")) icon = "fa-brands fa-firefox-browser text-orange-500";
else if (procLow.includes("sysmon")) icon = "fa-solid fa-eye text-cyan-600";
else if (procLow.includes("svchost") || procLow.includes("system")) icon = "fa-solid fa-server text-slate-500";
else if (procLow.includes("powershell") || procLow.includes("cmd")) icon = "fa-solid fa-terminal text-slate-700";
else if (procLow.includes("git")) icon = "fa-brands fa-git-alt text-orange-600";
else if (procLow.includes("code")) icon = "fa-solid fa-code text-blue-500";
else if (procLow.includes("torrent") || procLow.includes("p2p")) icon = "fa-solid fa-share-nodes text-purple-500";
if (s.type === 'dns') {
if (payload.includes('github') || payload.includes('gitlab')) {
cat = "Developer Tools";
} else if (payload.includes('google') || payload.includes('facebook') || payload.includes('arpa')) {
cat = "Web Surfing";
} else if (payload.includes('torrent')) {
cat = "P2P File Sharing"; action = "Drop";
} else {
cat = "DNS Query";
}
} else {
if (procLow.includes("chrome") || procLow.includes("edge") || procLow.includes("firefox")) {
cat = "Web Surfing";
} else if (procLow.includes("git") || procLow.includes("code")) {
cat = "Developer Tools";
} else if (procLow.includes("svchost") || procLow.includes("system") || procLow.includes("sysmon")) {
cat = "Infrastructure";
} else if (procLow.includes("torrent") || procLow.includes("p2p")) {
cat = "P2P File Sharing"; action = "Drop";
}
}
let fullPath = s.full_path || 'Unknown';
if (fullPath === 'Unknown') fullPath = 'Path not available';
// Escape backslashes for JS template literal
fullPath = fullPath.replace(/\\/g, '\\\\');
html += `
<tr class="hover:bg-slate-50/70 transition-colors flow-log-row" data-category="${cat}">
<td class="px-6 py-4 text-slate-400 font-mono text-xs">${s.timestamp}</td>
<td class="px-6 py-4">
<div class="text-slate-800 font-bold tracking-wide">${s.dst_ip}</div>
<div class="text-slate-500 font-mono text-xs mt-0.5">Port ${s.dst_port}</div>
</td>
<td class="px-6 py-4">
<div class="flex items-center">
<div class="w-8 h-8 rounded-lg bg-slate-100 border border-slate-200 flex justify-center items-center mr-3 flex-shrink-0">
<i class="${icon} text-lg"></i>
</div>
<div class="flex flex-col max-w-[200px] xl:max-w-[280px] truncate" title="${s.process}">
<span class="text-slate-800 font-semibold truncate leading-tight">${s.process}</span>
</div>
</div>
</td>
<td class="px-6 py-4">
<span class="px-3 py-1 rounded-md border border-slate-200 text-slate-600 text-xs">${cat}</span>
</td>
<td class="px-6 py-4">
<div class="text-amber-500/80 font-mono text-xs">${payload}</div>
<div class="text-slate-500 text-xs mt-0.5">${s.protocol} Protocol</div>
</td>
</tr>
`;
});
} else {
html += `<tr><td colspan="6" class="px-6 py-10 text-center text-slate-500">No flow logs detected.</td></tr>`;
}
html += `
</tbody>
</table>
</div>
</div>
</div>`;
Swal.update({ html: html });
// Render Charts After HTML Injection
setTimeout(() => {
if (typeof Chart !== 'undefined') {
Chart.defaults.color = '#64748b';
Chart.defaults.font.family = 'Inter, sans-serif';
// App Donut Chart
let procLabels = Object.keys(res.top_processes);
let procData = Object.values(res.top_processes);
new Chart(document.getElementById('appChart'), {
type: 'doughnut',
data: {
labels: procLabels.length ? procLabels : ['No Data'],
datasets: [{
data: procData.length ? procData : [1],
backgroundColor: ['#6366f1', '#06b6d4', '#94a3b8', '#f59e0b', '#f43f5e'],
borderWidth: 0,
hoverOffset: 4
}]
},
options: {
cutout: '75%',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'right', labels: { boxWidth: 12, usePointStyle: true, padding: 15 } }
}
}
});
// Process Category Chart Data
let catCounts = {};
res.sysmon.forEach(s => {
let procLow = s.process.toLowerCase();
let cat = "Uncategorized";
let payload = s.payload || '';
if (s.type === 'dns') {
if (payload.includes('github') || payload.includes('gitlab')) cat = "Developer Tools";
else if (payload.includes('google') || payload.includes('facebook') || payload.includes('arpa')) cat = "Web Surfing";
else if (payload.includes('torrent')) cat = "P2P File Sharing";
else cat = "DNS Query";
} else {
if (procLow.includes("chrome") || procLow.includes("edge") || procLow.includes("firefox")) cat = "Web Surfing";
else if (procLow.includes("git") || procLow.includes("code")) cat = "Developer Tools";
else if (procLow.includes("svchost") || procLow.includes("system") || procLow.includes("sysmon")) cat = "Infrastructure";
else if (procLow.includes("torrent") || procLow.includes("p2p")) cat = "P2P File Sharing";
}
catCounts[cat] = (catCounts[cat] || 0) + 1;
});
new Chart(document.getElementById('categoryChart'), {
type: 'doughnut',
data: {
labels: Object.keys(catCounts).length ? Object.keys(catCounts) : ['No Data'],
datasets: [{
data: Object.values(catCounts).length ? Object.values(catCounts) : [1],
backgroundColor: ['#10b981', '#3b82f6', '#8b5cf6', '#f59e0b', '#94a3b8', '#f43f5e'],
borderWidth: 0,
hoverOffset: 4
}]
},
options: {
cutout: '75%',
responsive: true,
maintainAspectRatio: false,
plugins: {
legend: { position: 'right', labels: { boxWidth: 12, usePointStyle: true, padding: 15 } }
}
}
});
// Process Bar Chart Data
let barLabels = res.timeline.map(t => t.hr);
let barData = res.timeline.map(t => t.cnt);
new Chart(document.getElementById('activityChart'), {
type: 'bar',
data: {
labels: barLabels.length ? barLabels : ['15:00', '16:00', '17:00'],
datasets: [{
label: 'Connections',
data: barData.length ? barData : [0, 0, 0],
backgroundColor: '#6366f1',
borderRadius: 4,
barThickness: 12
}]
},
options: {
responsive: true,
maintainAspectRatio: false,
plugins: { legend: { display: false } },
scales: {
y: { grid: { color: '#e2e8f0', drawBorder: false }, border: { dash: [4, 4] } },
x: { grid: { display: false, drawBorder: false } }
}
}
});
}
}, 500);
} else {
Swal.update({ html: '<div class="text-rose-500 p-4"><i class="fas fa-exclamation-triangle mr-2"></i>' + res.message + '</div>' });
}
},
error: function() {
Swal.update({ html: '<div class="text-rose-500 p-4"><i class="fas fa-exclamation-triangle mr-2"></i>Failed to fetch network data.</div>' });
}
});
}
});
};
window.viewSpecs = function(agentId, agentName) {
Swal.fire({
title: '<?= __('Hardware Specs') ?> - ' + agentName,
html: '<div class="text-center py-4"><i class="fas fa-spinner fa-spin text-3xl text-slate-600"></i></div>',
showConfirmButton: false,
showCloseButton: true,
width: '800px',
customClass: { popup: 'rounded-xl shadow-2xl' }
});
$.ajax({
url: `<?= $baseUrl ?>/api/agents/${agentId}/details`,
method: 'GET',
success: function(res) {
if (res.status === 'success') {
const tmpl = document.getElementById('specsTemplate').innerHTML;
Swal.update({ html: tmpl });
const modal = Swal.getHtmlContainer();
const hw = (res.hardware && res.hardware.length > 0) ? res.hardware[0] : {};
// CPU
if (hw.cpu && hw.cpu.name) {
const cores = hw.cpu.cores ? ` (${hw.cpu.cores} Cores)` : '';
modal.querySelector('#spec-cpu').innerText = hw.cpu.name + cores;
} else {
modal.querySelector('#spec-cpu').innerText = '-';
}
// RAM
if (hw.ram && hw.ram.total) {
const totalMB = hw.ram.total / 1024;
const gb = (totalMB / 1024).toFixed(2);
let usageHtml = `${gb} GB`;
if (hw.ram.free !== undefined) {
const freeMB = hw.ram.free / 1024;
const usageMB = Math.max(0, totalMB - freeMB);
const pct = Math.round((usageMB / totalMB) * 100);
const alertThresh = <?= $_ENV['ALERT_RAM_PERCENT'] ?? 85 ?>;
const colorClass = pct >= alertThresh ? 'text-rose-600 font-bold' : (pct >= alertThresh - 15 ? 'text-orange-500' : 'text-emerald-600');
usageHtml = `${gb} GB <span class="ml-2 text-xs px-2 py-0.5 bg-slate-100 rounded-full ${colorClass}">${pct}% Used</span>`;
}
modal.querySelector('#spec-ram').innerHTML = usageHtml;
} else {
modal.querySelector('#spec-ram').innerText = '-';
}
// Disk
const diskContainer = modal.querySelector('#spec-disk');
let diskTotal = 0, diskFree = 0, diskUsed = 0;
if (res.disk_info) {
let diskData = res.disk_info;
if (typeof diskData === 'string') {
try { diskData = JSON.parse(diskData); } catch (e) {}
}
if (diskData && !Array.isArray(diskData) && typeof diskData === 'object') {
diskData = [diskData];
}
if (diskData && Array.isArray(diskData)) {
diskData = diskData.filter(disk => {
let d = String(disk.DriveLetter || disk.Drive || disk.DeviceID || disk.Name || '');
return d.toUpperCase().includes('C');
});
if (diskData.length > 0) {
let dInfo = diskData[0];
diskTotal = parseFloat(dInfo.Total_GB || dInfo.TotalSpace || dInfo.SizeGB || (dInfo.Size ? dInfo.Size / 1073741824 : 0));
diskFree = parseFloat(dInfo.Free_GB || dInfo.FreeGB || (dInfo.FreeSpace ? dInfo.FreeSpace / 1073741824 : 0));
// If parsed values are extraordinarily large (e.g., FreeSpace was just a number in bytes but not named FreeSpace)
// or if TotalSpace was in bytes
if (dInfo.TotalSpace && dInfo.TotalSpace > 100000) diskTotal = diskTotal / 1073741824;
if (dInfo.FreeSpace && dInfo.FreeSpace > 100000) diskFree = diskFree / 1073741824;
diskUsed = parseFloat(dInfo.Used_GB || (diskTotal - diskFree));
diskUsed = Math.max(0, diskUsed);
}
}
}
if (diskTotal > 0) {
const pct = Math.round((diskUsed / diskTotal) * 100);
const alertThresh = <?= $_ENV['ALERT_DISK_PERCENT'] ?? 85 ?>;
let colorClass = 'bg-emerald-500';
if (pct >= alertThresh) colorClass = 'bg-rose-500';
else if (pct >= alertThresh - 15) colorClass = 'bg-orange-500';
diskContainer.innerHTML = `
<div class="flex justify-between text-xs mb-1">
<span class="font-bold text-slate-700">Capacity: ${diskTotal.toFixed(2)} GB</span>
<span class="text-slate-500">${diskUsed.toFixed(2)} GB Used (${diskFree.toFixed(2)} GB Free)</span>
</div>
<div class="w-full bg-slate-200 rounded-full h-2.5">
<div class="${colorClass} h-2.5 rounded-full transition-all duration-500" style="width: ${pct}%"></div>
</div>
`;
} else {
diskContainer.innerHTML = '<div class="text-slate-400 text-xs italic">No disk information available</div>';
}
// Remote Access
const remoteContainer = modal.querySelector('#spec-remote');
remoteContainer.innerHTML = '';
let tvInfo = null;
if (res.tv_info) {
let t = Array.isArray(res.tv_info) ? res.tv_info[0] : res.tv_info;
tvInfo = (t && t.TeamViewerID && t.TeamViewerID !== 'Not Found') ? t.TeamViewerID : null;
}
let adInfo = null;
if (res.ad_info) {
let a = Array.isArray(res.ad_info) ? res.ad_info[0] : res.ad_info;
adInfo = (a && a.AnyDeskID && a.AnyDeskID !== 'Not Found') ? a.AnyDeskID : null;
}
if (tvInfo || adInfo) {
if (tvInfo) {
let tvId = tvInfo.toString();
if (tvId.match(/^\d{9,10}$/)) {
tvId = tvId.replace(/(\d{1,2})(\d{3})(\d{3})(\d{3})?/, "$1 $2 $3 $4").trim();
}
remoteContainer.innerHTML += `
<div class="bg-blue-50 p-3 rounded-lg border border-blue-100 flex items-center justify-between">
<div class="flex items-center text-blue-700 font-bold text-xs"><i class="fas fa-desktop mr-2"></i> TeamViewer</div>
<div class="font-mono text-sm text-slate-800 bg-white px-2 py-1 rounded shadow-sm">${tvId}</div>
</div>
`;
}
if (adInfo) {
let adId = adInfo.toString();
if (adId.match(/^\d{9,10}$/)) {
adId = adId.replace(/(\d{3})(\d{3})(\d{3})?/, "$1 $2 $3").trim();
}
remoteContainer.innerHTML += `
<div class="bg-rose-50 p-3 rounded-lg border border-rose-100 flex items-center justify-between">
<div class="flex items-center text-rose-700 font-bold text-xs"><i class="fas fa-desktop mr-2"></i> AnyDesk</div>
<div class="font-mono text-sm text-slate-800 bg-white px-2 py-1 rounded shadow-sm">${adId}</div>
</div>
`;
}
} else {
remoteContainer.innerHTML = '<div class="col-span-2 text-slate-400 text-xs italic">No remote access ID found</div>';
}
// Network
const netContainer = modal.querySelector('#spec-network');
netContainer.innerHTML = '';
let networkArray = [];
if (res.netiface && Array.isArray(res.netiface)) {
res.netiface.forEach(iface => {
let ips = [];
if (res.netaddr && Array.isArray(res.netaddr)) {
res.netaddr.forEach(addr => {
if (addr.iface === iface.name && addr.address && addr.proto === 'ipv4') {
ips.push(addr.address);
} else if (addr.iface === iface.name && addr.address && !addr.proto && addr.address.includes('.')) {
ips.push(addr.address);
}
});
}
networkArray.push({
name: iface.name,
mac: iface.mac,
state: iface.state,
ips: ips
});
});
}
if (networkArray.length > 0) {
let validIfaces = networkArray.filter(n => n.mac && n.mac !== '00:00:00:00:00:00' && n.ips && n.ips.length > 0);
validIfaces.forEach(iface => {
let stateText = iface.state ? iface.state.toLowerCase() : 'unknown';
if (stateText === 'unknown') stateText = (iface.ips.length > 0 ? 'up' : 'down');
iface._stateText = stateText;
let isExtra = false;
let n = iface.name.toLowerCase();
const extraWords = ['virtual', 'vmware', 'vbox', 'vethernet', 'bluetooth', 'pseudo', 'vpn', 'loopback', 'docker', 'br-', 'tailscale', 'zerotier', 'hamachi', 'wg', 'local area connection'];
for (let word of extraWords) {
if (n.includes(word)) { isExtra = true; break; }
}
iface._isExtra = isExtra;
});
validIfaces.sort((a, b) => {
if (a._stateText === 'up' && b._stateText !== 'up') return -1;
if (a._stateText !== 'up' && b._stateText === 'up') return 1;
if (!a._isExtra && b._isExtra) return -1;
if (a._isExtra && !b._isExtra) return 1;
return 0;
});
const toggleCb = document.getElementById('toggle-extra-net');
if (toggleCb) toggleCb.checked = false;
if (validIfaces.length > 0) {
validIfaces.forEach(iface => {
let stateBadge = '';
if (iface._stateText === 'up') {
stateBadge = '<span class="flex items-center text-[9px] font-bold text-emerald-600 bg-emerald-100 px-1.5 py-0.5 rounded uppercase"><span class="w-1.5 h-1.5 rounded-full bg-emerald-500 mr-1 animate-pulse"></span>Active</span>';
} else {
stateBadge = '<span class="flex items-center text-[9px] font-bold text-rose-600 bg-rose-100 px-1.5 py-0.5 rounded uppercase"><span class="w-1.5 h-1.5 rounded-full bg-rose-500 mr-1"></span>Offline</span>';
}
const div = document.createElement('div');
div.className = `bg-slate-50 p-2 rounded border border-slate-200 ${iface._isExtra ? 'iface-extra hidden' : ''}`;
div.innerHTML = `
<div class="flex justify-between items-center mb-2 pb-1 border-b border-slate-100">
<div class="font-bold text-slate-800 text-xs uppercase" title="${iface.name}">${iface.name.length > 20 ? iface.name.substring(0,20)+'...' : iface.name}</div>
${stateBadge}
</div>
<div class="flex justify-between text-xs mb-1">
<span class="text-slate-500">IP:</span>
<span class="font-mono text-blue-600 font-medium">${iface.ips.join(', ')}</span>
</div>
<div class="flex justify-between text-xs">
<span class="text-slate-500"><?= __('MAC Address') ?>:</span>
<span class="font-mono text-slate-700">${iface.mac || 'N/A'}</span>
</div>
`;
netContainer.appendChild(div);
});
let visibleCount = validIfaces.filter(i => !i._isExtra).length;
if (visibleCount === 0) {
const placeholder = document.createElement('div');
placeholder.className = 'text-slate-400 text-center py-2 col-span-full iface-placeholder';
placeholder.innerHTML = 'No primary interfaces found. Click "Show All" to view others.';
netContainer.appendChild(placeholder);
if (toggleCb) {
toggleCb.addEventListener('change', function() {
placeholder.style.display = this.checked ? 'none' : 'block';
});
}
}
} else {
netContainer.innerHTML = '<div class="text-slate-400 text-center py-2 col-span-full">No active LAN/WiFi interfaces found</div>';
}
} else {
netContainer.innerHTML = '<div class="text-slate-400 text-center py-2 col-span-full">No network data available</div>';
} } else {
Swal.update({
icon: 'error',
title: 'Error',
html: res.message || '<?= __('Failed to load specifications.') ?>',
showConfirmButton: true
});
}
},
error: function() {
Swal.update({
icon: 'error',
title: 'Error',
html: '<?= __('Failed to connect to the server.') ?>',
showConfirmButton: true
});
}
});
};
// Resource Stats Calculation
document.addEventListener('DOMContentLoaded', async function() {
const agents = <?= json_encode($agents) ?>;
const activeAgents = agents.filter(a => a.status === 'active');
if (activeAgents.length === 0) {
document.getElementById('stat-ram-count').innerText = '0';
document.getElementById('stat-disk-count').innerText = '0';
return;
}
let ramAlertCount = 0;
let diskAlertCount = 0;
const ramThresh = <?= $_ENV['ALERT_RAM_PERCENT'] ?? 85 ?>;
const diskThresh = <?= $_ENV['ALERT_DISK_PERCENT'] ?? 85 ?>;
// Concurrency limit function to not overload the server/Wazuh API
async function processInBatches(items, batchSize) {
for (let i = 0; i < items.length; i += batchSize) {
const batch = items.slice(i, i + batchSize);
await Promise.all(batch.map(async (agent) => {
try {
let res = await fetch('<?= $baseUrl ?>/api/agent/quick-details?agent_id=' + agent.id);
let data = await res.json();
if (data.status === 'success') {
let tvId = data.tv_info ? (data.tv_info.TeamViewerID || (Array.isArray(data.tv_info) && data.tv_info[0] ? data.tv_info[0].TeamViewerID : null)) : null;
let adId = data.ad_info ? (data.ad_info.AnyDeskID || (Array.isArray(data.ad_info) && data.ad_info[0] ? data.ad_info[0].AnyDeskID : null)) : null;
let remoteInfo = [];
if (tvId && tvId !== 'Not Found') remoteInfo.push(`<span class="text-blue-600 font-bold text-[11px]"><i class="fas fa-desktop"></i> TV: ${tvId}</span>`);
if (adId && adId !== 'Not Found') remoteInfo.push(`<span class="text-rose-600 font-bold text-[11px]"><i class="fas fa-desktop"></i> AD: ${adId}</span>`);
let remoteHtml = remoteInfo.length > 0 ? remoteInfo.join('<br>') : '<span class="text-slate-400 text-[11px]">-</span>';
let agentIp = agent.ip || 'N/A';
if (agentIp !== 'N/A' && agentIp.includes(',')) {
let ips = agentIp.split(',').map(ip => ip.trim()).filter(ip => !ip.includes(':'));
agentIp = ips.length > 0 ? ips[0] : agentIp;
} else if (agentIp.includes(':')) {
agentIp = 'N/A';
}
// Check RAM
if (data.hardware && data.hardware.ram && data.hardware.ram.total && data.hardware.ram.free !== undefined) {
let total = data.hardware.ram.total;
let free = data.hardware.ram.free;
let used = Math.max(0, total - free);
let pct = Math.round((used / total) * 100);
if (pct >= ramThresh) {
ramAlertCount++;
window.ramAlertAgents.push({
id: agent.id,
name: agent.name,
ip: agentIp,
remote: remoteHtml,
pct: pct,
detail: `${(used/1024/1024).toFixed(1)} GB / ${(total/1024/1024).toFixed(1)} GB`
});
// Inject RAM warning icon into Status column via DataTables API
if (window.agentsTableDt) {
let row = window.agentsTableDt.row('#row-agent-' + agent.id);
if (row.length) {
let cell = window.agentsTableDt.cell(row, 0);
let $cellHtml = $('<div>').html(cell.data());
$cellHtml.find('.sort-key').text('1');
$cellHtml.find('.flex').append(`<span class="text-rose-500 bg-rose-100 rounded-full px-1.5 py-0.5 text-[10px] font-bold" title="พบปัญหาการใช้งาน RAM สูง (${pct}%)"><i class="fas fa-memory"></i></span>`);
cell.data($cellHtml.html());
}
}
}
}
// Check Disk
if (data.disk_info) {
let disks = Array.isArray(data.disk_info) ? data.disk_info : [data.disk_info];
let cDrive = disks.find(d => {
let name = String(d.DriveLetter || d.Drive || d.DeviceID || d.Name || '');
return name.toUpperCase().includes('C');
});
if (cDrive) {
let dTotal = parseFloat(cDrive.Total_GB || cDrive.TotalSpace || cDrive.SizeGB || (cDrive.Size ? cDrive.Size / 1073741824 : 0));
let dFree = parseFloat(cDrive.Free_GB || cDrive.FreeGB || (cDrive.FreeSpace ? cDrive.FreeSpace / 1073741824 : 0));
if (cDrive.TotalSpace && cDrive.TotalSpace > 100000) dTotal = dTotal / 1073741824;
if (cDrive.FreeSpace && cDrive.FreeSpace > 100000) dFree = dFree / 1073741824;
if (dTotal > 0) {
let dUsed = dTotal - dFree;
let dPct = Math.round((dUsed / dTotal) * 100);
if (dPct >= diskThresh) {
diskAlertCount++;
window.diskAlertAgents.push({
id: agent.id,
name: agent.name,
ip: agentIp,
remote: remoteHtml,
pct: dPct,
detail: `${dUsed.toFixed(1)} GB / ${dTotal.toFixed(1)} GB`
});
// Inject Disk warning icon into Status column via DataTables API
if (window.agentsTableDt) {
let row = window.agentsTableDt.row('#row-agent-' + agent.id);
if (row.length) {
let cell = window.agentsTableDt.cell(row, 0);
let $cellHtml = $('<div>').html(cell.data());
$cellHtml.find('.sort-key').text('1');
$cellHtml.find('.flex').append(`<span class="text-amber-500 bg-amber-100 rounded-full px-1.5 py-0.5 text-[10px] font-bold" title="พบปัญหาการใช้งาน Disk สูง (${dPct}%)"><i class="fas fa-hdd"></i></span>`);
cell.data($cellHtml.html());
}
}
}
}
}
}
}
} catch (e) {
console.error('Failed to fetch specs for ' + agent.id, e);
}
}));
}
}
// Process 5 agents at a time
await processInBatches(activeAgents, 5);
// Refresh DataTables sorting
if (window.agentsTableDt) {
window.agentsTableDt.order([0, 'asc']).draw(false);
}
document.getElementById('stat-ram-count').innerHTML = ramAlertCount > 0 ? `<span class="text-rose-600">${ramAlertCount}</span>` : '0';
document.getElementById('stat-disk-count').innerHTML = diskAlertCount > 0 ? `<span class="text-amber-500">${diskAlertCount}</span>` : '0';
});
window.ramAlertAgents = [];
window.diskAlertAgents = [];
window.showResourceDetails = function(type) {
let list = type === 'ram' ? window.ramAlertAgents : window.diskAlertAgents;
let title = type === 'ram' ? 'RAM Usage Warning' : 'Disk Usage Warning';
let icon = type === 'ram' ? '<i class="fas fa-memory text-rose-500"></i>' : '<i class="fas fa-hdd text-amber-500"></i>';
if (list.length === 0) {
Swal.fire({
icon: 'success',
title: 'All Good!',
text: 'No agents currently exceed the usage threshold.',
confirmButtonColor: '#3085d6'
});
return;
}
// Sort by highest percentage
list.sort((a, b) => b.pct - a.pct);
let rowsHtml = list.map(a => `
<tr class="border-b border-slate-100 hover:bg-slate-50 transition-colors">
<td class="px-4 py-2 text-left">
<div class="font-bold text-slate-800 text-sm">${a.name}</div>
<div class="text-[10px] text-slate-500 font-mono">${a.id}</div>
</td>
<td class="px-4 py-2 text-left text-xs font-mono text-blue-600">${a.ip}</td>
<td class="px-4 py-2 text-left">${a.remote}</td>
<td class="px-4 py-2 text-right">
<div class="font-bold text-rose-600 text-sm">${a.pct}%</div>
<div class="text-[10px] text-slate-500">${a.detail}</div>
</td>
</tr>
`).join('');
let html = `
<div class="mt-2 border-t border-slate-200">
<div class="overflow-x-auto overflow-y-auto custom-scrollbar" style="max-height: 320px;">
<table class="w-full text-sm relative">
<thead class="sticky top-0 bg-slate-50 z-10 shadow-sm">
<tr class="text-slate-500 text-xs uppercase tracking-wider">
<th class="px-4 py-2 text-left">Agent</th>
<th class="px-4 py-2 text-left">IP Address</th>
<th class="px-4 py-2 text-left">Remote ID</th>
<th class="px-4 py-2 text-right">Usage</th>
</tr>
</thead>
<tbody class="divide-y divide-slate-100">
${rowsHtml}
</tbody>
</table>
</div>
</div>
`;
Swal.fire({
title: `<div class="flex items-center gap-2 border-b-2 border-slate-200 pb-2">${icon} <span class="font-bold text-lg text-slate-800">${title}</span></div>`,
html: html,
width: '650px',
showCloseButton: true,
showConfirmButton: false,
customClass: {
popup: 'corp-card rounded-md p-0'
}
});
};
</script>
<script>
window.searchFlowLogs = function(val) {
let lowerVal = val.toLowerCase();
let catFilter = document.querySelector('select[onchange^="window.filterFlowLogs"]').value;
document.querySelectorAll('.flow-log-row').forEach(row => {
let text = row.innerText.toLowerCase();
let catMatch = (catFilter === 'All' || row.getAttribute('data-category') === catFilter);
if (catMatch && text.includes(lowerVal)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
};
window.filterFlowLogs = function(val) {
let searchVal = document.querySelector('input[onkeyup^="window.searchFlowLogs"]').value.toLowerCase();
document.querySelectorAll('.flow-log-row').forEach(row => {
let text = row.innerText.toLowerCase();
let catMatch = (val === 'All' || row.getAttribute('data-category') === val);
if (catMatch && text.includes(searchVal)) {
row.style.display = '';
} else {
row.style.display = 'none';
}
});
};
</script>
<?php
$content = ob_get_clean();
require __DIR__ . '/layout.php';
?>