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:
@@ -0,0 +1,93 @@
|
||||
/* Custom styles for Resource Monitor Dashboard */
|
||||
|
||||
.loading-spinner {
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.fade-in {
|
||||
animation: fadeIn 0.3s ease-in;
|
||||
}
|
||||
|
||||
@keyframes fadeIn {
|
||||
from { opacity: 0; }
|
||||
to { opacity: 1; }
|
||||
}
|
||||
|
||||
.slide-down {
|
||||
animation: slideDown 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideDown {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateY(-10px);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateY(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom progress bar animations */
|
||||
.progress-bar {
|
||||
transition: width 0.5s ease-in-out;
|
||||
}
|
||||
|
||||
/* Responsive table */
|
||||
@media (max-width: 768px) {
|
||||
.mobile-table {
|
||||
font-size: 0.875rem;
|
||||
}
|
||||
|
||||
.mobile-table th,
|
||||
.mobile-table td {
|
||||
padding: 0.5rem 0.25rem;
|
||||
}
|
||||
}
|
||||
|
||||
/* Custom notification styles */
|
||||
.notification {
|
||||
position: fixed;
|
||||
top: 1rem;
|
||||
right: 1rem;
|
||||
z-index: 1000;
|
||||
max-width: 400px;
|
||||
animation: slideIn 0.3s ease-out;
|
||||
}
|
||||
|
||||
@keyframes slideIn {
|
||||
from {
|
||||
opacity: 0;
|
||||
transform: translateX(100%);
|
||||
}
|
||||
to {
|
||||
opacity: 1;
|
||||
transform: translateX(0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Process table hover effects */
|
||||
.process-row:hover {
|
||||
background-color: #f9fafb;
|
||||
transform: translateY(-1px);
|
||||
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
|
||||
transition: all 0.2s ease;
|
||||
}
|
||||
|
||||
/* Chart container responsive */
|
||||
.chart-container {
|
||||
position: relative;
|
||||
height: 200px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
@media (max-width: 640px) {
|
||||
.chart-container {
|
||||
height: 150px;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
<!-- This is a placeholder for a favicon. In a real deployment, you would place an actual favicon.ico file here -->
|
||||
<!-- For now, we'll use a Font Awesome icon as a favicon alternative -->
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 100 100%22><text y=%22.9em%22 font-size=%2290%22>📊</text></svg>">
|
||||
@@ -0,0 +1,205 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Resource Monitor Dashboard</title>
|
||||
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>📊</text></svg>">
|
||||
<script src="https://cdn.tailwindcss.com"></script>
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/microsoft-signalr/6.0.1/signalr.min.js"></script>
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.0.0/css/all.min.css">
|
||||
<link rel="stylesheet" href="css/dashboard.css">
|
||||
</head>
|
||||
<body class="bg-gray-100">
|
||||
<!-- Navigation -->
|
||||
<nav class="bg-blue-600 text-white shadow-lg">
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
|
||||
<div class="flex justify-between items-center h-16">
|
||||
<div class="flex items-center">
|
||||
<i class="fas fa-chart-line text-2xl mr-3"></i>
|
||||
<h1 class="text-xl font-bold">Resource Monitor</h1>
|
||||
</div>
|
||||
<div class="flex space-x-4">
|
||||
<button id="toggleAutoRefresh" class="bg-yellow-500 hover:bg-yellow-700 px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-sync mr-2"></i>Auto: ON
|
||||
</button>
|
||||
<button id="toggleProcesses" class="bg-purple-500 hover:bg-purple-700 px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-list mr-2"></i>Processes
|
||||
</button>
|
||||
<button id="toggleDetails" class="bg-blue-500 hover:bg-blue-700 px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-info-circle mr-2"></i>Details
|
||||
</button>
|
||||
<button id="refreshData" class="bg-green-500 hover:bg-green-700 px-4 py-2 rounded-lg transition-colors">
|
||||
<i class="fas fa-sync-alt mr-2"></i>Refresh
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<!-- Main Content -->
|
||||
<div class="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8 py-6">
|
||||
<!-- Dashboard Overview -->
|
||||
<div id="dashboard" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6 mb-8">
|
||||
<!-- CPU Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">CPU Usage</p>
|
||||
<p id="cpuUsage" class="text-3xl font-bold text-blue-600">0%</p>
|
||||
</div>
|
||||
<div class="bg-blue-100 p-3 rounded-full">
|
||||
<i class="fas fa-microchip text-blue-600 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div id="cpuBar" class="bg-blue-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Memory Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">Memory Usage</p>
|
||||
<p id="memoryUsage" class="text-3xl font-bold text-green-600">0%</p>
|
||||
</div>
|
||||
<div class="bg-green-100 p-3 rounded-full">
|
||||
<i class="fas fa-memory text-green-600 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div id="memoryBar" class="bg-green-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- GPU Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">GPU Usage</p>
|
||||
<p id="gpuUsage" class="text-3xl font-bold text-purple-600">0%</p>
|
||||
</div>
|
||||
<div class="bg-purple-100 p-3 rounded-full">
|
||||
<i class="fas fa-display text-purple-600 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div id="gpuBar" class="bg-purple-600 h-2 rounded-full transition-all duration-300" style="width: 0%"></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Network Card -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm font-medium text-gray-500">Network</p>
|
||||
<p id="networkSpeed" class="text-3xl font-bold text-orange-600">0 MB/s</p>
|
||||
</div>
|
||||
<div class="bg-orange-100 p-3 rounded-full">
|
||||
<i class="fas fa-network-wired text-orange-600 text-xl"></i>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4">
|
||||
<p id="networkDetail" class="text-sm text-gray-500">↑ 0 MB/s ↓ 0 MB/s</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Game Detection Section -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold text-gray-800">
|
||||
<i class="fas fa-gamepad mr-2"></i>Game Detection
|
||||
</h2>
|
||||
<span id="gameStatus" class="text-sm text-gray-500">No game detected</span>
|
||||
</div>
|
||||
<div id="gameInfo" 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>
|
||||
</div>
|
||||
|
||||
<!-- Top Processes Section (Hidden by default) -->
|
||||
<div id="processesSection" class="hidden">
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
|
||||
<div class="flex justify-between items-center mb-4">
|
||||
<h2 class="text-xl font-bold text-gray-800">
|
||||
<i class="fas fa-list mr-2"></i>Top Processes
|
||||
</h2>
|
||||
<span class="text-sm text-gray-500">Click process name to terminate</span>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full table-auto">
|
||||
<thead>
|
||||
<tr class="bg-gray-50">
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Process</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">CPU %</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Memory</th>
|
||||
<th class="px-4 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="processTable" class="bg-white divide-y divide-gray-200">
|
||||
<!-- Process rows will be populated here -->
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Detailed Information (Hidden by default) -->
|
||||
<div id="detailsSection" class="hidden">
|
||||
<!-- System Information -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-4">
|
||||
<i class="fas fa-server mr-2"></i>System Information
|
||||
</h2>
|
||||
<div id="systemInfo" class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
||||
<!-- System info will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Disk Usage -->
|
||||
<div class="bg-white rounded-lg shadow-lg p-6 mb-8">
|
||||
<h2 class="text-xl font-bold text-gray-800 mb-4">
|
||||
<i class="fas fa-hard-drive mr-2"></i>Disk Usage
|
||||
</h2>
|
||||
<div id="diskUsage" class="space-y-4">
|
||||
<!-- Disk usage will be populated here -->
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Performance Charts -->
|
||||
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
|
||||
<div class="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-4">CPU History</h3>
|
||||
<canvas id="cpuChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg shadow-lg p-6">
|
||||
<h3 class="text-lg font-bold text-gray-800 mb-4">Memory History</h3>
|
||||
<canvas id="memoryChart" width="400" height="200"></canvas>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Overlay -->
|
||||
<div id="loadingOverlay" class="fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 flex items-center space-x-3">
|
||||
<div class="animate-spin rounded-full h-8 w-8 border-b-2 border-blue-600"></div>
|
||||
<span class="text-gray-700">Loading...</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -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();
|
||||
});
|
||||
Reference in New Issue
Block a user