1877 lines
111 KiB
PHP
1877 lines
111 KiB
PHP
<?php
|
|
session_start();
|
|
// ==========================================
|
|
// 1. ตั้งค่าการเชื่อมต่อ Wazuh API
|
|
// ==========================================
|
|
$wazuh_ip = 'wazuh.samuihospital.go.th'; // เปลี่ยนเป็น IP ของ Wazuh Manager ของคุณ
|
|
$wazuh_port = '55000';
|
|
$api_user = 'api_inventory'; // Username ของ Wazuh API
|
|
$api_pass = '@Samui@10742'; // Password ของ Wazuh API
|
|
$api_url = "https://{$wazuh_ip}:{$wazuh_port}";
|
|
|
|
// ==========================================
|
|
// 2. ตั้งค่าการเชื่อมต่อ Wazuh Indexer (ดึง Log Disk)
|
|
// ==========================================
|
|
$indexer_ip = $wazuh_ip; // ปกติจะอยู่ที่ Server เดียวกัน
|
|
$indexer_port = '9200';
|
|
$indexer_user = 'adminksh'; // User ของ Indexer (ค่าเริ่มต้นคือ admin)
|
|
$indexer_pass = '@Samui@10742'; // Password ของ Indexer (ค่าเริ่มต้น หรือที่คุณตั้งไว้)
|
|
|
|
if (isset($_GET['refresh'])) {
|
|
unset($_SESSION['wazuh_token']);
|
|
header("Location: " . strtok($_SERVER["REQUEST_URI"], '?'));
|
|
exit;
|
|
}
|
|
|
|
function call_wazuh_api($url, $method = 'GET', $token = null, $auth = null)
|
|
{
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, $method);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 20);
|
|
$headers = ["Content-Type: application/json"];
|
|
if ($auth) {
|
|
curl_setopt($ch, CURLOPT_HTTPAUTH, CURLAUTH_BASIC);
|
|
curl_setopt($ch, CURLOPT_USERPWD, $auth);
|
|
}
|
|
if ($token) {
|
|
$headers[] = "Authorization: Bearer {$token}";
|
|
}
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, $headers);
|
|
$response = curl_exec($ch);
|
|
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
if (!$response || $httpcode >= 400) return null;
|
|
return json_decode($response, true);
|
|
}
|
|
|
|
function get_wazuh_token()
|
|
{
|
|
global $api_url, $api_user, $api_pass;
|
|
if (!isset($_SESSION['wazuh_token'])) {
|
|
$auth = call_wazuh_api("{$api_url}/security/user/authenticate", 'POST', null, "{$api_user}:{$api_pass}");
|
|
if ($auth && isset($auth['data']['token'])) {
|
|
$_SESSION['wazuh_token'] = $auth['data']['token'];
|
|
} else {
|
|
return null;
|
|
}
|
|
}
|
|
return $_SESSION['wazuh_token'];
|
|
}
|
|
|
|
function check_indexer_connection()
|
|
{
|
|
global $indexer_ip, $indexer_port, $indexer_user, $indexer_pass;
|
|
$url = "https://{$indexer_ip}:{$indexer_port}/";
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 3);
|
|
curl_setopt($ch, CURLOPT_USERPWD, "{$indexer_user}:{$indexer_pass}");
|
|
curl_exec($ch);
|
|
$httpcode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
curl_close($ch);
|
|
if ($httpcode == 200) return 'OK';
|
|
if ($httpcode == 401) return 'AUTH_FAILED';
|
|
return 'CONNECTION_ERROR';
|
|
}
|
|
|
|
function get_custom_inventory_from_indexer($agent_id, $location)
|
|
{
|
|
global $indexer_ip, $indexer_port, $indexer_user, $indexer_pass;
|
|
$url = "https://{$indexer_ip}:{$indexer_port}/wazuh-alerts-*/_search";
|
|
$payload = json_encode([
|
|
'size' => 1,
|
|
'sort' => ['@timestamp' => 'desc'],
|
|
'query' => ['bool' => ['must' => [['match' => ['agent.id' => $agent_id]], ['match' => ['location' => $location]]]]]
|
|
]);
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 5);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
|
|
curl_setopt($ch, CURLOPT_USERPWD, "{$indexer_user}:{$indexer_pass}");
|
|
$response = curl_exec($ch);
|
|
curl_close($ch);
|
|
|
|
if ($response) {
|
|
$data = json_decode($response, true);
|
|
if (isset($data['hits']['hits'][0]['_source']['full_log'])) {
|
|
$log = $data['hits']['hits'][0]['_source']['full_log'];
|
|
$log = preg_replace('/^ossec: output: \'' . preg_quote($location, '/') . '\':\s*/', '', $log);
|
|
|
|
// ป้องกัน json_decode พังถ้ารูปแบบผิด
|
|
$json = json_decode($log, true);
|
|
return $json !== null ? $json : $log;
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function get_indexer_metadata()
|
|
{
|
|
global $indexer_ip, $indexer_port, $indexer_user, $indexer_pass;
|
|
$url = "https://{$indexer_ip}:{$indexer_port}/wazuh-alerts-*/_search";
|
|
$payload = json_encode([
|
|
'size' => 10000,
|
|
'sort' => ['@timestamp' => 'desc'],
|
|
'query' => ['terms' => ['location' => ['teamviewer_inventory', 'anydesk_inventory', 'disk_inventory', 'model_inventory', 'manufacturer_inventory']]],
|
|
'_source' => ['agent.id', 'location', 'full_log', 'agent.labels']
|
|
]);
|
|
|
|
$ch = curl_init();
|
|
curl_setopt($ch, CURLOPT_URL, $url);
|
|
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
|
|
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, 'POST');
|
|
curl_setopt($ch, CURLOPT_POSTFIELDS, $payload);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false);
|
|
curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false);
|
|
curl_setopt($ch, CURLOPT_TIMEOUT, 6);
|
|
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Content-Type: application/json"]);
|
|
curl_setopt($ch, CURLOPT_USERPWD, "{$indexer_user}:{$indexer_pass}");
|
|
$response = curl_exec($ch);
|
|
curl_close($ch);
|
|
|
|
$metadata = [];
|
|
if ($response) {
|
|
$data = json_decode($response, true);
|
|
if (isset($data['hits']['hits'])) {
|
|
foreach ($data['hits']['hits'] as $hit) {
|
|
$src = $hit['_source'];
|
|
$aid = $src['agent']['id'] ?? '';
|
|
$loc = $src['location'] ?? '';
|
|
$log = $src['full_log'] ?? '';
|
|
$labels = $src['agent']['labels'] ?? null;
|
|
|
|
if (!$aid) continue;
|
|
if (!isset($metadata[$aid])) {
|
|
$metadata[$aid] = ['tv' => null, 'ad' => null, 'asset_id' => null, 'location' => null, 'disk_c_total' => null, 'disk_c_free' => null, 'model' => null, 'manufacturer' => null];
|
|
}
|
|
|
|
if ($labels) {
|
|
if ($metadata[$aid]['asset_id'] === null && isset($labels['asset_id'])) $metadata[$aid]['asset_id'] = $labels['asset_id'];
|
|
if ($metadata[$aid]['location'] === null && isset($labels['location'])) $metadata[$aid]['location'] = $labels['location'];
|
|
}
|
|
|
|
if ($loc === 'teamviewer_inventory' && $metadata[$aid]['tv'] === null) {
|
|
$log = preg_replace('/^ossec: output: \'teamviewer_inventory\':\s*/', '', $log);
|
|
$json = json_decode($log, true);
|
|
$metadata[$aid]['tv'] = ($json && isset($json['TeamViewerID']) && $json['TeamViewerID'] !== 'Not Found') ? $json['TeamViewerID'] : 'None';
|
|
}
|
|
if ($loc === 'anydesk_inventory' && $metadata[$aid]['ad'] === null) {
|
|
$log = preg_replace('/^ossec: output: \'anydesk_inventory\':\s*/', '', $log);
|
|
$json = json_decode($log, true);
|
|
$metadata[$aid]['ad'] = ($json && isset($json['AnyDeskID']) && $json['AnyDeskID'] !== 'Not Found') ? $json['AnyDeskID'] : 'None';
|
|
}
|
|
|
|
// 🌟 อัปเดตตัวกรอง Disk C สำหรับฝั่ง PHP (ดึงเข้า Excel) 🌟
|
|
if ($loc === 'disk_inventory' && $metadata[$aid]['disk_c_total'] === null) {
|
|
$log = preg_replace('/^ossec: output: \'disk_inventory\':\s*/', '', $log);
|
|
$json = json_decode($log, true);
|
|
if ($json && !is_array($json) && is_object((object)$json)) {
|
|
$json = [$json];
|
|
}
|
|
if (is_array($json)) {
|
|
foreach ($json as $disk) {
|
|
// รองรับทั้งคีย์ DriveLetter, Drive, DeviceID, Name
|
|
$dLetter = $disk['DriveLetter'] ?? $disk['Drive'] ?? $disk['DeviceID'] ?? $disk['Name'] ?? '';
|
|
// เช็คให้มีตัวอักษร C ไม่ว่าจะมาแบบ "C:", "C:\", "C"
|
|
if (strpos(strtoupper($dLetter), 'C') !== false) {
|
|
// รองรับขนาดทั้งหน่วย GB และ Bytes
|
|
$t = $disk['Total_GB'] ?? $disk['SizeGB'] ?? (isset($disk['Size']) ? round($disk['Size'] / 1073741824, 2) : 0);
|
|
$f = $disk['Free_GB'] ?? $disk['FreeGB'] ?? (isset($disk['FreeSpace']) ? round($disk['FreeSpace'] / 1073741824, 2) : 0);
|
|
$metadata[$aid]['disk_c_total'] = $t ?: 'N/A';
|
|
$metadata[$aid]['disk_c_free'] = $f ?: 'N/A';
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if ($loc === 'model_inventory' && $metadata[$aid]['model'] === null) {
|
|
$log = preg_replace('/^ossec: output: \'model_inventory\':\s*/', '', $log);
|
|
$json = json_decode($log, true);
|
|
$metadata[$aid]['model'] = ($json && isset($json['PCModel'])) ? $json['PCModel'] : 'Unknown';
|
|
}
|
|
if ($loc === 'manufacturer_inventory' && $metadata[$aid]['manufacturer'] === null) {
|
|
$log = preg_replace('/^ossec: output: \'manufacturer_inventory\':\s*/', '', $log);
|
|
$json = json_decode($log, true);
|
|
$metadata[$aid]['manufacturer'] = ($json && isset($json['PCManufacturer'])) ? $json['PCManufacturer'] : 'Unknown';
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return $metadata;
|
|
}
|
|
|
|
function format_os_name($agent)
|
|
{
|
|
if (($agent['id'] ?? '') === '000') return 'Wazuh Server';
|
|
$os_name = $agent['os']['name'] ?? $agent['os']['platform'] ?? 'Unknown';
|
|
$nameLower = strtolower($os_name);
|
|
$version = $agent['os']['version'] ?? '';
|
|
|
|
if (strpos($nameLower, 'windows 11') !== false) return 'Windows 11';
|
|
if (strpos($nameLower, 'windows 10') !== false) return 'Windows 10';
|
|
if (strpos($nameLower, 'windows 7') !== false) return 'Windows 7';
|
|
if (strpos($nameLower, 'windows 8') !== false) return 'Windows 8';
|
|
if (strpos($nameLower, 'windows server 2022') !== false) return 'Windows Server 2022';
|
|
if (strpos($nameLower, 'windows server 2019') !== false) return 'Windows Server 2019';
|
|
if (strpos($nameLower, 'windows server 2016') !== false) return 'Windows Server 2016';
|
|
if (strpos($nameLower, 'windows server 2012') !== false) return 'Windows Server 2012';
|
|
|
|
if (strpos($nameLower, 'ubuntu') !== false) {
|
|
if (preg_match('/Ubuntu (\d+\.\d+)/i', $os_name, $m)) return 'Ubuntu ' . $m[1];
|
|
return 'Ubuntu';
|
|
}
|
|
if (strpos($nameLower, 'centos') !== false) {
|
|
if (preg_match('/CentOS.*?(\d+)/i', $os_name, $m)) return 'CentOS ' . $m[1];
|
|
return 'CentOS';
|
|
}
|
|
if (strpos($nameLower, 'mac') !== false || strpos($nameLower, 'darwin') !== false) {
|
|
return $version ? 'macOS ' . $version : 'macOS';
|
|
}
|
|
return $os_name;
|
|
}
|
|
|
|
// ==========================================
|
|
// 3. ระบบ AJAX สำหรับดึงข้อมูล
|
|
// ==========================================
|
|
if (isset($_GET['action']) && $_GET['action'] === 'get_specs' && isset($_GET['agent_id'])) {
|
|
header('Content-Type: application/json');
|
|
$token = get_wazuh_token();
|
|
if (!$token) {
|
|
echo json_encode(['error' => 'Unauthorized']);
|
|
exit;
|
|
}
|
|
|
|
$agent_id = htmlspecialchars($_GET['agent_id']);
|
|
|
|
$hw_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/hardware", 'GET', $token);
|
|
$netiface_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/netiface", 'GET', $token);
|
|
$netaddr_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/netaddr", 'GET', $token);
|
|
$netproto_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/netproto", 'GET', $token);
|
|
$packages_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/packages?limit=2000", 'GET', $token);
|
|
$hotfixes_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/hotfixes?limit=1000", 'GET', $token);
|
|
|
|
$disk_info = get_custom_inventory_from_indexer($agent_id, 'disk_inventory');
|
|
$tv_info = get_custom_inventory_from_indexer($agent_id, 'teamviewer_inventory');
|
|
$anydesk_info = get_custom_inventory_from_indexer($agent_id, 'anydesk_inventory');
|
|
|
|
echo json_encode([
|
|
'hardware' => $hw_res['data']['affected_items'][0] ?? null,
|
|
'disk_info' => $disk_info,
|
|
'tv_info' => $tv_info,
|
|
'anydesk_info' => $anydesk_info,
|
|
'netiface' => $netiface_res['data']['affected_items'] ?? [],
|
|
'netaddr' => $netaddr_res['data']['affected_items'] ?? [],
|
|
'netproto' => $netproto_res['data']['affected_items'] ?? [],
|
|
'packages' => $packages_res['data']['affected_items'] ?? [],
|
|
'hotfixes' => $hotfixes_res['data']['affected_items'] ?? []
|
|
]);
|
|
exit;
|
|
}
|
|
|
|
if (isset($_GET['action']) && $_GET['action'] === 'get_dashboard_data' && isset($_GET['agent_id'])) {
|
|
header('Content-Type: application/json');
|
|
$token = get_wazuh_token();
|
|
$agent_id = htmlspecialchars($_GET['agent_id']);
|
|
$hw_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/hardware", 'GET', $token);
|
|
$packages_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/packages?limit=5000", 'GET', $token);
|
|
$hotfixes_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/hotfixes?limit=2000", 'GET', $token);
|
|
echo json_encode(['hardware' => $hw_res['data']['affected_items'][0] ?? null, 'packages' => $packages_res['data']['affected_items'] ?? [], 'hotfixes' => $hotfixes_res['data']['affected_items'] ?? []]);
|
|
exit;
|
|
}
|
|
|
|
if (isset($_GET['action']) && $_GET['action'] === 'get_processes' && isset($_GET['agent_id'])) {
|
|
header('Content-Type: application/json');
|
|
$token = get_wazuh_token();
|
|
$agent_id = htmlspecialchars($_GET['agent_id']);
|
|
$processes_res = call_wazuh_api("{$api_url}/syscollector/{$agent_id}/processes?limit=5000", 'GET', $token);
|
|
echo json_encode(['processes' => $processes_res['data']['affected_items'] ?? []]);
|
|
exit;
|
|
}
|
|
|
|
// ==========================================
|
|
// 4. ดึงข้อมูลหน้าหลัก
|
|
// ==========================================
|
|
$token = get_wazuh_token();
|
|
$indexer_status = check_indexer_connection();
|
|
$agents = [];
|
|
$all_metadata = [];
|
|
$unique_os_list = [];
|
|
|
|
if ($token) {
|
|
$agents_response = call_wazuh_api("{$api_url}/agents?limit=10000", 'GET', $token);
|
|
if ($agents_response && isset($agents_response['data']['affected_items'])) {
|
|
$agents = $agents_response['data']['affected_items'];
|
|
|
|
foreach ($agents as $agent) {
|
|
$formatted_os = format_os_name($agent);
|
|
$unique_os_list[$formatted_os] = true;
|
|
}
|
|
$unique_os_list = array_keys($unique_os_list);
|
|
sort($unique_os_list);
|
|
}
|
|
|
|
if ($indexer_status === 'OK') {
|
|
$all_metadata = get_indexer_metadata();
|
|
foreach ($agents as &$ag) {
|
|
$aid = $ag['id'];
|
|
if (isset($all_metadata[$aid])) {
|
|
if (!isset($ag['labels'])) $ag['labels'] = [];
|
|
if (isset($all_metadata[$aid]['asset_id'])) $ag['labels']['asset_id'] = $all_metadata[$aid]['asset_id'];
|
|
if (isset($all_metadata[$aid]['location'])) $ag['labels']['location'] = $all_metadata[$aid]['location'];
|
|
}
|
|
}
|
|
unset($ag);
|
|
}
|
|
}
|
|
?>
|
|
|
|
<!DOCTYPE html>
|
|
<html lang="th">
|
|
|
|
<head>
|
|
<meta charset="UTF-8">
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
<title>Enterprise Asset Management</title>
|
|
|
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
|
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
|
<link href="https://fonts.googleapis.com/css2?family=Sarabun:wght@300;400;500;600;700;800&display=swap" rel="stylesheet">
|
|
|
|
<script src="https://cdn.tailwindcss.com"></script>
|
|
<script>
|
|
tailwind.config = {
|
|
theme: {
|
|
extend: {
|
|
fontFamily: {
|
|
sans: ['Sarabun', 'sans-serif']
|
|
},
|
|
colors: {
|
|
primary: '#2563EB',
|
|
secondary: '#475569',
|
|
success: '#059669',
|
|
warning: '#D97706',
|
|
danger: '#DC2626',
|
|
info: '#0284C7'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
</script>
|
|
|
|
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
|
|
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
<script src="https://cdn.jsdelivr.net/npm/xlsx/dist/xlsx.full.min.js"></script>
|
|
|
|
<script src="https://code.jquery.com/jquery-3.7.0.min.js"></script>
|
|
<script src="https://cdn.datatables.net/1.13.6/js/jquery.dataTables.min.js"></script>
|
|
|
|
<style>
|
|
/* =========================================
|
|
🌟 Modern 3D & DataTables Layout Fixes 🌟
|
|
========================================= */
|
|
|
|
body {
|
|
background-color: #e2e8f0;
|
|
font-family: 'Sarabun', sans-serif !important;
|
|
}
|
|
|
|
/* 3D Card Hover Effects */
|
|
.glass-card {
|
|
background-color: rgba(255, 255, 255, 0.95);
|
|
backdrop-filter: blur(10px);
|
|
border: 1px solid rgba(255, 255, 255, 0.5);
|
|
box-shadow: 0 4px 6px -1px rgba(0, 0, 0, 0.05), 0 2px 4px -1px rgba(0, 0, 0, 0.03);
|
|
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
|
}
|
|
|
|
.glass-card:hover {
|
|
transform: translateY(-4px);
|
|
box-shadow: 0 20px 25px -5px rgba(0, 0, 0, 0.1), 0 10px 10px -5px rgba(0, 0, 0, 0.04);
|
|
border-color: rgba(37, 99, 235, 0.3);
|
|
}
|
|
|
|
.text-gradient {
|
|
background-clip: text;
|
|
-webkit-background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
background-image: linear-gradient(to right, #1e3a8a, #3b82f6);
|
|
}
|
|
|
|
/* 🌟 DataTables Layout Correcting (จัดบรรทัด Search & Length) 🌟 */
|
|
.dataTables_wrapper {
|
|
width: 100%;
|
|
color: #334155;
|
|
font-family: 'Sarabun', sans-serif;
|
|
}
|
|
|
|
@media (min-width: 768px) {
|
|
.dataTables_wrapper .dataTables_length {
|
|
float: left;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_filter {
|
|
float: right;
|
|
margin-bottom: 1.5rem;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_info {
|
|
float: left;
|
|
clear: both;
|
|
padding-top: 1.5rem;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_paginate {
|
|
float: right;
|
|
padding-top: 1.5rem;
|
|
}
|
|
}
|
|
|
|
@media (max-width: 767px) {
|
|
|
|
.dataTables_wrapper .dataTables_length,
|
|
.dataTables_wrapper .dataTables_filter {
|
|
float: none;
|
|
text-align: left;
|
|
margin-bottom: 1rem;
|
|
display: block;
|
|
}
|
|
}
|
|
|
|
.dataTables_wrapper::after {
|
|
content: "";
|
|
display: table;
|
|
clear: both;
|
|
}
|
|
|
|
/* 🌟 แก้ปัญหาลูกศรซ้อนและขยายช่อง Show Entries 🌟 */
|
|
.dataTables_wrapper .dataTables_length select {
|
|
-webkit-appearance: none !important;
|
|
-moz-appearance: none !important;
|
|
appearance: none !important;
|
|
background-color: #f8fafc;
|
|
border: 1px solid #cbd5e1;
|
|
color: #1e293b;
|
|
font-weight: 600;
|
|
border-radius: 0.5rem;
|
|
padding: 0.4rem 2.5rem 0.4rem 1rem !important;
|
|
min-width: 90px !important;
|
|
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 16 16'%3e%3cpath fill='none' stroke='%23475569' stroke-linecap='round' stroke-linejoin='round' stroke-width='2' d='m2 5 6 6 6-6'/%3e%3c/svg%3e") !important;
|
|
background-repeat: no-repeat !important;
|
|
background-position: right 0.75rem center !important;
|
|
background-size: 16px 12px !important;
|
|
outline: none;
|
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_length select:focus {
|
|
border-color: #2563eb;
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_filter input {
|
|
border: 1px solid #cbd5e1;
|
|
border-radius: 0.5rem;
|
|
padding: 0.4rem 1rem;
|
|
margin-left: 0.5rem;
|
|
background-color: #f8fafc;
|
|
outline: none;
|
|
font-family: 'Sarabun', sans-serif;
|
|
box-shadow: inset 0 1px 2px rgba(0, 0, 0, 0.05);
|
|
transition: all 0.2s;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_filter input:focus {
|
|
border-color: #2563eb;
|
|
box-shadow: 0 0 0 3px rgba(37, 99, 235, 0.2);
|
|
}
|
|
|
|
/* Table Styles */
|
|
table.dataTable {
|
|
width: 100% !important;
|
|
border-collapse: collapse !important;
|
|
margin-top: 1rem !important;
|
|
margin-bottom: 1rem !important;
|
|
}
|
|
|
|
table.dataTable thead th {
|
|
border-bottom: 2px solid #e2e8f0 !important;
|
|
padding: 1rem;
|
|
color: #475569;
|
|
text-transform: uppercase;
|
|
font-size: 0.85rem;
|
|
font-weight: 700;
|
|
background-color: #f8fafc;
|
|
letter-spacing: 0.5px;
|
|
}
|
|
|
|
table.dataTable tbody td {
|
|
border-bottom: 1px solid #f1f5f9;
|
|
padding: 1.2rem 1rem;
|
|
vertical-align: middle;
|
|
}
|
|
|
|
table.dataTable tbody tr:hover {
|
|
background-color: #f8fafc;
|
|
}
|
|
|
|
/* Pagination */
|
|
.dataTables_wrapper .dataTables_info {
|
|
color: #64748b;
|
|
font-size: 0.95rem;
|
|
font-weight: 500;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_paginate {
|
|
padding-top: 1rem;
|
|
float: right;
|
|
display: flex;
|
|
gap: 0.25rem;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_paginate .paginate_button {
|
|
padding: 0.5rem 1rem;
|
|
border-radius: 0.5rem;
|
|
border: 1px solid transparent;
|
|
cursor: pointer;
|
|
color: #475569;
|
|
font-weight: 600;
|
|
transition: all 0.2s;
|
|
margin-left: 4px;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_paginate .paginate_button:hover {
|
|
background-color: #f1f5f9;
|
|
color: #2563eb;
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_paginate .paginate_button.current {
|
|
background: linear-gradient(to bottom right, #2563eb, #1d4ed8);
|
|
color: #ffffff !important;
|
|
box-shadow: 0 4px 6px -1px rgba(37, 99, 235, 0.3);
|
|
}
|
|
|
|
.dataTables_wrapper .dataTables_paginate .paginate_button.disabled {
|
|
opacity: 0.5;
|
|
cursor: not-allowed;
|
|
}
|
|
|
|
.hide-scrollbar::-webkit-scrollbar {
|
|
display: none;
|
|
}
|
|
|
|
.hide-scrollbar {
|
|
-ms-overflow-style: none;
|
|
scrollbar-width: none;
|
|
}
|
|
|
|
/* Tooltip Native CSS */
|
|
[data-tooltip] {
|
|
position: relative;
|
|
cursor: help;
|
|
}
|
|
|
|
[data-tooltip]:hover::before {
|
|
content: attr(data-tooltip);
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(-8px);
|
|
background-color: #0f172a;
|
|
color: white;
|
|
padding: 6px 12px;
|
|
border-radius: 6px;
|
|
font-size: 0.85rem;
|
|
white-space: nowrap;
|
|
z-index: 50;
|
|
box-shadow: 0 10px 15px -3px rgba(0, 0, 0, 0.1);
|
|
}
|
|
|
|
[data-tooltip]:hover::after {
|
|
content: '';
|
|
position: absolute;
|
|
bottom: 100%;
|
|
left: 50%;
|
|
transform: translateX(-50%) translateY(0);
|
|
border-width: 6px;
|
|
border-style: solid;
|
|
border-color: #0f172a transparent transparent transparent;
|
|
}
|
|
</style>
|
|
</head>
|
|
|
|
<body class="text-slate-800 antialiased">
|
|
|
|
<div class="bg-gradient-to-r from-white to-slate-50 border-b border-slate-200 py-5 mb-8 shadow-[0_4px_20px_-2px_rgba(0,0,0,0.05)]">
|
|
<div class="max-w-[1400px] mx-auto px-4 flex flex-wrap justify-between items-center gap-4">
|
|
<h3 class="text-2xl font-black m-0 flex items-center text-gradient tracking-tight">
|
|
<i class="fa-solid fa-shield-halved mr-3 text-blue-600"></i> Enterprise Asset Management
|
|
</h3>
|
|
<div class="flex items-center gap-3">
|
|
<span class="inline-flex items-center px-4 py-2 rounded-xl text-sm font-bold bg-slate-100 text-slate-700 border border-slate-200 shadow-inner">
|
|
<i class="fa-solid fa-desktop mr-2 text-slate-500"></i> Total Assets: <?= count($agents) ?>
|
|
</span>
|
|
<button onclick="exportToExcel()" class="inline-flex items-center px-5 py-2 text-sm font-bold rounded-xl text-white bg-gradient-to-r from-emerald-500 to-green-600 hover:from-emerald-600 hover:to-green-700 transition-all shadow-lg shadow-emerald-500/30 transform hover:-translate-y-0.5">
|
|
<i class="fa-solid fa-file-excel mr-2"></i> Export Excel
|
|
</button>
|
|
<a href="?refresh=1" class="inline-flex items-center px-5 py-2 text-sm font-bold rounded-xl text-slate-700 bg-white border border-slate-300 hover:bg-slate-50 transition-all shadow-sm hover:shadow-md transform hover:-translate-y-0.5">
|
|
<i class="fa-solid fa-rotate-right mr-2 text-primary"></i> Refresh
|
|
</a>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="max-w-[1400px] mx-auto px-4 mb-16">
|
|
|
|
<?php if (!$token): ?>
|
|
<div class="mb-6 bg-gradient-to-r from-red-50 to-red-100 border border-red-200 p-5 rounded-2xl flex items-center shadow-sm">
|
|
<i class="fa-solid fa-triangle-exclamation text-3xl text-red-600 mr-5"></i>
|
|
<div><strong class="text-red-800 text-lg">Wazuh API Error:</strong> <span class="text-red-700 font-medium">ไม่สามารถยืนยันตัวตนได้ กรุณาตรวจสอบ IP หรือ Credentials</span></div>
|
|
</div>
|
|
<?php endif; ?>
|
|
<?php if ($token && $indexer_status === 'AUTH_FAILED'): ?>
|
|
<div class="mb-6 bg-gradient-to-r from-amber-50 to-amber-100 border border-amber-200 p-5 rounded-2xl flex items-center shadow-sm">
|
|
<i class="fa-solid fa-lock text-3xl text-warning mr-5"></i>
|
|
<div><strong class="text-amber-800 text-lg">Wazuh Indexer Warning:</strong> <br><span class="text-amber-700 font-medium">รหัสผ่าน Indexer ผิดพลาด ระบบจะไม่สามารถแสดงข้อมูล Custom Logs (เช่น PC Model, Remote ID)</span></div>
|
|
</div>
|
|
<?php elseif ($token && $indexer_status === 'CONNECTION_ERROR'): ?>
|
|
<div class="mb-6 bg-gradient-to-r from-amber-50 to-amber-100 border border-amber-200 p-5 rounded-2xl flex items-center shadow-sm">
|
|
<i class="fa-solid fa-plug-circle-xmark text-3xl text-warning mr-5"></i>
|
|
<div><strong class="text-amber-800 text-lg">Wazuh Indexer Disconnected:</strong> <br><span class="text-amber-700 font-medium">ไม่สามารถเชื่อมต่อ Indexer พอร์ต 9200 ได้</span></div>
|
|
</div>
|
|
<?php endif; ?>
|
|
|
|
<?php if (empty($agents) && $token): ?>
|
|
<div class="mb-6 bg-blue-50 border border-blue-200 p-5 rounded-2xl flex items-center text-primary font-bold shadow-sm">
|
|
<i class="fa-solid fa-circle-info text-2xl mr-4"></i> Connected to API successfully, but no agents found.
|
|
</div>
|
|
<?php elseif ($token): ?>
|
|
|
|
<div class="grid grid-cols-1 lg:grid-cols-3 gap-6 mb-8">
|
|
<div class="glass-card rounded-2xl p-6">
|
|
<h6 class="font-black text-slate-800 text-base mb-5 flex items-center pb-3 border-b border-slate-100 uppercase tracking-wide">
|
|
<span class="w-1.5 h-5 rounded-full bg-gradient-to-b from-blue-500 to-blue-700 mr-3 shadow-sm"></span> Operating Systems
|
|
</h6>
|
|
<div class="relative h-[250px] w-full flex justify-center"><canvas id="osChart"></canvas></div>
|
|
</div>
|
|
<div class="glass-card rounded-2xl p-6">
|
|
<h6 class="font-black text-slate-800 text-base mb-5 flex items-center pb-3 border-b border-slate-100 uppercase tracking-wide">
|
|
<span class="w-1.5 h-5 rounded-full bg-gradient-to-b from-sky-400 to-sky-600 mr-3 shadow-sm"></span> Hardware Models
|
|
</h6>
|
|
<div class="relative h-[250px] w-full flex justify-center"><canvas id="hwChart"></canvas></div>
|
|
</div>
|
|
<div class="glass-card rounded-2xl p-6">
|
|
<h6 class="font-black text-slate-800 text-base mb-5 flex items-center pb-3 border-b border-slate-100 uppercase tracking-wide">
|
|
<span class="w-1.5 h-5 rounded-full bg-gradient-to-b from-emerald-400 to-emerald-600 mr-3 shadow-sm"></span> RAM Distribution
|
|
</h6>
|
|
<div class="relative h-[250px] w-full flex justify-center items-center" id="ramChartContainer">
|
|
<div class="text-center text-slate-400">
|
|
<div class="inline-flex justify-center items-center w-20 h-20 rounded-full bg-slate-50 border border-slate-100 mb-3 shadow-inner">
|
|
<i class="fa-solid fa-chart-pie text-4xl text-slate-300"></i>
|
|
</div><br>
|
|
<span class="text-sm font-bold tracking-wide">REQUIRES SCAN</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-gradient-to-br from-slate-800 to-slate-900 rounded-2xl p-8 mb-8 shadow-xl shadow-slate-900/20 border border-slate-700 relative overflow-hidden group">
|
|
<div class="absolute top-0 right-0 -mt-10 -mr-10 w-64 h-64 bg-blue-500 opacity-20 rounded-full blur-3xl group-hover:bg-blue-400 transition-colors duration-700"></div>
|
|
<div class="absolute bottom-0 left-10 -mb-10 w-32 h-32 bg-emerald-500 opacity-10 rounded-full blur-2xl"></div>
|
|
|
|
<div class="flex flex-col md:flex-row items-center justify-between relative z-10">
|
|
<div class="mb-5 md:mb-0 text-center md:text-left">
|
|
<h4 class="text-white font-black text-2xl mb-2 tracking-wide flex items-center justify-center md:justify-start">
|
|
<i class="fa-solid fa-radar text-sky-400 mr-3 drop-shadow-[0_0_8px_rgba(56,189,248,0.5)]"></i> GLOBAL DEEP SCAN
|
|
</h4>
|
|
<p class="text-slate-300 text-sm m-0 font-medium">Run a comprehensive scan to fetch Live RAM, Software, and Updates across all endpoints.</p>
|
|
</div>
|
|
<button id="btnScanGlobal" class="px-8 py-3 bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-500 hover:to-indigo-500 text-white font-bold rounded-xl shadow-lg shadow-blue-600/30 transition-all transform hover:-translate-y-1" onclick="startGlobalScan()">
|
|
<i class="fa-solid fa-magnifying-glass mr-2"></i> Start Scan
|
|
</button>
|
|
</div>
|
|
|
|
<div id="scanProgressArea" class="mt-6 hidden relative z-10">
|
|
<div class="flex justify-between mb-2">
|
|
<span class="text-sm font-bold text-slate-300 uppercase tracking-wider" id="scanStatusText">Scanning Endpoints...</span>
|
|
<span class="text-sm font-black text-sky-400" id="scanPercentage">0%</span>
|
|
</div>
|
|
<div class="w-full bg-slate-950 rounded-full h-3 shadow-inner overflow-hidden border border-slate-700">
|
|
<div id="scanProgressBar" class="bg-gradient-to-r from-blue-500 to-sky-400 h-full rounded-full transition-all duration-300"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="glass-card rounded-2xl overflow-hidden mb-10">
|
|
|
|
<div class="flex overflow-x-auto hide-scrollbar bg-slate-50 p-2 gap-2 border-b border-slate-200">
|
|
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-primary bg-white shadow-sm border border-slate-200 whitespace-nowrap outline-none transition-all" data-target="#tab-inventory">
|
|
<i class="fa-solid fa-list text-primary mr-1.5"></i> Agent Inventory
|
|
</button>
|
|
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-slate-500 hover:text-slate-800 hover:bg-white whitespace-nowrap outline-none transition-all" data-target="#tab-asset-filter">
|
|
<i class="fa-solid fa-filter text-slate-400 mr-1.5"></i> Asset Filter
|
|
</button>
|
|
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-slate-500 hover:text-slate-800 hover:bg-white whitespace-nowrap outline-none transition-all" data-target="#tab-global-software">
|
|
<i class="fa-solid fa-box-open text-slate-400 mr-1.5"></i> Global Software
|
|
</button>
|
|
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-slate-500 hover:text-slate-800 hover:bg-white whitespace-nowrap outline-none transition-all" data-target="#tab-global-updates">
|
|
<i class="fa-brands fa-windows text-slate-400 mr-1.5"></i> Global Update
|
|
</button>
|
|
<button class="tab-btn px-6 py-2.5 text-sm font-bold rounded-lg text-slate-500 hover:text-slate-800 hover:bg-white whitespace-nowrap outline-none transition-all" data-target="#tab-global-process">
|
|
<i class="fa-solid fa-terminal text-slate-400 mr-1.5"></i> Check Process
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6">
|
|
|
|
<div id="tab-inventory" class="tab-pane block">
|
|
<div class="overflow-x-auto pb-4">
|
|
<table id="mainAgentTable" class="w-full text-left">
|
|
<thead>
|
|
<tr>
|
|
<th class="w-16 text-center">OS</th>
|
|
<th>Endpoint & Asset Info</th>
|
|
<th>IP Address</th>
|
|
<th>Version</th>
|
|
<th>Status</th>
|
|
<th class="text-center">Remote</th>
|
|
<th class="text-right">Actions</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-slate-700 font-medium">
|
|
<?php foreach ($agents as $agent):
|
|
$os_platform = strtolower($agent['os']['platform'] ?? '');
|
|
if ($agent['id'] === '000') $os_platform = 'wazuh server';
|
|
|
|
$icon = 'fa-solid fa-desktop text-slate-300';
|
|
if (strpos($os_platform, 'windows') !== false) $icon = 'fa-brands fa-windows text-blue-500';
|
|
elseif (strpos($os_platform, 'ubuntu') !== false || strpos($os_platform, 'linux') !== false || strpos($os_platform, 'centos') !== false) $icon = 'fa-brands fa-linux text-orange-500';
|
|
elseif (strpos($os_platform, 'darwin') !== false || strpos($os_platform, 'mac') !== false) $icon = 'fa-brands fa-apple text-slate-600';
|
|
elseif (strpos($os_platform, 'server') !== false) $icon = 'fa-solid fa-server text-emerald-500';
|
|
|
|
$status = strtolower($agent['status'] ?? 'unknown');
|
|
$status_bg = $status === 'active' ? 'bg-green-100 text-success border border-green-200' : ($status === 'disconnected' ? 'bg-red-100 text-danger border border-red-200' : 'bg-slate-100 text-slate-600 border border-slate-200');
|
|
$status_icon = $status === 'active' ? 'fa-solid fa-circle-check' : 'fa-solid fa-circle-xmark';
|
|
|
|
$aid = $agent['id'];
|
|
$tv_id = (isset($all_metadata[$aid]['tv']) && $all_metadata[$aid]['tv'] !== 'None') ? htmlspecialchars($all_metadata[$aid]['tv']) : null;
|
|
$ad_id = (isset($all_metadata[$aid]['ad']) && $all_metadata[$aid]['ad'] !== 'None') ? htmlspecialchars($all_metadata[$aid]['ad']) : null;
|
|
$pc_model = (isset($all_metadata[$aid]['model']) && $all_metadata[$aid]['model'] !== 'Unknown') ? htmlspecialchars($all_metadata[$aid]['model']) : null;
|
|
$pc_manufacturer = (isset($all_metadata[$aid]['manufacturer']) && $all_metadata[$aid]['manufacturer'] !== 'Unknown') ? htmlspecialchars($all_metadata[$aid]['manufacturer']) : null;
|
|
|
|
$asset_id = isset($all_metadata[$aid]['asset_id']) ? htmlspecialchars($all_metadata[$aid]['asset_id']) : null;
|
|
$location = isset($all_metadata[$aid]['location']) ? htmlspecialchars($all_metadata[$aid]['location']) : null;
|
|
?>
|
|
<tr class="group">
|
|
<td data-sort="<?= $os_platform ?>" class="text-center"><i class="<?= $icon ?> text-3xl group-hover:scale-110 transition-transform"></i></td>
|
|
<td>
|
|
<div class="font-black text-slate-900 text-base"><?= htmlspecialchars($agent['name']) ?></div>
|
|
|
|
<?php $machine_info = trim("$pc_manufacturer $pc_model"); ?>
|
|
<?php if ($machine_info): ?>
|
|
<div class="text-xs font-semibold text-slate-500 mt-1"><i class="fa-solid fa-laptop mr-1 text-slate-400"></i><?= $machine_info ?></div>
|
|
<?php endif; ?>
|
|
|
|
<div class="mt-2.5 flex flex-wrap gap-2">
|
|
<?php if ($asset_id): ?><span class="inline-flex items-center px-2 py-0.5 rounded border border-slate-200 bg-white shadow-sm text-[11px] font-bold text-slate-600"><i class="fa-solid fa-tag mr-1.5 text-blue-500"></i><?= $asset_id ?></span><?php endif; ?>
|
|
<?php if ($location): ?><span class="inline-flex items-center px-2 py-0.5 rounded border border-slate-200 bg-white shadow-sm text-[11px] font-bold text-slate-600"><i class="fa-solid fa-location-dot mr-1.5 text-emerald-500"></i><?= $location ?></span><?php endif; ?>
|
|
</div>
|
|
</td>
|
|
<td><code class="text-red-600 bg-red-50 border border-red-100 px-2.5 py-1 rounded-md text-sm shadow-inner"><?= htmlspecialchars($agent['ip'] ?? 'N/A') ?></code></td>
|
|
<td class="font-bold text-slate-700"><?= htmlspecialchars(format_os_name($agent)) ?></td>
|
|
<td>
|
|
<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold shadow-sm <?= $status_bg ?>">
|
|
<i class="<?= $status_icon ?> mr-1.5"></i> <?= ucfirst($status) ?>
|
|
</span>
|
|
</td>
|
|
<td class="text-center text-nowrap">
|
|
<?php
|
|
if ($tv_id || $ad_id) {
|
|
if ($tv_id) echo "<span class='inline-flex justify-center items-center w-10 h-10 rounded-xl bg-blue-50 text-blue-600 border border-blue-100 cursor-help hover:bg-blue-600 hover:text-white transition-all shadow-sm' data-tooltip='TeamViewer: {$tv_id}'><i class='fa-solid fa-people-arrows text-lg'></i></span>";
|
|
if ($ad_id) echo "<span class='inline-flex justify-center items-center w-10 h-10 rounded-xl bg-red-50 text-red-600 border border-red-100 cursor-help hover:bg-red-600 hover:text-white transition-all shadow-sm ml-2' data-tooltip='AnyDesk: {$ad_id}'><i class='fa-solid fa-desktop text-lg'></i></span>";
|
|
} else {
|
|
echo "<span class='text-slate-300 text-sm font-bold'>-</span>";
|
|
}
|
|
?>
|
|
</td>
|
|
<td class="text-right">
|
|
<button class="inline-flex items-center px-4 py-2 text-sm font-bold rounded-xl text-blue-700 bg-blue-50 border border-blue-200 hover:bg-blue-600 hover:text-white hover:border-blue-600 transition-all shadow-sm transform hover:-translate-y-0.5" onclick="showDetailedModal('<?= $agent['id'] ?>')">
|
|
<i class="fa-solid fa-microchip mr-2"></i> Specs
|
|
</button>
|
|
</td>
|
|
</tr>
|
|
<?php endforeach; ?>
|
|
</tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-asset-filter" class="tab-pane hidden">
|
|
<div id="filterNotScannedMsg" class="bg-blue-50 border border-blue-200 text-blue-700 p-5 rounded-2xl flex items-center mb-0 font-bold shadow-sm">
|
|
<i class="fa-solid fa-info-circle text-2xl mr-4 text-blue-500"></i> Please run "Global Deep Scan" above to fetch RAM data.
|
|
</div>
|
|
<div id="filterContentArea" style="display:none;">
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-5 p-6 bg-slate-50 border border-slate-200 rounded-2xl mb-8 shadow-inner">
|
|
<div>
|
|
<label class="block text-sm font-black text-slate-700 mb-2 uppercase tracking-wide">OS Platform</label>
|
|
<select id="filterOS" class="block w-full bg-white border border-slate-300 text-slate-700 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 p-3 outline-none font-sans appearance-none shadow-sm font-medium" style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%23475569\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'m2 5 6 6 6-6\'/%3e%3c/svg%3e'); background-repeat: no-repeat; background-position: right 1rem center; background-size: 16px 12px;">
|
|
<option value="all">All OS</option>
|
|
<?php foreach ($unique_os_list as $os_opt): ?>
|
|
<option value="<?= htmlspecialchars($os_opt) ?>"><?= htmlspecialchars($os_opt) ?></option>
|
|
<?php endforeach; ?>
|
|
</select>
|
|
</div>
|
|
<div>
|
|
<label class="block text-sm font-black text-slate-700 mb-2 uppercase tracking-wide">RAM Capacity</label>
|
|
<select id="filterRAM" class="block w-full bg-white border border-slate-300 text-slate-700 rounded-xl focus:ring-2 focus:ring-blue-500/20 focus:border-blue-500 p-3 outline-none font-sans appearance-none shadow-sm font-medium" style="background-image: url('data:image/svg+xml,%3csvg xmlns=\'http://www.w3.org/2000/svg\' viewBox=\'0 0 16 16\'%3e%3cpath fill=\'none\' stroke=\'%23475569\' stroke-linecap=\'round\' stroke-linejoin=\'round\' stroke-width=\'2\' d=\'m2 5 6 6 6-6\'/%3e%3c/svg%3e'); background-repeat: no-repeat; background-position: right 1rem center; background-size: 16px 12px;">
|
|
<option value="all">All RAM Sizes</option>
|
|
<option value="<4">Less than 4 GB</option>
|
|
<option value="4-8">4 GB - 8 GB</option>
|
|
<option value="8-16">8 GB - 16 GB</option>
|
|
<option value=">16">More than 16 GB</option>
|
|
</select>
|
|
</div>
|
|
<div class="flex items-end">
|
|
<button class="w-full inline-flex items-center justify-center px-4 py-3 text-sm font-bold rounded-xl text-white bg-slate-800 hover:bg-slate-900 transition-all shadow-md transform hover:-translate-y-0.5" onclick="applyAssetFilter()">
|
|
<i class="fa-solid fa-filter mr-2"></i> Apply Filter
|
|
</button>
|
|
</div>
|
|
</div>
|
|
<div class="overflow-x-auto pb-4">
|
|
<table id="assetFilterTable" class="w-full text-left">
|
|
<thead>
|
|
<tr>
|
|
<th>Agent Name</th>
|
|
<th>IP Address</th>
|
|
<th>OS Version</th>
|
|
<th>Total RAM</th>
|
|
<th class="text-center">Action</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-slate-700 font-medium"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-global-software" class="tab-pane hidden">
|
|
<div id="softwareNotScannedMsg" class="bg-blue-50 border border-blue-200 text-blue-700 p-5 rounded-2xl flex items-center mb-0 font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4 text-blue-500"></i> Please run "Global Deep Scan" above.</div>
|
|
<div id="softwareTableContainer" style="display:none;" class="overflow-x-auto pb-4">
|
|
<table id="globalSoftwareTable" class="w-full text-left">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-left">Software Name</th>
|
|
<th>Version</th>
|
|
<th>Vendor</th>
|
|
<th>Installed On (Agent)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-slate-700 font-medium"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-global-updates" class="tab-pane hidden">
|
|
<div id="updatesNotScannedMsg" class="bg-blue-50 border border-blue-200 text-blue-700 p-5 rounded-2xl flex items-center mb-0 font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4 text-blue-500"></i> Please run "Global Deep Scan" above.</div>
|
|
<div id="updatesTableContainer" style="display:none;" class="overflow-x-auto pb-4">
|
|
<table id="globalUpdateTable" class="w-full text-left">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-left w-1/4">Hotfix ID</th>
|
|
<th>Description / Subject</th>
|
|
<th>Installed On (Agent)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-slate-700 font-medium"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
<div id="tab-global-process" class="tab-pane hidden">
|
|
<div class="max-w-3xl mx-auto mb-8">
|
|
<div class="relative flex items-center w-full h-14 rounded-2xl focus-within:ring-4 focus-within:ring-blue-500/20 bg-white overflow-hidden border border-slate-300 shadow-sm transition-all">
|
|
<div class="grid place-items-center h-full w-14 text-slate-400 text-lg"><i class="fa-solid fa-magnifying-glass"></i></div>
|
|
<input class="peer h-full w-full outline-none text-base text-slate-700 font-bold pr-2 bg-transparent" type="text" id="inputProcessSearch" placeholder="Enter process name (e.g. chrome, powershell)..." />
|
|
<button class="h-full px-8 bg-slate-800 hover:bg-slate-900 text-white font-bold text-sm transition-colors" type="button" id="btnSearchProcess" onclick="searchGlobalProcess()">Search</button>
|
|
</div>
|
|
</div>
|
|
<div id="processProgressArea" class="max-w-3xl mx-auto mb-8 hidden">
|
|
<div class="flex justify-between mb-2"><small class="font-bold text-slate-600 uppercase tracking-wide" id="processStatusText">Checking Agent...</small><small class="font-black text-blue-600" id="processPercentage">0%</small></div>
|
|
<div class="w-full bg-slate-200 rounded-full h-3 shadow-inner">
|
|
<div id="processProgressBar" class="bg-blue-600 h-3 rounded-full transition-all duration-300" style="width: 0%"></div>
|
|
</div>
|
|
</div>
|
|
<div id="processTableContainer" style="display:none;" class="overflow-x-auto pb-4">
|
|
<table id="globalProcessTable" class="w-full text-left">
|
|
<thead>
|
|
<tr>
|
|
<th class="text-left">Process Name</th>
|
|
<th>PID</th>
|
|
<th>State</th>
|
|
<th>Command / Path</th>
|
|
<th>Running On (Agent)</th>
|
|
</tr>
|
|
</thead>
|
|
<tbody class="text-slate-700 font-medium"></tbody>
|
|
</table>
|
|
</div>
|
|
</div>
|
|
|
|
</div>
|
|
</div>
|
|
<?php endif; ?>
|
|
</div>
|
|
|
|
<div id="agentModal" class="fixed inset-0 z-[1050] hidden bg-slate-900/70 backdrop-blur-md overflow-y-auto flex items-start justify-center pt-8 pb-10 px-4 transition-opacity">
|
|
<div class="bg-white rounded-3xl shadow-2xl w-full max-w-6xl flex flex-col max-h-[92vh] border border-slate-200 overflow-hidden">
|
|
|
|
<div class="flex items-center justify-between p-6 border-b border-slate-200 bg-gradient-to-r from-slate-50 to-white">
|
|
<h5 class="text-2xl font-black text-slate-800 m-0 flex items-center" id="modalTitle"></h5>
|
|
<button type="button" class="text-slate-400 bg-white border border-slate-200 hover:bg-red-50 hover:text-red-600 hover:border-red-200 rounded-xl text-sm w-10 h-10 flex justify-center items-center transition-all shadow-sm" onclick="closeModal()">
|
|
<i class="fa-solid fa-xmark text-xl"></i>
|
|
</button>
|
|
</div>
|
|
|
|
<div class="flex-1 overflow-y-auto p-0 bg-slate-50/50">
|
|
<div class="flex border-b border-slate-200 overflow-x-auto hide-scrollbar bg-white px-6 pt-4 gap-2">
|
|
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-primary border-b-[3px] border-primary focus:outline-none whitespace-nowrap transition-colors rounded-t-lg bg-blue-50/50" data-target="#general">
|
|
<i class="fa-solid fa-circle-info mr-2"></i> General
|
|
</button>
|
|
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#hardware">
|
|
<i class="fa-solid fa-server mr-2"></i> Hardware & Disk
|
|
</button>
|
|
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#network">
|
|
<i class="fa-solid fa-network-wired mr-2"></i> Network
|
|
</button>
|
|
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#software">
|
|
<i class="fa-solid fa-box-open mr-2"></i> Software
|
|
</button>
|
|
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#hotfixes">
|
|
<i class="fa-brands fa-windows mr-2"></i> Windows Update
|
|
</button>
|
|
<button class="modal-tab-btn px-5 py-3 text-sm font-bold text-slate-500 border-b-[3px] border-transparent hover:text-slate-800 hover:bg-slate-50 focus:outline-none whitespace-nowrap transition-colors rounded-t-lg" data-target="#remote-desktop">
|
|
<i class="fa-solid fa-headset mr-2"></i> Remote
|
|
</button>
|
|
</div>
|
|
|
|
<div class="p-6 md:p-8">
|
|
<div id="general" class="modal-tab-pane block">
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6" id="content-general"></div>
|
|
</div>
|
|
<div id="hardware" class="modal-tab-pane hidden">
|
|
<div id="content-hardware" class="text-center py-10"></div>
|
|
</div>
|
|
<div id="network" class="modal-tab-pane hidden">
|
|
<div id="content-network" class="text-center py-10"></div>
|
|
</div>
|
|
<div id="software" class="modal-tab-pane hidden">
|
|
<div id="content-software" class="text-center py-10"></div>
|
|
</div>
|
|
<div id="hotfixes" class="modal-tab-pane hidden">
|
|
<div id="content-hotfixes" class="text-center py-10"></div>
|
|
</div>
|
|
<div id="remote-desktop" class="modal-tab-pane hidden">
|
|
<div id="content-remote-desktop" class="text-center py-10"></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="flex items-center justify-end p-5 border-t border-slate-200 bg-white">
|
|
<button type="button" class="px-8 py-3 text-sm font-bold rounded-xl text-slate-700 bg-white border border-slate-300 hover:bg-slate-100 transition-all shadow-sm" onclick="closeModal()">Close Window</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const agentsData = <?= json_encode($agents) ?>;
|
|
const allMetadata = <?= json_encode($all_metadata) ?>;
|
|
|
|
document.querySelectorAll('.tab-btn').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
document.querySelectorAll('.tab-btn').forEach(btn => {
|
|
btn.classList.remove('text-primary', 'border-primary', 'bg-white', 'shadow-sm');
|
|
btn.classList.add('text-slate-500', 'border-transparent');
|
|
});
|
|
button.classList.remove('text-slate-500', 'border-transparent');
|
|
button.classList.add('text-primary', 'border-primary', 'bg-white', 'shadow-sm');
|
|
|
|
document.querySelectorAll('.tab-pane').forEach(pane => pane.classList.add('hidden'));
|
|
document.querySelector(button.dataset.target).classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
document.querySelectorAll('.modal-tab-btn').forEach(button => {
|
|
button.addEventListener('click', () => {
|
|
document.querySelectorAll('.modal-tab-btn').forEach(btn => {
|
|
btn.classList.remove('text-primary', 'border-primary', 'bg-blue-50/50');
|
|
btn.classList.add('text-slate-500', 'border-transparent');
|
|
});
|
|
button.classList.remove('text-slate-500', 'border-transparent');
|
|
button.classList.add('text-primary', 'border-primary', 'bg-blue-50/50');
|
|
|
|
document.querySelectorAll('.modal-tab-pane').forEach(pane => pane.classList.add('hidden'));
|
|
document.querySelector(button.dataset.target).classList.remove('hidden');
|
|
});
|
|
});
|
|
|
|
function closeModal() {
|
|
document.getElementById('agentModal').classList.add('hidden');
|
|
document.body.style.overflow = 'auto';
|
|
}
|
|
|
|
const corporateColors = [
|
|
'#1e3a8a', '#059669', '#d97706', '#dc2626',
|
|
'#475569', '#0284c7', '#7c3aed', '#ea580c'
|
|
];
|
|
|
|
$(document).ready(function() {
|
|
$('#mainAgentTable').DataTable({
|
|
pageLength: 10,
|
|
order: [
|
|
[1, 'asc']
|
|
],
|
|
language: {
|
|
search: "_INPUT_",
|
|
searchPlaceholder: "Search endpoints..."
|
|
}
|
|
});
|
|
|
|
renderOSChart();
|
|
renderHWChart();
|
|
|
|
document.getElementById("inputProcessSearch").addEventListener("keypress", function(e) {
|
|
if (e.key === "Enter") {
|
|
e.preventDefault();
|
|
searchGlobalProcess();
|
|
}
|
|
});
|
|
});
|
|
|
|
function exportToExcel() {
|
|
let wb = XLSX.utils.book_new();
|
|
let wsData = [];
|
|
|
|
wsData.push([
|
|
"Agent ID", "Agent Name", "Manufacturer", "PC Model", "Asset ID", "Location",
|
|
"IP Address", "OS Name", "OS Version", "Architecture",
|
|
"Status", "TeamViewer ID", "AnyDesk Address",
|
|
"Disk C: Total (GB)", "Disk C: Free (GB)", "Total RAM",
|
|
"Date Added", "Last Keep Alive"
|
|
]);
|
|
|
|
agentsData.forEach(agent => {
|
|
let aid = agent.id;
|
|
let meta = allMetadata[aid] || {};
|
|
|
|
let assetId = agent.labels?.asset_id || meta.asset_id || '-';
|
|
let location = agent.labels?.location || meta.location || '-';
|
|
let pcManufacturer = meta.manufacturer && meta.manufacturer !== 'Unknown' ? meta.manufacturer : '-';
|
|
let pcModel = meta.model && meta.model !== 'Unknown' ? meta.model : '-';
|
|
|
|
let tv = meta.tv && meta.tv !== 'None' ? meta.tv : '-';
|
|
let ad = meta.ad && meta.ad !== 'None' ? meta.ad : '-';
|
|
let diskTotal = meta.disk_c_total ? meta.disk_c_total : 'N/A';
|
|
let diskFree = meta.disk_c_free ? meta.disk_c_free : 'N/A';
|
|
|
|
let osName = getFormattedOSName(agent);
|
|
let osVersion = agent.os?.version || '-';
|
|
let arch = agent.os?.arch || '-';
|
|
|
|
let dateAdd = agent.dateAdd ? new Date(agent.dateAdd).toLocaleDateString() : '-';
|
|
let lastAlive = agent.lastKeepAlive ? new Date(agent.lastKeepAlive).toLocaleString() : '-';
|
|
|
|
let scannedData = globalAgentHardwareList.find(a => a.id === aid);
|
|
let ram = scannedData ? scannedData.ramStr : 'Not Scanned';
|
|
|
|
wsData.push([
|
|
aid, agent.name, pcManufacturer, pcModel, assetId, location,
|
|
agent.ip || '-', osName, osVersion, arch,
|
|
agent.status || '-', tv, ad,
|
|
diskTotal, diskFree, ram,
|
|
dateAdd, lastAlive
|
|
]);
|
|
});
|
|
|
|
let ws = XLSX.utils.aoa_to_sheet(wsData);
|
|
XLSX.utils.book_append_sheet(wb, ws, "Inventory");
|
|
|
|
let d = new Date();
|
|
let dateStr = d.getFullYear() + "-" + String(d.getMonth() + 1).padStart(2, '0') + "-" + String(d.getDate()).padStart(2, '0');
|
|
XLSX.writeFile(wb, "Endpoint_Inventory_" + dateStr + ".xlsx");
|
|
}
|
|
|
|
function getFormattedOSName(agent) {
|
|
if (agent.id === '000') return 'Wazuh Server';
|
|
let osName = agent.os?.name || agent.os?.platform || 'Unknown';
|
|
let nameLower = osName.toLowerCase();
|
|
|
|
if (nameLower.includes('windows 11')) return 'Windows 11';
|
|
if (nameLower.includes('windows 10')) return 'Windows 10';
|
|
if (nameLower.includes('windows 7')) return 'Windows 7';
|
|
if (nameLower.includes('windows 8')) return 'Windows 8';
|
|
if (nameLower.includes('windows server 2022')) return 'Windows Server 2022';
|
|
if (nameLower.includes('windows server 2019')) return 'Windows Server 2019';
|
|
if (nameLower.includes('windows server 2016')) return 'Windows Server 2016';
|
|
if (nameLower.includes('windows server 2012')) return 'Windows Server 2012';
|
|
|
|
if (nameLower.includes('ubuntu')) {
|
|
let m = osName.match(/Ubuntu (\d+\.\d+)/i);
|
|
return m ? `Ubuntu ${m[1]}` : 'Ubuntu';
|
|
}
|
|
if (nameLower.includes('centos')) {
|
|
let m = osName.match(/CentOS.*?(\d+)/i);
|
|
return m ? `CentOS ${m[1]}` : 'CentOS';
|
|
}
|
|
if (nameLower.includes('mac') || nameLower.includes('darwin')) {
|
|
return agent.os?.version ? `macOS ${agent.os.version}` : 'macOS';
|
|
}
|
|
return osName;
|
|
}
|
|
|
|
function renderOSChart() {
|
|
let osCounts = {};
|
|
agentsData.forEach(a => {
|
|
let osName = getFormattedOSName(a);
|
|
osCounts[osName] = (osCounts[osName] || 0) + 1;
|
|
});
|
|
|
|
if (Object.keys(osCounts).length > 0) {
|
|
new Chart(document.getElementById('osChart').getContext('2d'), {
|
|
type: 'doughnut',
|
|
data: {
|
|
labels: Object.keys(osCounts),
|
|
datasets: [{
|
|
data: Object.values(osCounts),
|
|
backgroundColor: corporateColors,
|
|
borderWidth: 3,
|
|
borderColor: '#ffffff'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
font: {
|
|
family: 'Sarabun',
|
|
size: 13,
|
|
weight: 'bold'
|
|
},
|
|
boxWidth: 12,
|
|
color: '#334155'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
function renderHWChart() {
|
|
let mfgData = {};
|
|
let allModels = new Set();
|
|
|
|
agentsData.forEach(a => {
|
|
if (a.id === '000') return;
|
|
let meta = allMetadata[a.id] || {};
|
|
let mfg = meta.manufacturer && meta.manufacturer !== 'Unknown' ? meta.manufacturer : 'Unknown';
|
|
let model = meta.model && meta.model !== 'Unknown' ? meta.model : 'Unknown';
|
|
|
|
if (mfg.toLowerCase().includes('hewlett-packard') || mfg.toLowerCase() === 'hp') mfg = 'HP';
|
|
else if (mfg.toLowerCase().includes('lenovo')) mfg = 'Lenovo';
|
|
else if (mfg.toLowerCase().includes('dell')) mfg = 'Dell';
|
|
else if (mfg.toLowerCase().includes('vmware')) mfg = 'VMware';
|
|
else if (mfg.toLowerCase().includes('microsoft')) mfg = 'Microsoft';
|
|
|
|
if (!mfgData[mfg]) mfgData[mfg] = {};
|
|
mfgData[mfg][model] = (mfgData[mfg][model] || 0) + 1;
|
|
allModels.add(model);
|
|
});
|
|
|
|
let labels = Object.keys(mfgData);
|
|
if (labels.length === 0) return;
|
|
|
|
let datasets = [];
|
|
let colorIdx = 0;
|
|
|
|
allModels.forEach(model => {
|
|
let data = [];
|
|
let hasData = false;
|
|
labels.forEach(mfg => {
|
|
let count = mfgData[mfg][model] || 0;
|
|
data.push(count);
|
|
if (count > 0) hasData = true;
|
|
});
|
|
|
|
if (hasData) {
|
|
datasets.push({
|
|
label: model,
|
|
data: data,
|
|
backgroundColor: corporateColors[colorIdx % corporateColors.length],
|
|
borderRadius: 4
|
|
});
|
|
colorIdx++;
|
|
}
|
|
});
|
|
|
|
new Chart(document.getElementById('hwChart').getContext('2d'), {
|
|
type: 'bar',
|
|
data: {
|
|
labels: labels,
|
|
datasets: datasets
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
scales: {
|
|
x: {
|
|
stacked: true,
|
|
grid: {
|
|
display: false
|
|
},
|
|
ticks: {
|
|
font: {
|
|
family: 'Sarabun',
|
|
weight: 'bold'
|
|
},
|
|
color: '#475569'
|
|
}
|
|
},
|
|
y: {
|
|
stacked: true,
|
|
beginAtZero: true,
|
|
border: {
|
|
display: false
|
|
},
|
|
grid: {
|
|
color: '#f1f5f9'
|
|
},
|
|
ticks: {
|
|
stepSize: 1,
|
|
font: {
|
|
family: 'Sarabun',
|
|
weight: 'bold'
|
|
},
|
|
color: '#475569'
|
|
}
|
|
}
|
|
},
|
|
plugins: {
|
|
legend: {
|
|
display: false
|
|
},
|
|
tooltip: {
|
|
titleFont: {
|
|
family: 'Sarabun',
|
|
size: 14,
|
|
weight: 'bold'
|
|
},
|
|
bodyFont: {
|
|
family: 'Sarabun',
|
|
size: 13
|
|
},
|
|
callbacks: {
|
|
title: function(context) {
|
|
return 'Brand: ' + context[0].label;
|
|
},
|
|
label: function(context) {
|
|
return ' ' + context.dataset.label + ': ' + context.parsed.y;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
let globalAgentHardwareList = [];
|
|
let globalSoftwareList = [];
|
|
let globalUpdateList = [];
|
|
let globalRamCounts = {
|
|
"< 4GB": 0,
|
|
"4 - 8GB": 0,
|
|
"8 - 16GB": 0,
|
|
"> 16GB": 0
|
|
};
|
|
|
|
async function startGlobalScan() {
|
|
const scanTargets = agentsData.filter(a => a.status === 'active' || a.status === 'disconnected');
|
|
if (scanTargets.length === 0) return alert("No active agents found to scan.");
|
|
|
|
let btn = document.getElementById('btnScanGlobal');
|
|
btn.disabled = true;
|
|
btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin mr-2"></i> Scanning...`;
|
|
document.getElementById('scanProgressArea').classList.remove('hidden');
|
|
|
|
let completed = 0;
|
|
globalAgentHardwareList = [];
|
|
globalSoftwareList = [];
|
|
globalUpdateList = [];
|
|
globalRamCounts = {
|
|
"< 4GB": 0,
|
|
"4 - 8GB": 0,
|
|
"8 - 16GB": 0,
|
|
"> 16GB": 0
|
|
};
|
|
|
|
for (let agent of scanTargets) {
|
|
let ramBytes = 0;
|
|
let ramStr = 'N/A';
|
|
try {
|
|
let res = await fetch(`?action=get_dashboard_data&agent_id=${agent.id}`);
|
|
let data = await res.json();
|
|
if (data.hardware && data.hardware.ram && data.hardware.ram.total) {
|
|
ramBytes = data.hardware.ram.total;
|
|
ramStr = formatKB(ramBytes);
|
|
let gb = ramBytes / 1024 / 1024;
|
|
if (gb < 4.0) globalRamCounts["< 4GB"]++;
|
|
else if (gb <= 8.0) globalRamCounts["4 - 8GB"]++;
|
|
else if (gb <= 16.0) globalRamCounts["8 - 16GB"]++;
|
|
else globalRamCounts["> 16GB"]++;
|
|
}
|
|
if (data.packages) {
|
|
data.packages.forEach(pkg => {
|
|
globalSoftwareList.push([pkg.name || '-', pkg.version || '-', pkg.vendor || '-', `<div class="font-bold text-slate-900">${agent.name}</div><div class="text-xs text-slate-500 font-mono mt-1">${agent.ip || '-'}</div>`]);
|
|
});
|
|
}
|
|
if (data.hotfixes) {
|
|
data.hotfixes.forEach(hf => {
|
|
globalUpdateList.push([`<span class="text-success font-bold"><i class="fa-solid fa-shield-virus mr-2"></i>${hf.hotfix || '-'}</span>`, hf.description || hf.caption || 'System Update', `<div class="font-bold text-slate-900">${agent.name}</div><div class="text-xs text-slate-500 font-mono mt-1">${agent.ip || '-'}</div>`]);
|
|
});
|
|
}
|
|
} catch (err) {}
|
|
|
|
globalAgentHardwareList.push({
|
|
id: agent.id,
|
|
name: agent.name,
|
|
ip: agent.ip || 'N/A',
|
|
osName: getFormattedOSName(agent),
|
|
ramBytes: ramBytes,
|
|
ramStr: ramStr
|
|
});
|
|
|
|
completed++;
|
|
let percent = Math.round((completed / scanTargets.length) * 100);
|
|
document.getElementById('scanStatusText').innerText = `Scanning Agent ${completed} of ${scanTargets.length}...`;
|
|
document.getElementById('scanPercentage').innerText = `${percent}%`;
|
|
document.getElementById('scanProgressBar').style.width = `${percent}%`;
|
|
}
|
|
|
|
btn.innerHTML = `<i class="fa-solid fa-check mr-2"></i> Scan Complete!`;
|
|
btn.classList.replace('from-blue-600', 'from-emerald-500');
|
|
btn.classList.replace('to-indigo-600', 'to-green-600');
|
|
btn.classList.replace('shadow-blue-600/30', 'shadow-emerald-500/30');
|
|
|
|
document.getElementById('ramChartContainer').innerHTML = '<canvas id="ramChart"></canvas>';
|
|
new Chart(document.getElementById('ramChart').getContext('2d'), {
|
|
type: 'pie',
|
|
data: {
|
|
labels: Object.keys(globalRamCounts),
|
|
datasets: [{
|
|
data: Object.values(globalRamCounts),
|
|
backgroundColor: corporateColors,
|
|
borderWidth: 3,
|
|
borderColor: '#ffffff'
|
|
}]
|
|
},
|
|
options: {
|
|
responsive: true,
|
|
maintainAspectRatio: false,
|
|
plugins: {
|
|
legend: {
|
|
position: 'right',
|
|
labels: {
|
|
font: {
|
|
family: 'Sarabun',
|
|
weight: 'bold'
|
|
},
|
|
color: '#334155'
|
|
}
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
document.getElementById('filterNotScannedMsg').style.display = 'none';
|
|
document.getElementById('filterContentArea').style.display = 'block';
|
|
applyAssetFilter();
|
|
|
|
document.getElementById('softwareNotScannedMsg').style.display = 'none';
|
|
document.getElementById('softwareTableContainer').style.display = 'block';
|
|
$('#globalSoftwareTable').DataTable({
|
|
data: globalSoftwareList,
|
|
pageLength: 10,
|
|
deferRender: true,
|
|
order: [
|
|
[0, 'asc']
|
|
],
|
|
language: {
|
|
search: "_INPUT_",
|
|
searchPlaceholder: "Filter software..."
|
|
},
|
|
destroy: true
|
|
});
|
|
|
|
document.getElementById('updatesNotScannedMsg').style.display = 'none';
|
|
document.getElementById('updatesTableContainer').style.display = 'block';
|
|
$('#globalUpdateTable').DataTable({
|
|
data: globalUpdateList,
|
|
pageLength: 10,
|
|
deferRender: true,
|
|
order: [
|
|
[0, 'asc']
|
|
],
|
|
language: {
|
|
search: "_INPUT_",
|
|
searchPlaceholder: "Filter Update (e.g. KB500)..."
|
|
},
|
|
destroy: true
|
|
});
|
|
}
|
|
|
|
function applyAssetFilter() {
|
|
const osFilter = document.getElementById('filterOS').value;
|
|
const ramFilter = document.getElementById('filterRAM').value;
|
|
|
|
let filteredData = globalAgentHardwareList.filter(agent => {
|
|
let osMatch = true;
|
|
if (osFilter !== 'all') {
|
|
osMatch = (agent.osName === osFilter);
|
|
}
|
|
let ramMatch = true;
|
|
if (ramFilter !== 'all') {
|
|
let gb = agent.ramBytes / 1024 / 1024;
|
|
if (ramFilter === '<4') ramMatch = gb > 0 && gb < 4.0;
|
|
else if (ramFilter === '4-8') ramMatch = gb >= 4.0 && gb <= 8.0;
|
|
else if (ramFilter === '8-16') ramMatch = gb > 8.0 && gb <= 16.0;
|
|
else if (ramFilter === '>16') ramMatch = gb > 16.0;
|
|
}
|
|
return osMatch && ramMatch;
|
|
});
|
|
|
|
let tableData = filteredData.map(a => [
|
|
`<div class="font-black text-slate-900">${a.name}</div>`,
|
|
`<code class="text-danger bg-red-50 border border-red-100 px-2.5 py-1 rounded-md text-sm shadow-inner">${a.ip}</code>`,
|
|
`<span class="font-bold text-slate-700">${a.osName}</span>`,
|
|
`<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold bg-green-100 text-success border border-green-200"><i class="fa-solid fa-memory mr-1.5"></i> ${a.ramStr}</span>`,
|
|
`<button class="inline-flex items-center px-4 py-2 text-sm font-bold rounded-xl text-primary bg-blue-50 border border-blue-200 hover:bg-blue-600 hover:text-white transition-all shadow-sm transform hover:-translate-y-0.5" onclick="showDetailedModal('${a.id}')"><i class="fa-solid fa-microchip mr-2"></i> Specs</button>`
|
|
]);
|
|
|
|
if ($.fn.DataTable.isDataTable('#assetFilterTable')) {
|
|
$('#assetFilterTable').DataTable().destroy();
|
|
}
|
|
$('#assetFilterTable').DataTable({
|
|
data: tableData,
|
|
pageLength: 10,
|
|
deferRender: true,
|
|
language: {
|
|
search: "_INPUT_",
|
|
searchPlaceholder: "Search filtered results..."
|
|
},
|
|
columnDefs: [{
|
|
targets: 4,
|
|
className: "text-center",
|
|
orderable: false
|
|
}]
|
|
});
|
|
}
|
|
|
|
let globalProcessList = [];
|
|
async function searchGlobalProcess() {
|
|
const query = document.getElementById('inputProcessSearch').value.trim().toLowerCase();
|
|
if (!query) return alert("Please enter a process name.");
|
|
const scanTargets = agentsData.filter(a => a.status === 'active');
|
|
if (scanTargets.length === 0) return alert("No active agents.");
|
|
|
|
let btn = document.getElementById('btnSearchProcess');
|
|
btn.disabled = true;
|
|
btn.innerHTML = `<i class="fa-solid fa-spinner fa-spin mr-2"></i> Checking...`;
|
|
|
|
document.getElementById('processProgressArea').classList.remove('hidden');
|
|
document.getElementById('processTableContainer').style.display = 'none';
|
|
globalProcessList = [];
|
|
let completed = 0;
|
|
|
|
for (let agent of scanTargets) {
|
|
try {
|
|
let res = await fetch(`?action=get_processes&agent_id=${agent.id}`);
|
|
let data = await res.json();
|
|
if (data.processes) {
|
|
data.processes.forEach(proc => {
|
|
let procName = (proc.name || '').toLowerCase();
|
|
let cmdLine = (proc.cmd || proc.cmdline || '').toLowerCase();
|
|
if (procName.includes(query) || cmdLine.includes(query)) {
|
|
let stateClass = proc.state === 'running' || proc.state === 'R' ? 'bg-green-100 text-success border border-green-200' : (proc.state === 'sleeping' || proc.state === 'S' ? 'bg-slate-100 text-slate-600 border border-slate-200' : 'bg-amber-100 text-warning border border-amber-200');
|
|
globalProcessList.push([
|
|
`<span class="font-bold text-slate-900">${proc.name || '-'}</span>`,
|
|
`<code class="text-slate-600 bg-slate-50 border border-slate-200 px-2 py-0.5 rounded shadow-inner">${proc.pid || '-'}</code>`,
|
|
`<span class="inline-flex items-center px-2.5 py-0.5 rounded text-xs font-bold ${stateClass}">${proc.state || 'Unknown'}</span>`,
|
|
`<div class="text-xs text-slate-500 break-all max-w-md bg-slate-50 p-2 rounded border border-slate-100">${proc.cmd || proc.cmdline || '-'}</div>`,
|
|
`<div class="font-black text-slate-900">${agent.name}</div><div class="text-xs text-slate-500 font-mono mt-1">${agent.ip || '-'}</div>`
|
|
]);
|
|
}
|
|
});
|
|
}
|
|
} catch (err) {}
|
|
completed++;
|
|
let percent = Math.round((completed / scanTargets.length) * 100);
|
|
document.getElementById('processStatusText').innerText = `Checking Agent ${completed} of ${scanTargets.length}...`;
|
|
document.getElementById('processPercentage').innerText = `${percent}%`;
|
|
document.getElementById('processProgressBar').style.width = `${percent}%`;
|
|
}
|
|
|
|
btn.disabled = false;
|
|
btn.innerHTML = `Search`;
|
|
document.getElementById('processProgressArea').classList.add('hidden');
|
|
document.getElementById('processTableContainer').style.display = 'block';
|
|
|
|
if ($.fn.DataTable.isDataTable('#globalProcessTable')) {
|
|
$('#globalProcessTable').DataTable().destroy();
|
|
}
|
|
$('#globalProcessTable').DataTable({
|
|
data: globalProcessList,
|
|
pageLength: 10,
|
|
deferRender: true,
|
|
order: [
|
|
[4, 'asc'],
|
|
[0, 'asc']
|
|
],
|
|
language: {
|
|
search: "_INPUT_",
|
|
searchPlaceholder: "Filter within results..."
|
|
}
|
|
});
|
|
}
|
|
|
|
function formatKB(kb) {
|
|
if (!kb || isNaN(kb)) return 'N/A';
|
|
if (kb >= 1048576) return (kb / 1024 / 1024).toFixed(2) + ' GB';
|
|
return (kb / 1024).toFixed(2) + ' MB';
|
|
}
|
|
|
|
function showDetailedModal(agentId) {
|
|
const agent = agentsData.find(a => a.id === agentId);
|
|
if (!agent) return;
|
|
|
|
let meta = allMetadata[agentId] || {};
|
|
let assetId = agent.labels?.asset_id || meta.asset_id || 'ไม่ระบุ';
|
|
let locationStr = agent.labels?.location || meta.location || 'ไม่ระบุ';
|
|
let pcModel = meta.model && meta.model !== 'Unknown' ? meta.model : 'ไม่ระบุ';
|
|
let pcManufacturer = meta.manufacturer && meta.manufacturer !== 'Unknown' ? meta.manufacturer : 'ไม่ระบุ';
|
|
|
|
document.getElementById('modalTitle').innerHTML = `<i class="fa-solid fa-terminal mr-3 text-primary"></i> ${agent.name} <span class="bg-slate-100 text-slate-600 border border-slate-300 ml-3 px-3 py-1 rounded-lg text-sm font-bold shadow-inner">ID: ${agent.id}</span>`;
|
|
|
|
// Reset Internal Tabs to General
|
|
document.querySelectorAll('.modal-tab-btn').forEach(btn => {
|
|
btn.classList.remove('text-primary', 'border-primary', 'bg-blue-50/50');
|
|
btn.classList.add('text-slate-500', 'border-transparent');
|
|
});
|
|
document.querySelector('.modal-tab-btn[data-target="#general"]').classList.remove('text-slate-500', 'border-transparent');
|
|
document.querySelector('.modal-tab-btn[data-target="#general"]').classList.add('text-primary', 'border-primary', 'bg-blue-50/50');
|
|
document.querySelectorAll('.modal-tab-pane').forEach(pane => pane.classList.add('hidden'));
|
|
document.getElementById('general').classList.remove('hidden');
|
|
|
|
document.getElementById('content-general').innerHTML = `
|
|
<div class="col-span-1 md:col-span-2">
|
|
<div class="bg-white border border-slate-200 border-l-4 border-l-warning rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
|
|
<h6 class="text-warning font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-building mr-2"></i> Asset Information</h6>
|
|
<div class="grid grid-cols-2 md:grid-cols-4 gap-6">
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Manufacturer</p><p class="text-slate-900 font-black text-lg m-0">${pcManufacturer}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Model</p><p class="text-slate-900 font-black text-lg m-0">${pcModel}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Asset ID</p><p class="text-primary font-black text-lg m-0">${assetId}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Location</p><p class="text-success font-black text-lg m-0">${locationStr}</p></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-span-1">
|
|
<div class="bg-white border border-slate-200 border-l-4 border-l-primary rounded-xl p-6 shadow-sm h-full hover:shadow-md transition-all">
|
|
<h6 class="text-primary font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-compact-disc mr-2"></i> OS Info</h6>
|
|
<div class="grid grid-cols-2 gap-y-5 gap-x-4">
|
|
<div class="col-span-2"><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Name</p><p class="text-slate-900 font-black text-lg m-0">${getFormattedOSName(agent)}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Arch</p><p class="text-slate-900 font-bold m-0">${agent.os?.arch || '-'}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Version</p><p class="text-slate-900 font-bold m-0">${agent.os?.version || '-'}</p></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-span-1">
|
|
<div class="bg-white border border-slate-200 border-l-4 border-l-success rounded-xl p-6 shadow-sm h-full hover:shadow-md transition-all">
|
|
<h6 class="text-success font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-heart-pulse mr-2"></i> Agent Status</h6>
|
|
<div class="grid grid-cols-2 gap-y-5 gap-x-4">
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">IP Address</p><p class="text-slate-900 font-bold m-0"><code class="bg-red-50 text-danger border border-red-100 px-2 py-1 rounded shadow-inner">${agent.ip || '-'}</code></p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Version</p><p class="text-slate-900 font-bold m-0">${agent.version || '-'}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Added</p><p class="text-slate-900 font-bold m-0">${agent.dateAdd ? new Date(agent.dateAdd).toLocaleDateString() : '-'}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Last Alive</p><p class="text-slate-900 font-bold m-0">${agent.lastKeepAlive ? new Date(agent.lastKeepAlive).toLocaleString() : '-'}</p></div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
|
|
const loader = `<div class="flex flex-col items-center justify-center py-16"><i class="fa-solid fa-circle-notch fa-spin text-5xl text-primary mb-5 drop-shadow-md"></i><p class="text-slate-500 font-bold text-lg tracking-wide uppercase">Fetching Asset Data...</p></div>`;
|
|
document.getElementById('content-hardware').innerHTML = document.getElementById('content-network').innerHTML = document.getElementById('content-software').innerHTML = document.getElementById('content-hotfixes').innerHTML = document.getElementById('content-remote-desktop').innerHTML = loader;
|
|
|
|
document.getElementById('agentModal').classList.remove('hidden');
|
|
document.body.style.overflow = 'hidden';
|
|
|
|
fetch(`?action=get_specs&agent_id=${agentId}`).then(res => res.json()).then(data => {
|
|
if (data.error) throw new Error(data.error);
|
|
|
|
// --- 1. Hardware & Disk Tab ---
|
|
let hwHtml = `<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">`;
|
|
if (data.hardware) {
|
|
const hw = data.hardware;
|
|
const ramUsage = hw.ram?.usage || 0;
|
|
const ramColor = ramUsage > 85 ? 'text-danger' : (ramUsage > 60 ? 'text-warning' : 'text-success');
|
|
const ramBgColor = ramUsage > 85 ? 'bg-danger' : (ramUsage > 60 ? 'bg-warning' : 'bg-success');
|
|
|
|
hwHtml += `
|
|
<div class="col-span-1 lg:col-span-2">
|
|
<div class="bg-white border border-slate-200 border-l-4 border-l-primary rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
|
|
<h6 class="text-primary font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-microchip mr-2"></i> Processor & Motherboard</h6>
|
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-6">
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Processor</p><p class="text-slate-900 font-bold m-0 text-lg">${hw.cpu?.name || 'Unknown'}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Cores / Speed</p><p class="text-slate-900 font-bold m-0 text-lg">${hw.cpu?.cores || '-'} Cores / ${hw.cpu?.mhz ? hw.cpu.mhz+' MHz' : '-'}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Board Serial</p><p class="text-slate-900 font-bold m-0"><code class="bg-slate-100 text-slate-700 px-3 py-1 rounded-lg border border-slate-200 shadow-inner">${hw.board_serial || '-'}</code></p></div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
<div class="col-span-1 lg:col-span-2">
|
|
<div class="bg-white border border-slate-200 border-l-4 border-l-success rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
|
|
<h6 class="text-success font-black text-base mb-2 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-memory mr-2"></i> Memory (RAM)</h6>
|
|
<div class="flex justify-between items-end mb-2 mt-5">
|
|
<span class="text-xs text-slate-500 uppercase tracking-wider font-bold">Usage</span>
|
|
<span class="text-2xl font-black ${ramColor}">${ramUsage}%</span>
|
|
</div>
|
|
<div class="w-full bg-slate-100 rounded-full h-3 mb-6 shadow-inner overflow-hidden border border-slate-200">
|
|
<div class="${ramBgColor} h-3 rounded-full transition-all duration-1000" style="width: ${ramUsage}%"></div>
|
|
</div>
|
|
<div class="grid grid-cols-2 gap-6 border-t border-slate-100 pt-5">
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Total</p><p class="text-slate-900 font-black m-0 text-xl">${formatKB(hw.ram?.total)}</p></div>
|
|
<div><p class="text-xs text-slate-500 uppercase tracking-wider mb-1 font-bold">Free</p><p class="text-slate-900 font-black m-0 text-xl">${formatKB(hw.ram?.free)}</p></div>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
hwHtml += `
|
|
<div class="col-span-1 lg:col-span-2">
|
|
<div class="bg-white border border-slate-200 border-l-4 border-l-info rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
|
|
<h6 class="text-info font-black text-base mb-5 pb-3 border-b border-slate-100 uppercase tracking-wide"><i class="fa-solid fa-hard-drive mr-2"></i> Storage Drives (Drive C Only)</h6>`;
|
|
|
|
let diskData = data.disk_info;
|
|
if (diskData && 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 && Array.isArray(diskData) && diskData.length > 0) {
|
|
hwHtml += `<div class="grid grid-cols-1 md:grid-cols-2 gap-5">`;
|
|
diskData.forEach(disk => {
|
|
let driveLetter = disk.DriveLetter || disk.Drive || disk.DeviceID || disk.Name || 'C:';
|
|
let model = disk.Model || disk.Description || disk.FileSystem || 'Local Disk';
|
|
let health = disk.HealthStatus || disk.Status || 'Healthy';
|
|
let total = parseFloat(disk.Total_GB || disk.SizeGB || (disk.Size ? disk.Size / 1073741824 : 0));
|
|
let free = parseFloat(disk.Free_GB || disk.FreeGB || (disk.FreeSpace ? disk.FreeSpace / 1073741824 : 0));
|
|
let used = parseFloat(disk.Used_GB || (total - free));
|
|
|
|
let usagePercent = total > 0 ? ((used / total) * 100).toFixed(1) : 0;
|
|
let barColor = usagePercent > 85 ? 'bg-danger' : (usagePercent > 70 ? 'bg-warning' : 'bg-success');
|
|
let healthColor = health.toLowerCase() === 'healthy' || health.toLowerCase() === 'ok' ? 'text-success' : 'text-danger';
|
|
|
|
total = total > 0 ? total.toFixed(2) : 0;
|
|
free = free > 0 ? free.toFixed(2) : 0;
|
|
|
|
hwHtml += `
|
|
<div class="bg-gradient-to-b from-white to-slate-50 border border-slate-200 rounded-xl p-5 hover:shadow-md transition-shadow">
|
|
<div class="flex justify-between items-start mb-4">
|
|
<div>
|
|
<strong class="text-info font-black text-xl flex items-center"><i class="fa-solid fa-server mr-2 text-slate-400"></i> Drive ${driveLetter}</strong>
|
|
<div class="text-sm text-slate-500 mt-1.5 font-medium"><i class="fa-solid fa-microchip mr-1"></i> ${model}</div>
|
|
</div>
|
|
<div class="text-right flex flex-col items-end gap-2">
|
|
<span class="inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold text-white ${barColor} shadow-sm">${usagePercent}% Used</span>
|
|
<span class="inline-flex items-center px-2 py-0.5 rounded text-[11px] font-bold bg-white border border-slate-200 text-slate-700 shadow-sm"><i class="fa-solid fa-heart-pulse mr-1.5 ${healthColor}"></i> ${health}</span>
|
|
</div>
|
|
</div>
|
|
<div class="w-full bg-slate-200 border border-slate-300 rounded-full h-2 mb-4 overflow-hidden shadow-inner"><div class="${barColor} h-2 rounded-full transition-all duration-1000" style="width: ${usagePercent}%;"></div></div>
|
|
<div class="flex justify-between text-sm font-bold">
|
|
<span class="text-success"><i class="fa-solid fa-circle-check mr-1"></i> Free: ${free} GB</span>
|
|
<span class="text-slate-600">Total: ${total} GB</span>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
hwHtml += `</div>`;
|
|
} else {
|
|
hwHtml += `<div class="bg-slate-50 border border-slate-200 text-slate-500 p-5 rounded-xl flex items-center text-sm font-bold shadow-inner"><i class="fa-solid fa-info-circle text-info mr-3 text-2xl"></i> ไม่พบข้อมูล Drive C (รอรับ Log จาก Agent)</div>`;
|
|
}
|
|
hwHtml += `</div></div></div>`;
|
|
document.getElementById('content-hardware').innerHTML = hwHtml;
|
|
|
|
// --- 2. Network Tab ---
|
|
let netMap = {};
|
|
(data.netiface || []).forEach(net => {
|
|
if (net.state === 'up') netMap[net.name] = {
|
|
name: net.name,
|
|
mac: net.mac || '-',
|
|
mtu: net.mtu || '-',
|
|
rx_bytes: net.rx_bytes || 0,
|
|
tx_bytes: net.tx_bytes || 0,
|
|
ipv4: [],
|
|
ipv6: [],
|
|
gateway: '-',
|
|
dhcp: '-'
|
|
};
|
|
});
|
|
(data.netaddr || []).forEach(addr => {
|
|
let ifaceName = addr.iface || addr.name;
|
|
if (netMap[ifaceName]) {
|
|
if (addr.proto === 'ipv4' || !addr.address.includes(':')) netMap[ifaceName].ipv4.push({
|
|
ip: addr.address,
|
|
mask: addr.netmask
|
|
});
|
|
else netMap[ifaceName].ipv6.push(addr.address);
|
|
}
|
|
});
|
|
let activeNets = Object.values(netMap).filter(net => {
|
|
const name = (net.name || '').toLowerCase();
|
|
const vp = ['lo', 'veth', 'docker', 'br-', 'vmnet', 'vbox', 'tun', 'tap', 'wg', 'tailscale'];
|
|
return !(vp.some(p => name.startsWith(p)) || name.includes('loopback') || name.includes('virtual') || name.includes('teredo') || name.includes('vmware'));
|
|
});
|
|
|
|
if (activeNets.length > 0) {
|
|
let netHtml = `<div class="grid grid-cols-1 md:grid-cols-2 gap-5">`;
|
|
activeNets.forEach(net => {
|
|
let ipv4List = net.ipv4.length > 0 ? net.ipv4.map(ip => `<code class="bg-blue-50 px-2 py-0.5 rounded border border-blue-100 text-primary font-bold shadow-sm">${ip.ip}</code>`).join(' ') : 'No IPv4';
|
|
netHtml += `
|
|
<div class="bg-white border border-slate-200 border-l-4 border-l-success rounded-xl p-6 shadow-sm hover:shadow-md transition-all">
|
|
<div class="flex justify-between items-center mb-5 pb-3 border-b border-slate-100">
|
|
<h6 class="text-success font-black text-lg m-0 uppercase tracking-wide"><i class="fa-solid fa-ethernet mr-2"></i>${net.name}</h6>
|
|
<span class="bg-gradient-to-r from-emerald-500 to-green-600 text-white inline-flex items-center px-3 py-1 rounded-lg text-xs font-bold shadow-sm">UP</span>
|
|
</div>
|
|
<div class="space-y-4 text-sm">
|
|
<div class="flex justify-between items-center"><span class="text-slate-500 font-bold uppercase tracking-wider text-xs">MAC Address:</span> <span class="font-mono font-bold text-slate-700 bg-slate-50 border border-slate-200 px-2 py-1 rounded shadow-inner">${net.mac}</span></div>
|
|
<div class="flex justify-between items-start pt-3 border-t border-slate-50"><span class="text-slate-500 font-bold uppercase tracking-wider text-xs mt-1">IPv4 Address:</span> <div class="flex flex-col items-end gap-2">${ipv4List}</div></div>
|
|
</div>
|
|
</div>`;
|
|
});
|
|
document.getElementById('content-network').innerHTML = netHtml + `</div>`;
|
|
} else {
|
|
document.getElementById('content-network').innerHTML = `<div class="bg-blue-50 border border-blue-200 text-primary p-5 rounded-2xl flex items-center font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4"></i> No active physical networks found.</div>`;
|
|
}
|
|
|
|
// --- 3. Software Tab ---
|
|
if ($.fn.DataTable.isDataTable('#dt-software-modal')) {
|
|
$('#dt-software-modal').DataTable().destroy();
|
|
}
|
|
if (data.packages && data.packages.length > 0) {
|
|
let softHtml = `<div class="overflow-x-auto bg-white border border-slate-200 rounded-xl p-5 shadow-sm"><table id="dt-software-modal" class="w-full text-left"><thead><tr><th class="text-left pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Software Name</th><th class="pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Version</th><th class="pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Vendor</th></tr></thead><tbody class="text-sm">`;
|
|
data.packages.forEach(pkg => {
|
|
softHtml += `<tr class="border-b border-slate-100 hover:bg-slate-50 transition-colors"><td class="py-4 font-bold text-slate-800">${pkg.name || '-'}</td><td class="py-4 text-slate-600 font-medium">${pkg.version || '-'}</td><td class="py-4 text-slate-500 font-medium">${pkg.vendor || '-'}</td></tr>`;
|
|
});
|
|
document.getElementById('content-software').innerHTML = softHtml + `</tbody></table></div>`;
|
|
$('#dt-software-modal').DataTable({
|
|
pageLength: 10,
|
|
language: {
|
|
search: "_INPUT_",
|
|
searchPlaceholder: "Search software..."
|
|
}
|
|
});
|
|
} else {
|
|
document.getElementById('content-software').innerHTML = `<div class="bg-blue-50 border border-blue-200 text-primary p-5 rounded-2xl flex items-center font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4"></i> No Software data available.</div>`;
|
|
}
|
|
|
|
// --- 4. Hotfixes Tab ---
|
|
if ($.fn.DataTable.isDataTable('#dt-hotfixes-modal')) {
|
|
$('#dt-hotfixes-modal').DataTable().destroy();
|
|
}
|
|
if (data.hotfixes && data.hotfixes.length > 0) {
|
|
let hfHtml = `<div class="overflow-x-auto bg-white border border-slate-200 rounded-xl p-5 shadow-sm"><table id="dt-hotfixes-modal" class="w-full text-left"><thead><tr><th class="text-left pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Hotfix ID</th><th class="pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Description</th><th class="text-center pb-3 border-b-2 border-slate-200 text-slate-500 text-xs font-black uppercase tracking-wider">Action</th></tr></thead><tbody class="text-sm">`;
|
|
data.hotfixes.forEach(hf => {
|
|
let kbID = hf.hotfix || '-';
|
|
let desc = hf.description || hf.caption || 'System Update';
|
|
let kbLink = '-';
|
|
if (kbID.toUpperCase().includes('KB')) {
|
|
kbLink = `<a href="https://support.microsoft.com/help/${kbID.replace(/\D/g, '')}" target="_blank" class="inline-flex items-center px-4 py-1.5 text-xs font-bold rounded-lg text-primary bg-blue-50 hover:bg-primary hover:text-white transition-colors border border-blue-200 shadow-sm"><i class="fa-solid fa-arrow-up-right-from-square mr-1.5"></i> Info</a>`;
|
|
} else if (kbID !== '-') {
|
|
kbLink = `<a href="https://www.google.com/search?q=Windows+Update+${kbID}" target="_blank" class="inline-flex items-center px-4 py-1.5 text-xs font-bold rounded-lg text-slate-600 bg-slate-100 border border-slate-300 hover:bg-slate-200 transition-colors shadow-sm"><i class="fa-brands fa-google mr-1.5"></i> Search</a>`;
|
|
}
|
|
hfHtml += `<tr class="border-b border-slate-100 hover:bg-slate-50 transition-colors"><td class="py-4 font-black text-slate-800"><i class="fa-solid fa-shield-virus mr-2 text-success text-lg"></i>${kbID}</td><td class="py-4 text-slate-600 font-medium">${desc}</td><td class="py-4 text-center">${kbLink}</td></tr>`;
|
|
});
|
|
document.getElementById('content-hotfixes').innerHTML = hfHtml + `</tbody></table></div>`;
|
|
$('#dt-hotfixes-modal').DataTable({
|
|
pageLength: 10,
|
|
language: {
|
|
search: "_INPUT_",
|
|
searchPlaceholder: "Search updates..."
|
|
}
|
|
});
|
|
} else {
|
|
document.getElementById('content-hotfixes').innerHTML = `<div class="bg-blue-50 border border-blue-200 text-primary p-5 rounded-2xl flex items-center font-bold shadow-sm"><i class="fa-solid fa-info-circle text-2xl mr-4"></i> No Windows Update data available.</div>`;
|
|
}
|
|
|
|
// --- 🌟 5. Remote Desktop Tab (TeamViewer & AnyDesk) 🌟 ---
|
|
let tvId = (meta && meta.tv && meta.tv !== 'None') ? meta.tv : 'ไม่พบข้อมูล / ไม่ได้ติดตั้ง';
|
|
let tvActive = (tvId !== 'ไม่พบข้อมูล / ไม่ได้ติดตั้ง');
|
|
|
|
let adId = (meta && meta.ad && meta.ad !== 'None') ? meta.ad : 'ไม่พบข้อมูล / ไม่ได้ติดตั้ง';
|
|
let adActive = (adId !== 'ไม่พบข้อมูล / ไม่ได้ติดตั้ง');
|
|
|
|
let rdHtml = `
|
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
<div class="bg-white border border-slate-200 border-l-4 rounded-xl p-8 shadow-sm transition-all hover:shadow-md ${tvActive ? 'border-l-primary' : 'border-l-slate-300 opacity-75 grayscale-[50%]'}">
|
|
<div class="flex items-center mb-6 pb-4 border-b border-slate-100">
|
|
<div class="w-12 h-12 rounded-xl flex items-center justify-center mr-4 shadow-inner ${tvActive ? 'bg-gradient-to-br from-blue-500 to-blue-700 text-white' : 'bg-slate-100 text-slate-400 border border-slate-200'}"><i class="fa-solid fa-people-arrows text-2xl"></i></div>
|
|
<h6 class="font-black text-xl m-0 tracking-wide ${tvActive ? 'text-slate-900' : 'text-slate-500'}">TeamViewer</h6>
|
|
</div>
|
|
<div class="bg-slate-50 p-5 rounded-xl border border-slate-100 shadow-inner">
|
|
<p class="text-xs text-slate-500 uppercase tracking-wider mb-2 font-bold">TeamViewer ID (Client ID)</p>
|
|
<p class="font-black text-3xl m-0 ${tvActive ? 'text-primary' : 'text-slate-400'} tracking-widest font-mono drop-shadow-sm">${tvId}</p>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="bg-white border border-slate-200 border-l-4 rounded-xl p-8 shadow-sm transition-all hover:shadow-md ${adActive ? 'border-l-danger' : 'border-l-slate-300 opacity-75 grayscale-[50%]'}">
|
|
<div class="flex items-center mb-6 pb-4 border-b border-slate-100">
|
|
<div class="w-12 h-12 rounded-xl flex items-center justify-center mr-4 shadow-inner ${adActive ? 'bg-gradient-to-br from-red-500 to-red-700 text-white' : 'bg-slate-100 text-slate-400 border border-slate-200'}"><i class="fa-solid fa-desktop text-2xl"></i></div>
|
|
<h6 class="font-black text-xl m-0 tracking-wide ${adActive ? 'text-slate-900' : 'text-slate-500'}">AnyDesk</h6>
|
|
</div>
|
|
<div class="bg-slate-50 p-5 rounded-xl border border-slate-100 shadow-inner">
|
|
<p class="text-xs text-slate-500 uppercase tracking-wider mb-2 font-bold">AnyDesk Address (ID)</p>
|
|
<p class="font-black text-3xl m-0 ${adActive ? 'text-danger' : 'text-slate-400'} tracking-widest font-mono drop-shadow-sm">${adId}</p>
|
|
</div>
|
|
</div>
|
|
</div>`;
|
|
document.getElementById('content-remote-desktop').innerHTML = rdHtml;
|
|
|
|
}).catch(err => {
|
|
const errorMsg = `<div class="bg-red-50 border border-red-200 text-danger p-5 rounded-2xl flex items-center font-bold shadow-sm"><i class="fa-solid fa-xmark text-3xl mr-4"></i> Error Fetching Data</div>`;
|
|
document.getElementById('content-hardware').innerHTML = document.getElementById('content-network').innerHTML = document.getElementById('content-software').innerHTML = document.getElementById('content-hotfixes').innerHTML = document.getElementById('content-remote-desktop').innerHTML = errorMsg;
|
|
});
|
|
}
|
|
</script>
|
|
</body>
|
|
|
|
</html>
|