khd/templates/index.html

927 lines
32 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>客户端控制面板</title>
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.js"></script>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/css/bootstrap.min.css" rel="stylesheet">
<style>
body { padding: 20px; }
.config { margin-bottom: 20px; }
.actions { margin-bottom: 20px; }
#commandResults {
margin-top: 20px;
border: 1px solid #ddd;
padding: 10px;
height: 400px;
overflow-y: auto;
}
.result-item {
margin-bottom: 10px;
padding: 5px;
border-bottom: 1px solid #eee;
}
.result-item pre {
margin: 5px 0;
white-space: pre-wrap;
word-wrap: break-word;
}
.modal-dialog {
display: flex;
align-items: center;
min-height: calc(100% - 1rem);
}
#manualCommand {
height: 150px;
}
.group-tag {
display: inline-block;
margin-right: 5px;
margin-bottom: 5px;
padding: 5px 10px;
background-color: #f0f0f0;
border-radius: 3px;
cursor: pointer;
}
.group-tag.active {
background-color: #007bff;
color: white;
}
#clientTable tbody tr {
cursor: pointer;
}
#clientTable tbody tr.selected {
background-color: #e0e0e0;
}
#selectionBox {
position: absolute;
border: 1px solid #007bff;
background-color: rgba(0, 123, 255, 0.1);
pointer-events: none;
}
</style>
</head>
<body>
<div id="selectionBox" style="display: none;"></div>
<div class="container">
<div class="row mt-3 mb-3">
<div class="col-md-12 text-end">
<a href="/logout" class="btn btn-secondary">登出</a>
</div>
</div>
<h1 class="mb-4">客户端控制面板</h1>
<div class="config row">
<div class="col-md-3">
<label for="heartbeatTime" class="form-label">心跳时间(秒):</label>
<input type="number" id="heartbeatTime" class="form-control" value="30">
</div>
<div class="col-md-3">
<label for="reportTime" class="form-label">报告时间(秒):</label>
<input type="number" id="reportTime" class="form-control" value="10">
</div>
<div class="col-md-3 mt-4">
<button onclick="updateSettings()" class="btn btn-primary">更新设置</button>
</div>
</div>
<div class="config row">
<div class="col-md-3">
<label for="configSelector" class="form-label">选择配置:</label>
<select id="configSelector" class="form-select" onchange="loadSelectedConfig()">
<option value="">-- 选择配置 --</option>
</select>
</div>
<div class="col-md-3">
<label for="configName" class="form-label">配置名称:</label>
<input type="text" id="configName" class="form-control" placeholder="输入新配置名称">
</div>
<div class="col-md-3 mt-4">
<button onclick="saveConfig()" class="btn btn-success">保存配置</button>
<button onclick="deleteConfig()" class="btn btn-danger">删除配置</button>
</div>
</div>
<div class="config row">
<div class="col-md-3">
<label for="updateUrl" class="form-label">更新URL:</label>
<input type="text" id="updateUrl" class="form-control" value="https://example.com/update">
</div>
<div class="col-md-3">
<label for="programName" class="form-label">程序名:</label>
<input type="text" id="programName" class="form-control" value="example-program">
</div>
<div class="col-md-3">
<label for="programPath" class="form-label">程序路径:</label>
<input type="text" id="programPath" class="form-control" value="/path/to/program">
</div>
<div class="col-md-3">
<label for="commandParams" class="form-label">命令参数:</label>
<input type="text" id="commandParams" class="form-control" value="--param value">
</div>
</div>
<div class="group-selector row mb-3">
<div class="col-md-12">
<label class="form-label">选择分组:</label>
<div id="groupTags"></div>
</div>
<div class="col-md-3 mt-2">
<input type="text" id="newGroupName" class="form-control" placeholder="新分组名称">
</div>
<div class="col-md-3 mt-2">
<button onclick="addNewGroup()" class="btn btn-secondary">添加新分组</button>
</div>
</div>
<div class="actions">
<button onclick="executeCommand('start')" class="btn btn-success">启动程序</button>
<button onclick="executeCommand('stop')" class="btn btn-danger">停止程序</button>
<button onclick="executeCommand('update')" class="btn btn-info">更新程序</button>
<button onclick="executeCommand('restart')" class="btn btn-warning">重启程序</button>
<button onclick="showManualCommandModal()" class="btn btn-secondary">手动命令</button>
<button onclick="deleteSelectedClients()" class="btn btn-danger">删除选中客户端</button>
<button onclick="changeBulkClientGroup()" class="btn btn-info">批量更改分组</button>
<button onclick="executeCommand('force_update')" class="btn btn-warning">强制更新</button>
</div>
<div class="mt-3">
<button onclick="selectAllClients()" class="btn btn-outline-primary">全选</button>
<button onclick="deselectAllClients()" class="btn btn-outline-secondary">取消全选</button>
</div>
<table id="clientTable" class="table table-hover mt-3">
<thead>
<tr>
<th>ID</th>
<th>客户端ID</th>
<th>IP地址</th>
<th>CPU使用率</th>
<th>命令状态</th>
<th>程序状态</th>
<th>分组</th>
<th>操作</th>
</tr>
</thead>
<tbody></tbody>
</table>
<div id="commandResults">
<h3>命令执行结果</h3>
</div>
</div>
<!-- 手动命令模态框 -->
<div class="modal fade" id="manualCommandModal" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">输入手动命令</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<textarea id="manualCommand" class="form-control" placeholder="输入命令"></textarea>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">取消</button>
<button type="button" class="btn btn-primary" onclick="executeManualCommand()">执行</button>
</div>
</div>
</div>
</div>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.3/dist/js/bootstrap.bundle.min.js"></script>
<script>
const ACCESS_TOKEN = '131417';
const socket = io('http://ore.uqdm.com:5003', {
transports: ['websocket', 'polling'],
upgrade: true,
auth: {
token: ACCESS_TOKEN
}
});
let clients = {};
let groups = [];
let manualCommandModal;
let selectedClients = new Set();
let currentGroup = 'all';
let isMouseDown = false;
let startX, startY;
const selectionBox = document.getElementById('selectionBox');
socket.on('connect', () => {
console.log('Connected to server');
fetchClients();
loadConfig();
});
socket.on('command_result', (data) => {
console.log('Received real-time command result:', data);
displayCommandResult(data);
});
function fetchClients() {
console.log('Fetching clients...');
fetch('/api/clients', {
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(data => {
console.log('Received client data:', data);
if (data.clients && Object.keys(data.clients).length > 0) {
clients = data.clients;
groups = data.groups;
updateClientTable();
updateGroupTags();
} else {
console.warn('No clients data received');
document.querySelector('#clientTable tbody').innerHTML = '<tr><td colspan="8">No clients found</td></tr>';
}
})
.catch(error => {
console.error('Error fetching clients:', error);
document.querySelector('#clientTable tbody').innerHTML = '<tr><td colspan="8">Error loading clients</td></tr>';
});
}
function updateClientTable() {
console.log('Updating client table');
console.log('Current group:', currentGroup);
console.log('Clients:', clients);
const tbody = document.querySelector('#clientTable tbody');
tbody.innerHTML = '';
let clientCount = 0;
for (const [clientId, data] of Object.entries(clients)) {
if (currentGroup === 'all' || data.group === currentGroup) {
clientCount++;
const row = tbody.insertRow();
row.dataset.clientId = clientId;
row.className = selectedClients.has(clientId) ? 'selected' : '';
// 添加序号列
row.insertCell(0).textContent = clientCount;
row.insertCell(1).textContent = clientId;
row.insertCell(2).textContent = data.system_info?.ip_address || 'N/A';
row.insertCell(3).textContent = data.system_info?.cpu || 'N/A';
const commandStatusCell = row.insertCell(4);
if (data.command_status && typeof data.command_status === 'object') {
commandStatusCell.textContent = data.command_status.status || 'N/A';
commandStatusCell.title = data.command_status.output || data.command_status.error || '';
} else {
commandStatusCell.textContent = 'N/A';
}
row.insertCell(5).textContent = data.system_info?.program_status || 'N/A';
row.insertCell(6).textContent = data.group || 'default';
row.insertCell(7).innerHTML = `<button onclick="changeClientGroup('${clientId}')" class="btn btn-sm btn-outline-primary">更改分组</button>`;
}
}
console.log(`Updated table with ${clientCount} clients`);
}
function updateGroupTags() {
const groupTags = document.getElementById('groupTags');
groupTags.innerHTML = '';
const allTag = document.createElement('span');
allTag.className = `group-tag ${currentGroup === 'all' ? 'active' : ''}`;
allTag.textContent = '全部';
allTag.onclick = () => selectGroup('all');
groupTags.appendChild(allTag);
groups.forEach(group => {
const tag = document.createElement('span');
tag.className = `group-tag ${currentGroup === group ? 'active' : ''}`;
tag.textContent = group;
tag.onclick = () => selectGroup(group);
if (group !== 'default' && group !== '离线') {
const deleteBtn = document.createElement('button');
deleteBtn.textContent = 'x';
deleteBtn.className = 'btn btn-sm btn-danger ml-1';
deleteBtn.onclick = (e) => {
e.stopPropagation();
deleteGroup(group);
};
tag.appendChild(deleteBtn);
}
groupTags.appendChild(tag);
});
}
function selectGroup(group) {
currentGroup = group;
updateGroupTags();
updateClientTable();
}
function toggleClientSelection(clientId, row, addToSelection = true) {
if (addToSelection) {
if (selectedClients.has(clientId)) {
selectedClients.delete(clientId);
row.classList.remove('selected');
} else {
selectedClients.add(clientId);
row.classList.add('selected');
}
} else {
if (selectedClients.has(clientId)) {
selectedClients.delete(clientId);
row.classList.remove('selected');
}
}
}
function selectAllClients() {
const rows = document.querySelectorAll('#clientTable tbody tr');
rows.forEach(row => {
const clientId = row.dataset.clientId;
selectedClients.add(clientId);
row.classList.add('selected');
});
}
function deselectAllClients() {
const rows = document.querySelectorAll('#clientTable tbody tr');
rows.forEach(row => {
const clientId = row.dataset.clientId;
selectedClients.delete(clientId);
row.classList.remove('selected');
});
}
function getSelectedClients() {
return Array.from(selectedClients);
}
function updateClientStatus(clientId, data) {
console.log('Updating client status:', clientId, data);
if (clients[clientId]) {
clients[clientId].command_status = {
status: data.status || 'N/A',
program_status: data.program_status || 'N/A'
};
if (data.output) {
clients[clientId].command_status.output = data.output;
}
if (data.error) {
clients[clientId].command_status.error = data.error;
}
}
}
function executeCommand(action) {
const selectedClients = getSelectedClients();
if (selectedClients.length === 0) {
alert('请选择至少一个客户端');
return;
}
let command = action;
let params = {
updateUrl: document.getElementById('updateUrl').value,
programName: document.getElementById('programName').value,
programPath: document.getElementById('programPath').value,
commandParams: document.getElementById('commandParams').value
};
if (action === 'manual') {
params.manualCommand = document.getElementById('manualCommand').value;
}
// 清空之前的结果
const resultsDiv = document.getElementById('commandResults');
while (resultsDiv.children.length > 1) { // 保留标题 <h3>
resultsDiv.removeChild(resultsDiv.lastChild);
}
fetch('/api/execute_command', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({
client_ids: selectedClients,
command: command,
params: params
}),
})
.then(response => response.json())
.then(data => {
console.log('Command sent:', data);
alert(`命令已发送到 ${data.affected_clients} 个客户端。请等待结果...`);
selectedClients.forEach(clientId => {
if (clients[clientId]) {
updateClientStatus(clientId, { status: '执行中' });
}
});
updateClientTable();
})
.catch(error => {
console.error('Error sending command:', error);
alert('发送命令时出错');
});
}
function updateSettings() {
const heartbeatTime = document.getElementById('heartbeatTime').value;
const reportTime = document.getElementById('reportTime').value;
fetch('/api/update_settings', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ heartbeatTime, reportTime }),
})
.then(response => response.json())
.then(data => {
console.log('Settings updated:', data);
alert('设置已更新');
});
}
function addNewGroup() {
const newGroup = document.getElementById('newGroupName').value;
if (newGroup && !groups.includes(newGroup)) {
fetch('/api/add_group', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ group: newGroup }),
})
.then(response => response.json())
.then(data => {
console.log('Group added:', data);
alert(`新分组 "${newGroup}" 已添加`);
fetchClients();
});
} else {
alert("分组名称无效或已存在");
}
}
function deleteGroup(group) {
if (group === 'default' || group === '离线') {
alert('无法删除"默认"或"离线"分组');
return;
}
if (confirm(`确定要删除分组 "${group}" 吗?`)) {
fetch('/api/delete_group', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ group: group }),
})
.then(response => response.json())
.then(data => {
console.log('Group deleted:', data);
alert(`分组 "${group}" 已删除`);
fetchClients();
});
}
}
function changeClientGroup(clientId) {
const newGroup = prompt(`请为客户端 ${clientId} 输入新的分组名称:`);
if (newGroup) {
fetch('/api/update_client_group', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ client_ids: [clientId], group: newGroup }),
})
.then(response => response.json())
.then(data => {
console.log('Client group updated:', data);
if (data.status === "Client group updated") {
alert(`客户端 ${clientId} 已更新到分组 "${newGroup}"`);
fetchClients();
} else {
alert('更新分组失败');
}
});
}
}
function displayCommandResult(data) {
console.log('Displaying command result:', data);
const resultsDiv = document.getElementById('commandResults');
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
// 创建一个 Date 对象并格式化为北京时间
const timestamp = new Date(data.timestamp);
const localTime = timestamp.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
resultItem.innerHTML = `
<strong>客户端 ID:</strong> ${data.client_id}<br>
<strong>命令:</strong> ${data.command || 'N/A'}<br>
<strong>状态:</strong> ${data.status || 'N/A'}<br>
<strong>输出:</strong> <pre>${data.output || '无输出'}</pre>
<strong>错误:</strong> <pre>${data.error || '无错误'}</pre>
<strong>程序状态:</strong> ${data.program_status || 'N/A'}<br>
<strong>时间:</strong> ${localTime}
`;
// 添加新结果到底部
resultsDiv.appendChild(resultItem);
// 保持最多显示10条结果
while (resultsDiv.children.length > 11) { // 11 = 标题 + 10条结果
resultsDiv.removeChild(resultsDiv.children[1]);
}
// 滚动到底部
resultsDiv.scrollTop = resultsDiv.scrollHeight;
updateClientStatus(data.client_id, {
status: data.status,
program_status: data.program_status
});
updateClientTable();
}
function showManualCommandModal() {
if (!manualCommandModal) {
manualCommandModal = new bootstrap.Modal(document.getElementById('manualCommandModal'));
}
manualCommandModal.show();
}
function executeManualCommand() {
const command = document.getElementById('manualCommand').value;
if (command) {
executeCommand('manual');
manualCommandModal.hide();
} else {
alert('请输入命令');
}
}
function saveConfig() {
const configName = document.getElementById('configName').value;
if (!configName) {
alert('请输入配置名称');
return;
}
const config = {
updateUrl: document.getElementById('updateUrl').value,
programName: document.getElementById('programName').value,
programPath: document.getElementById('programPath').value,
commandParams: document.getElementById('commandParams').value
};
fetch('/api/save_config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ name: configName, config: config }),
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert('配置已保存');
loadConfig();
} else {
alert('保存配置失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('保存配置时发生错误');
});
}
function loadConfig() {
fetch('/api/get_configs', {
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`
}
})
.then(response => response.json())
.then(configs => {
updateConfigSelector(configs);
})
.catch(error => {
console.error('Error:', error);
alert('加载配置时发生错误');
});
}
function updateConfigSelector(configs) {
const selector = document.getElementById('configSelector');
selector.innerHTML = '<option value="">-- 选择配置 --</option>';
for (const [name, _] of Object.entries(configs)) {
const option = document.createElement('option');
option.value = name;
option.textContent = name;
selector.appendChild(option);
}
}
function loadSelectedConfig() {
const configName = document.getElementById('configSelector').value;
if (!configName) {
// 如果没有选择配置,清空所有字段
clearConfigFields();
return;
}
fetch('/api/get_configs', {
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`
}
})
.then(response => {
if (!response.ok) {
throw new Error('Network response was not ok');
}
return response.json();
})
.then(configs => {
const config = configs[configName];
if (config) {
document.getElementById('updateUrl').value = config.updateUrl || '';
document.getElementById('programName').value = config.programName || '';
document.getElementById('programPath').value = config.programPath || '';
document.getElementById('commandParams').value = config.commandParams || '';
document.getElementById('configName').value = configName;
} else {
throw new Error('Selected configuration not found');
}
})
.catch(error => {
console.error('Error loading configuration:', error);
alert('加载配置时发生错误: ' + error.message);
clearConfigFields();
});
}
function clearConfigFields() {
document.getElementById('updateUrl').value = '';
document.getElementById('programName').value = '';
document.getElementById('programPath').value = '';
document.getElementById('commandParams').value = '';
document.getElementById('configName').value = '';
}
function deleteConfig() {
const configName = document.getElementById('configSelector').value;
if (!configName) {
alert('请选择要删除的配置');
return;
}
if (confirm(`确定要删除配置 "${configName}" 吗?`)) {
fetch('/api/delete_config', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ name: configName }),
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert('配置已删除');
loadConfig();
// 清空输入框
clearConfigFields();
} else {
alert('删除配置失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('删除配置时发生错误');
});
}
}
function deleteSelectedClients() {
const selectedClients = getSelectedClients();
if (selectedClients.length === 0) {
alert('请选择至少一个客户端');
return;
}
if (confirm(`确定要删除选中的 ${selectedClients.length} 个客户端吗?`)) {
fetch('/api/delete_clients', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ client_ids: selectedClients }),
})
.then(response => response.json())
.then(data => {
if (data.status === 'success') {
alert(data.message);
selectedClients.forEach(clientId => {
delete clients[clientId];
});
selectedClients.clear();
updateClientTable();
} else {
alert('删除客户端失败: ' + data.message);
}
})
.catch(error => {
console.error('Error:', error);
alert('删除客户端时发生错误');
});
}
}
function changeBulkClientGroup() {
const selectedClients = getSelectedClients();
if (selectedClients.length === 0) {
alert('请选择至少一个客户端');
return;
}
const newGroup = prompt(`请为选中的 ${selectedClients.length} 个客户端输入新的分组名称:`);
if (newGroup) {
fetch('/api/update_client_group', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${ACCESS_TOKEN}`
},
body: JSON.stringify({ client_ids: selectedClients, group: newGroup }),
})
.then(response => response.json())
.then(data => {
console.log('Client group updated:', data);
if (data.status === "Client group updated") {
alert(`${selectedClients.length} 个客户端已更新到分组 "${newGroup}"`);
fetchClients();
} else {
alert('更新分组失败');
}
});
}
}
function fetchCommandResults() {
console.log('Fetching command results...');
fetch('/api/command_results', {
headers: {
'Authorization': `Bearer ${ACCESS_TOKEN}`
}
})
.then(response => {
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return response.json();
})
.then(results => {
console.log('Received command results:', results);
// 清空现有结果
const resultsDiv = document.getElementById('commandResults');
while (resultsDiv.children.length > 1) { // 保留标题 <h3>
resultsDiv.removeChild(resultsDiv.lastChild);
}
// 按时间正序排列结果
results.sort((a, b) => new Date(a.timestamp) - new Date(b.timestamp));
// 显示最新的结果最多10条
results.slice(-10).forEach(result => {
const resultItem = document.createElement('div');
resultItem.className = 'result-item';
// 创建一个 Date 对象并格式化为北京时间
const timestamp = new Date(result.timestamp);
const localTime = timestamp.toLocaleString('zh-CN', { timeZone: 'Asia/Shanghai' });
resultItem.innerHTML = `
<strong>客户端 ID:</strong> ${result.client_id}<br>
<strong>命令:</strong> ${result.command || 'N/A'}<br>
<strong>状态:</strong> ${result.status || 'N/A'}<br>
<strong>输出:</strong> <pre>${result.output || '无输出'}</pre>
<strong>错误:</strong> <pre>${result.error || '无错误'}</pre>
<strong>程序状态:</strong> ${result.program_status || 'N/A'}<br>
<strong>时间:</strong> ${localTime}
`;
resultsDiv.appendChild(resultItem);
});
// 滚动到底部
resultsDiv.scrollTop = resultsDiv.scrollHeight;
})
.catch(error => {
console.error('Error fetching command results:', error);
});
}
function handleMouseDown(e) {
if (e.target.closest('#clientTable')) {
isMouseDown = true;
startX = e.clientX;
startY = e.clientY;
selectionBox.style.left = startX + 'px';
selectionBox.style.top = startY + 'px';
}
}
function handleMouseMove(e) {
if (!isMouseDown) return;
const currentX = e.clientX;
const currentY = e.clientY;
const boxLeft = Math.min(startX, currentX);
const boxTop = Math.min(startY, currentY);
const boxWidth = Math.abs(currentX - startX);
const boxHeight = Math.abs(currentY - startY);
selectionBox.style.display = 'block';
selectionBox.style.left = boxLeft + 'px';
selectionBox.style.top = boxTop + 'px';
selectionBox.style.width = boxWidth + 'px';
selectionBox.style.height = boxHeight + 'px';
const tableRect = document.getElementById('clientTable').getBoundingClientRect();
const rows = document.querySelectorAll('#clientTable tbody tr');
rows.forEach(row => {
const rowRect = row.getBoundingClientRect();
const isIntersecting = !(rowRect.right < boxLeft ||
rowRect.left > boxLeft + boxWidth ||
rowRect.bottom < boxTop ||
rowRect.top > boxTop + boxHeight);
if (isIntersecting) {
const clientId = row.dataset.clientId;
toggleClientSelection(clientId, row, true);
} else if (!e.shiftKey && !e.ctrlKey && !e.metaKey) {
const clientId = row.dataset.clientId;
toggleClientSelection(clientId, row, false);
}
});
}
function handleMouseUp() {
isMouseDown = false;
selectionBox.style.display = 'none';
selectionBox.style.width = '0';
selectionBox.style.height = '0';
}
function handleRowClick(e) {
const row = e.target.closest('tr');
if (!row) return;
const clientId = row.dataset.clientId;
if (e.shiftKey) {
const rows = Array.from(document.querySelectorAll('#clientTable tbody tr'));
const lastSelectedIndex = rows.findIndex(r => r.classList.contains('selected'));
const currentIndex = rows.indexOf(row);
const start = Math.min(lastSelectedIndex, currentIndex);
const end = Math.max(lastSelectedIndex, currentIndex);
for (let i = start; i <= end; i++) {
const r = rows[i];
toggleClientSelection(r.dataset.clientId, r, true);
}
} else if (e.ctrlKey || e.metaKey) {
toggleClientSelection(clientId, row, true);
} else {
selectedClients.clear();
document.querySelectorAll('#clientTable tbody tr').forEach(r => r.classList.remove('selected'));
toggleClientSelection(clientId, row, true);
}
}
function init() {
console.log('Initializing...');
fetchClients();
loadConfig();
fetchCommandResults();
setInterval(fetchClients, 10000);
setInterval(fetchCommandResults, 5000);
document.addEventListener('mousedown', handleMouseDown);
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
document.getElementById('clientTable').addEventListener('click', handleRowClick);
}
window.addEventListener('load', init);
</script>
</body>
</html>