添加 templates/index.html

This commit is contained in:
coolsd 2024-09-21 20:59:16 +08:00
parent 145ab01d4f
commit c410ec8a58

927
templates/index.html Normal file
View File

@ -0,0 +1,927 @@
<!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>