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

456 lines
23 KiB
JavaScript

// 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 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-300"></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">${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() {
$('#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.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-300"></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) {
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
});
}
});
};