Add ResourceHub for real-time updates and implement web dashboard with REST API

- Created ResourceHub.cs for SignalR group management.
- Developed a modern web dashboard using Tailwind CSS for responsive design.
- Implemented real-time updates with SignalR for CPU, Memory, GPU, and Network usage.
- Added REST API endpoints for resource information and process management.
- Integrated process management features to view and terminate high-usage processes.
- Enhanced UI with loading spinners, notifications, and responsive tables.
- Included performance charts for historical CPU and Memory usage.
- Configured Swagger UI for API documentation.
- Established security features including process kill restrictions and API key authentication.
This commit is contained in:
Phoenix
2025-08-07 23:02:03 +08:00
parent aa30c9f034
commit 3d47fc1439
23 changed files with 1405 additions and 547 deletions
+633
View File
@@ -0,0 +1,633 @@
// Dashboard JavaScript
class ResourceDashboard {
constructor() {
this.connection = null;
this.cpuChart = null;
this.memoryChart = null;
this.cpuHistory = [];
this.memoryHistory = [];
this.maxHistoryPoints = 20;
this.lastResourceData = null; // Store latest resource data
this.lastSystemInfo = null; // Store latest system info
this.autoRefreshEnabled = true; // Auto-refresh toggle
this.refreshInterval = null; // Manual refresh interval
this.init();
}
async init() {
this.setupEventListeners();
this.initializeCharts();
await this.connectSignalR();
await this.loadInitialData();
this.hideLoading();
this.startAutoRefresh(); // Use our new auto-refresh system
}
setupEventListeners() {
document.getElementById('toggleAutoRefresh').addEventListener('click', () => {
this.toggleAutoRefresh();
});
document.getElementById('toggleProcesses').addEventListener('click', () => {
this.toggleProcessesSection();
});
document.getElementById('toggleDetails').addEventListener('click', () => {
this.toggleDetailsSection();
});
document.getElementById('refreshData').addEventListener('click', () => {
this.refreshData();
});
}
toggleDetailsSection() {
const detailsSection = document.getElementById('detailsSection');
const toggleButton = document.getElementById('toggleDetails');
if (detailsSection.classList.contains('hidden')) {
detailsSection.classList.remove('hidden');
toggleButton.innerHTML = '<i class="fas fa-info-circle mr-2"></i>Hide Details';
// Refresh disk usage when details section becomes visible
this.refreshDetailsData();
} else {
detailsSection.classList.add('hidden');
toggleButton.innerHTML = '<i class="fas fa-info-circle mr-2"></i>Details';
}
}
toggleProcessesSection() {
const processesSection = document.getElementById('processesSection');
const toggleButton = document.getElementById('toggleProcesses');
if (processesSection.classList.contains('hidden')) {
processesSection.classList.remove('hidden');
toggleButton.innerHTML = '<i class="fas fa-list mr-2"></i>Hide Processes';
// Refresh processes when section becomes visible
this.refreshProcessesData();
} else {
processesSection.classList.add('hidden');
toggleButton.innerHTML = '<i class="fas fa-list mr-2"></i>Processes';
}
}
async refreshProcessesData() {
try {
// If we have cached data, use it immediately
if (this.lastResourceData && this.lastResourceData.topProcesses) {
this.updateProcessTable(this.lastResourceData.topProcesses);
}
// Then fetch fresh data
const resourceUsage = await this.fetchData('/api/resource/usage');
this.updateProcessTable(resourceUsage.topProcesses);
} catch (error) {
console.error('Error refreshing processes data:', error);
}
}
async refreshDetailsData() {
try {
// If we have cached data, use it immediately
if (this.lastResourceData && this.lastResourceData.disks) {
this.updateDiskUsage(this.lastResourceData.disks);
}
if (this.lastSystemInfo) {
this.updateSystemInfo(this.lastSystemInfo);
}
// Then fetch fresh data
const [resourceUsage, systemInfo] = await Promise.all([
this.fetchData('/api/resource/usage'),
this.fetchData('/api/resource/system-info')
]);
this.updateDiskUsage(resourceUsage.disks);
this.updateSystemInfo(systemInfo);
} catch (error) {
console.error('Error refreshing details data:', error);
}
}
toggleAutoRefresh() {
this.autoRefreshEnabled = !this.autoRefreshEnabled;
const toggleButton = document.getElementById('toggleAutoRefresh');
if (this.autoRefreshEnabled) {
toggleButton.innerHTML = '<i class="fas fa-sync mr-2"></i>Auto: ON';
toggleButton.className = 'bg-yellow-500 hover:bg-yellow-700 px-4 py-2 rounded-lg transition-colors';
this.startAutoRefresh();
} else {
toggleButton.innerHTML = '<i class="fas fa-pause mr-2"></i>Auto: OFF';
toggleButton.className = 'bg-gray-500 hover:bg-gray-700 px-4 py-2 rounded-lg transition-colors';
this.stopAutoRefresh();
}
console.log(`Auto-refresh ${this.autoRefreshEnabled ? 'enabled' : 'disabled'}`);
}
startAutoRefresh() {
// Connect SignalR for real-time updates
if (!this.connection || this.connection.state === 'Disconnected') {
this.connectSignalR();
}
// Also start a fallback manual refresh timer (every 60 seconds as backup)
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
}
this.refreshInterval = setInterval(() => {
if (this.autoRefreshEnabled) {
this.refreshData();
}
}, 60000); // 60 second fallback
}
stopAutoRefresh() {
// Disconnect SignalR
if (this.connection && this.connection.state === 'Connected') {
this.connection.stop();
}
// Clear manual refresh timer
if (this.refreshInterval) {
clearInterval(this.refreshInterval);
this.refreshInterval = null;
}
}
async connectSignalR() {
try {
// Only connect if auto-refresh is enabled
if (!this.autoRefreshEnabled) {
console.log("SignalR connection skipped - auto-refresh disabled");
return;
}
this.connection = new signalR.HubConnectionBuilder()
.withUrl("/resourceHub")
.build();
this.connection.on("ResourceUpdate", (data) => {
if (this.autoRefreshEnabled) {
this.updateDashboard(data);
}
});
await this.connection.start();
console.log("SignalR Connected");
} catch (err) {
console.error("SignalR Connection Error: ", err);
}
}
async loadInitialData() {
try {
const [resourceUsage, systemInfo] = await Promise.all([
this.fetchData('/api/resource/usage'),
this.fetchData('/api/resource/system-info')
]);
this.updateDashboard(resourceUsage);
this.updateSystemInfo(systemInfo);
} catch (error) {
console.error('Error loading initial data:', error);
}
}
async fetchData(url) {
const response = await fetch(url);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
return await response.json();
}
updateDashboard(data) {
try {
// Store the latest data for when details section is opened
this.lastResourceData = data;
// Update CPU
if (data.cpu) {
const cpuUsage = data.cpu.usage || 0;
document.getElementById('cpuUsage').textContent = `${cpuUsage.toFixed(1)}%`;
document.getElementById('cpuBar').style.width = `${cpuUsage}%`;
this.updateCpuChart(cpuUsage);
}
// Update Memory
if (data.memory) {
const memUsage = data.memory.usagePercentage || 0;
document.getElementById('memoryUsage').textContent = `${memUsage.toFixed(1)}%`;
document.getElementById('memoryBar').style.width = `${memUsage}%`;
this.updateMemoryChart(memUsage);
}
// Update GPU
if (data.gpu) {
const gpuUsage = data.gpu.usage || 0;
document.getElementById('gpuUsage').textContent = `${gpuUsage.toFixed(1)}%`;
document.getElementById('gpuBar').style.width = `${gpuUsage}%`;
}
// Update Network
if (data.network) {
const bytesReceived = data.network.bytesReceived || 0;
const bytesSent = data.network.bytesSent || 0;
const totalSpeed = (bytesReceived + bytesSent) / 1024 / 1024;
document.getElementById('networkSpeed').textContent = `${totalSpeed.toFixed(1)} MB/s`;
document.getElementById('networkDetail').textContent =
`${(bytesSent / 1024 / 1024).toFixed(1)} MB/s ↓ ${(bytesReceived / 1024 / 1024).toFixed(1)} MB/s`;
}
// Update Game Detection
if (data.runningGame) {
this.updateGameInfo(data.runningGame);
} else {
this.updateGameInfo(null);
}
// Update Processes (only if processes section is visible)
if (data.topProcesses && Array.isArray(data.topProcesses) && !document.getElementById('processesSection').classList.contains('hidden')) {
this.updateProcessTable(data.topProcesses);
}
// Update Disk Usage (only if details section is visible)
if (data.disks && !document.getElementById('detailsSection').classList.contains('hidden')) {
this.updateDiskUsage(data.disks);
}
} catch (error) {
console.error('Error updating dashboard:', error);
this.showNotification('Error updating dashboard data', 'error');
}
}
updateProcessTable(processes) {
const tableBody = document.getElementById('processTable');
tableBody.innerHTML = '';
if (!processes || !Array.isArray(processes)) {
const row = document.createElement('tr');
row.innerHTML = '<td colspan="4" class="px-4 py-4 text-center text-gray-500">No process data available</td>';
tableBody.appendChild(row);
return;
}
processes.slice(0, 10).forEach((process, index) => {
if (!process) return;
const row = document.createElement('tr');
row.className = 'hover:bg-gray-50';
const killButtonClass = index < 3 ?
'bg-red-500 hover:bg-red-700 text-white px-2 py-1 rounded text-xs transition-colors' :
'bg-gray-300 text-gray-500 px-2 py-1 rounded text-xs cursor-not-allowed';
const killButtonDisabled = index >= 3 ? 'disabled' : '';
const processName = process.name || 'Unknown';
const processId = process.processId || 0;
const cpuUsage = process.cpuUsage || 0;
const memoryUsage = process.memoryUsage || 0;
row.innerHTML = `
<td class="px-4 py-4 whitespace-nowrap">
<div class="flex items-center">
<div class="text-sm font-medium text-gray-900">${processName}</div>
<div class="text-sm text-gray-500 ml-2">PID: ${processId}</div>
</div>
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
${cpuUsage.toFixed(1)}%
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
${(memoryUsage / 1024 / 1024).toFixed(1)} MB
</td>
<td class="px-4 py-4 whitespace-nowrap text-sm text-gray-900">
<button onclick="dashboard.killProcess(${processId}, '${processName}')"
class="${killButtonClass}" ${killButtonDisabled}>
<i class="fas fa-times mr-1"></i>Kill
</button>
</td>
`;
tableBody.appendChild(row);
});
}
async killProcess(processId, processName) {
if (!confirm(`Are you sure you want to terminate "${processName}" (PID: ${processId})?`)) {
return;
}
try {
const response = await fetch(`/api/resource/kill-process/${processId}`, {
method: 'POST'
});
if (response.ok) {
const result = await response.json();
this.showNotification(result.message, 'success');
await this.refreshData();
} else {
const error = await response.text();
this.showNotification(`Failed to kill process: ${error}`, 'error');
}
} catch (error) {
console.error('Error killing process:', error);
this.showNotification('Error killing process', 'error');
}
}
updateSystemInfo(systemInfo) {
// Store the latest system info
this.lastSystemInfo = systemInfo;
const systemInfoDiv = document.getElementById('systemInfo');
systemInfoDiv.innerHTML = `
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">Machine Name</h4>
<p class="text-gray-600">${systemInfo.machineName || 'N/A'}</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">OS Version</h4>
<p class="text-gray-600">${systemInfo.osVersion || 'N/A'}</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">CPU</h4>
<p class="text-gray-600">${systemInfo.cpuName || 'N/A'}</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">Processor Count</h4>
<p class="text-gray-600">${systemInfo.processorCount || 0} cores</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">Total Memory</h4>
<p class="text-gray-600">${systemInfo.totalPhysicalMemory ? (systemInfo.totalPhysicalMemory / 1024 / 1024 / 1024).toFixed(1) : 'N/A'} GB</p>
</div>
<div class="bg-gray-50 p-4 rounded-lg">
<h4 class="font-semibold text-gray-700">Uptime</h4>
<p class="text-gray-600">${systemInfo.uptime ? this.formatUptime(systemInfo.uptime) : 'N/A'}</p>
</div>
`;
}
updateDiskUsage(disks) {
const diskUsageDiv = document.getElementById('diskUsage');
diskUsageDiv.innerHTML = '';
if (!disks || !Array.isArray(disks)) {
diskUsageDiv.innerHTML = '<p class="text-gray-500">No disk information available</p>';
return;
}
disks.forEach(disk => {
if (!disk || !disk.totalSize || !disk.freeSpace) return;
const usagePercentage = ((disk.totalSize - disk.freeSpace) / disk.totalSize * 100);
const diskName = disk.label ? `${disk.driveLetter} (${disk.label})` : disk.driveLetter || 'Unknown Drive';
const diskDiv = document.createElement('div');
diskDiv.className = 'bg-gray-50 p-4 rounded-lg';
diskDiv.innerHTML = `
<div class="flex justify-between items-center mb-2">
<h4 class="font-semibold text-gray-700">${diskName}</h4>
<span class="text-sm text-gray-500">${usagePercentage.toFixed(1)}% used</span>
</div>
<div class="w-full bg-gray-200 rounded-full h-2 mb-2">
<div class="bg-blue-600 h-2 rounded-full" style="width: ${usagePercentage}%"></div>
</div>
<div class="flex justify-between text-sm text-gray-600">
<span>Free: ${(disk.freeSpace / 1024 / 1024 / 1024).toFixed(1)} GB</span>
<span>Total: ${(disk.totalSize / 1024 / 1024 / 1024).toFixed(1)} GB</span>
</div>
`;
diskUsageDiv.appendChild(diskDiv);
});
}
updateGameInfo(gameInfo) {
console.log('updateGameInfo called with:', gameInfo); // Debug log
const gameStatusSpan = document.getElementById('gameStatus');
const gameInfoDiv = document.getElementById('gameInfo');
if (!gameInfo) {
console.log('No game detected, showing default state'); // Debug log
gameStatusSpan.textContent = 'No game detected';
gameInfoDiv.innerHTML = `
<div class="text-center py-8">
<div class="text-gray-400 mb-4">
<i class="fas fa-gamepad text-4xl"></i>
</div>
<p class="text-gray-500">No game currently running</p>
</div>
`;
return;
}
console.log('Game detected:', gameInfo.gameName); // Debug log
gameStatusSpan.textContent = 'Game detected';
gameStatusSpan.className = 'text-sm text-green-600 font-semibold';
gameInfoDiv.innerHTML = `
<div class="bg-gradient-to-r from-purple-50 to-blue-50 p-6 rounded-lg">
<div class="flex items-center justify-between mb-4">
<div class="flex items-center">
<div class="bg-green-100 p-3 rounded-full mr-4">
<i class="fas fa-gamepad text-green-600 text-2xl"></i>
</div>
<div>
<h3 class="text-xl font-bold text-gray-800">${gameInfo.gameName || 'Unknown Game'}</h3>
<p class="text-gray-600">Running since ${this.formatGameStartTime(gameInfo.startTime)}</p>
</div>
</div>
<div class="flex items-center space-x-3">
<div class="bg-green-500 text-white px-3 py-1 rounded-full text-sm font-medium">
<i class="fas fa-circle text-xs mr-1"></i>ACTIVE
</div>
<button onclick="dashboard.killProcess(${gameInfo.processId}, '${gameInfo.gameName || 'Unknown Game'}')"
class="bg-red-500 hover:bg-red-700 text-white px-3 py-2 rounded-lg text-sm font-medium transition-colors">
<i class="fas fa-times mr-1"></i>End Game
</button>
</div>
</div>
<div class="grid grid-cols-1 md:grid-cols-3 gap-4">
<div class="bg-white p-4 rounded-lg">
<h4 class="font-semibold text-gray-700 mb-2">Process ID</h4>
<p class="text-gray-600">${gameInfo.processId || 'N/A'}</p>
</div>
<div class="bg-white p-4 rounded-lg">
<h4 class="font-semibold text-gray-700 mb-2">Memory Usage</h4>
<p class="text-gray-600">${gameInfo.memoryUsage ? (gameInfo.memoryUsage / 1024 / 1024).toFixed(1) + ' MB' : 'N/A'}</p>
</div>
<div class="bg-white p-4 rounded-lg">
<h4 class="font-semibold text-gray-700 mb-2">CPU Usage</h4>
<p class="text-gray-600">${gameInfo.cpuUsage ? gameInfo.cpuUsage.toFixed(1) + '%' : 'N/A'}</p>
</div>
</div>
</div>
`;
}
formatGameStartTime(startTime) {
if (!startTime) return 'Unknown';
try {
const date = new Date(startTime);
const now = new Date();
const diffMs = now - date;
const diffMins = Math.floor(diffMs / 60000);
const diffHours = Math.floor(diffMins / 60);
if (diffHours > 0) {
return `${diffHours}h ${diffMins % 60}m ago`;
} else {
return `${diffMins}m ago`;
}
} catch (error) {
return 'Unknown';
}
}
initializeCharts() {
// CPU Chart
const cpuCtx = document.getElementById('cpuChart').getContext('2d');
this.cpuChart = new Chart(cpuCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'CPU Usage %',
data: [],
borderColor: 'rgb(59, 130, 246)',
backgroundColor: 'rgba(59, 130, 246, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
// Memory Chart
const memoryCtx = document.getElementById('memoryChart').getContext('2d');
this.memoryChart = new Chart(memoryCtx, {
type: 'line',
data: {
labels: [],
datasets: [{
label: 'Memory Usage %',
data: [],
borderColor: 'rgb(34, 197, 94)',
backgroundColor: 'rgba(34, 197, 94, 0.1)',
tension: 0.4
}]
},
options: {
responsive: true,
scales: {
y: {
beginAtZero: true,
max: 100
}
}
}
});
}
updateCpuChart(cpuUsage) {
this.cpuHistory.push(cpuUsage);
if (this.cpuHistory.length > this.maxHistoryPoints) {
this.cpuHistory.shift();
}
this.cpuChart.data.labels = this.cpuHistory.map((_, index) => '');
this.cpuChart.data.datasets[0].data = this.cpuHistory;
this.cpuChart.update('none');
}
updateMemoryChart(memoryUsage) {
this.memoryHistory.push(memoryUsage);
if (this.memoryHistory.length > this.maxHistoryPoints) {
this.memoryHistory.shift();
}
this.memoryChart.data.labels = this.memoryHistory.map((_, index) => '');
this.memoryChart.data.datasets[0].data = this.memoryHistory;
this.memoryChart.update('none');
}
async refreshData() {
try {
console.log('Manual refresh triggered');
const resourceUsage = await this.fetchData('/api/resource/usage');
this.updateDashboard(resourceUsage);
// Show brief success feedback
this.showNotification('Data refreshed successfully', 'success');
} catch (error) {
console.error('Error refreshing data:', error);
this.showNotification('Error refreshing data', 'error');
}
}
// Legacy method - now handled by startAutoRefresh()
startDataRefresh() {
console.log("Using new auto-refresh system instead");
this.startAutoRefresh();
}
formatUptime(uptime) {
// Parse TimeSpan string format "d.hh:mm:ss.fffffff"
if (typeof uptime === 'string') {
const parts = uptime.split('.');
const days = parseInt(parts[0]) || 0;
if (parts[1]) {
const timeParts = parts[1].split(':');
const hours = parseInt(timeParts[0]) || 0;
const minutes = parseInt(timeParts[1]) || 0;
return `${days}d ${hours}h ${minutes}m`;
}
return `${days}d 0h 0m`;
}
// Fallback for object format
const days = Math.floor(uptime.totalDays || 0);
const hours = Math.floor(uptime.hours || 0);
const minutes = Math.floor(uptime.minutes || 0);
return `${days}d ${hours}h ${minutes}m`;
}
showNotification(message, type = 'info') {
const notification = document.createElement('div');
notification.className = `fixed top-4 right-4 z-50 p-4 rounded-lg shadow-lg ${
type === 'success' ? 'bg-green-500' :
type === 'error' ? 'bg-red-500' : 'bg-blue-500'
} text-white`;
notification.textContent = message;
document.body.appendChild(notification);
setTimeout(() => {
notification.remove();
}, 3000);
}
hideLoading() {
document.getElementById('loadingOverlay').style.display = 'none';
}
}
// Initialize dashboard when page loads
let dashboard;
document.addEventListener('DOMContentLoaded', () => {
dashboard = new ResourceDashboard();
});