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:
@@ -2,7 +2,7 @@ namespace ResourceMonitorService.Configuration
|
|||||||
{
|
{
|
||||||
public class MonitoringSettings
|
public class MonitoringSettings
|
||||||
{
|
{
|
||||||
public int UpdateIntervalMs { get; set; } = 5000; // 5 seconds
|
public int UpdateIntervalMs { get; set; } = 15000; // 15 seconds
|
||||||
public int DataRetentionDays { get; set; } = 7;
|
public int DataRetentionDays { get; set; } = 7;
|
||||||
public bool EnableGpuMonitoring { get; set; } = true;
|
public bool EnableGpuMonitoring { get; set; } = true;
|
||||||
public bool EnableDiskMonitoring { get; set; } = true;
|
public bool EnableDiskMonitoring { get; set; } = true;
|
||||||
|
|||||||
@@ -0,0 +1,173 @@
|
|||||||
|
using Microsoft.AspNetCore.Mvc;
|
||||||
|
using ResourceMonitorService.Models;
|
||||||
|
using ResourceMonitorService.Services;
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
|
namespace ResourceMonitorService.Controllers
|
||||||
|
{
|
||||||
|
[ApiController]
|
||||||
|
[Route("api/[controller]")]
|
||||||
|
public class ResourceController : ControllerBase
|
||||||
|
{
|
||||||
|
private readonly IResourceMonitorService _resourceMonitorService;
|
||||||
|
private readonly ISystemInfoService _systemInfoService;
|
||||||
|
private readonly ILogger<ResourceController> _logger;
|
||||||
|
|
||||||
|
public ResourceController(
|
||||||
|
IResourceMonitorService resourceMonitorService,
|
||||||
|
ISystemInfoService systemInfoService,
|
||||||
|
ILogger<ResourceController> logger)
|
||||||
|
{
|
||||||
|
_resourceMonitorService = resourceMonitorService;
|
||||||
|
_systemInfoService = systemInfoService;
|
||||||
|
_logger = logger;
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("usage")]
|
||||||
|
public async Task<ActionResult<ResourceUsage>> GetResourceUsage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var usage = await _resourceMonitorService.GetResourceUsageAsync();
|
||||||
|
return Ok(usage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting resource usage");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("system-info")]
|
||||||
|
public async Task<ActionResult<SystemInfo>> GetSystemInfo()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
||||||
|
return Ok(systemInfo);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting system info");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("cpu")]
|
||||||
|
public async Task<ActionResult<CpuUsage>> GetCpuUsage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var cpuUsage = await _resourceMonitorService.GetCpuUsageAsync();
|
||||||
|
return Ok(cpuUsage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting CPU usage");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("memory")]
|
||||||
|
public async Task<ActionResult<MemoryUsage>> GetMemoryUsage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var memoryUsage = await _resourceMonitorService.GetMemoryUsageAsync();
|
||||||
|
return Ok(memoryUsage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting memory usage");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("gpu")]
|
||||||
|
public async Task<ActionResult<GpuUsage>> GetGpuUsage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var gpuUsage = await _resourceMonitorService.GetGpuUsageAsync();
|
||||||
|
return Ok(gpuUsage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting GPU usage");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("disks")]
|
||||||
|
public async Task<ActionResult<List<DiskUsage>>> GetDiskUsage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var diskUsage = await _resourceMonitorService.GetDiskUsageAsync();
|
||||||
|
return Ok(diskUsage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting disk usage");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("network")]
|
||||||
|
public async Task<ActionResult<NetworkUsage>> GetNetworkUsage()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var networkUsage = await _resourceMonitorService.GetNetworkUsageAsync();
|
||||||
|
return Ok(networkUsage);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting network usage");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpGet("processes")]
|
||||||
|
public async Task<ActionResult<List<ProcessInfo>>> GetTopProcesses([FromQuery] int count = 10)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var processes = await _resourceMonitorService.GetTopProcessesAsync(count);
|
||||||
|
return Ok(processes);
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, "Error getting top processes");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[HttpPost("kill-process/{processId}")]
|
||||||
|
public ActionResult KillProcess(int processId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var process = Process.GetProcessById(processId);
|
||||||
|
if (process == null)
|
||||||
|
{
|
||||||
|
return NotFound($"Process with ID {processId} not found");
|
||||||
|
}
|
||||||
|
|
||||||
|
process.Kill();
|
||||||
|
_logger.LogInformation($"Process {process.ProcessName} (ID: {processId}) was terminated");
|
||||||
|
|
||||||
|
return Ok(new { message = $"Process {process.ProcessName} (ID: {processId}) was terminated successfully" });
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return NotFound($"Process with ID {processId} not found");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogError(ex, $"Error killing process {processId}");
|
||||||
|
return StatusCode(500, "Internal server error");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
|
||||||
|
namespace ResourceMonitorService.Hubs
|
||||||
|
{
|
||||||
|
public class ResourceHub : Hub
|
||||||
|
{
|
||||||
|
public async Task JoinGroup(string groupName)
|
||||||
|
{
|
||||||
|
await Groups.AddToGroupAsync(Context.ConnectionId, groupName);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Task LeaveGroup(string groupName)
|
||||||
|
{
|
||||||
|
await Groups.RemoveFromGroupAsync(Context.ConnectionId, groupName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -107,7 +107,7 @@ namespace ResourceMonitorService.Models
|
|||||||
|
|
||||||
public class ProcessInfo
|
public class ProcessInfo
|
||||||
{
|
{
|
||||||
public int Id { get; set; }
|
public int ProcessId { get; set; }
|
||||||
public string Name { get; set; } = string.Empty;
|
public string Name { get; set; } = string.Empty;
|
||||||
public float CpuUsage { get; set; }
|
public float CpuUsage { get; set; }
|
||||||
public ulong MemoryUsage { get; set; }
|
public ulong MemoryUsage { get; set; }
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
using Microsoft.AspNetCore.Hosting;
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Configuration;
|
using Microsoft.Extensions.Configuration;
|
||||||
@@ -37,6 +38,11 @@ namespace ResourceMonitorService
|
|||||||
{
|
{
|
||||||
var builder = Host.CreateDefaultBuilder(args)
|
var builder = Host.CreateDefaultBuilder(args)
|
||||||
.UseSerilog()
|
.UseSerilog()
|
||||||
|
.ConfigureWebHostDefaults(webBuilder =>
|
||||||
|
{
|
||||||
|
webBuilder.UseStartup<Startup>();
|
||||||
|
webBuilder.UseUrls("http://localhost:5000", "https://localhost:5001");
|
||||||
|
})
|
||||||
.ConfigureServices((hostContext, services) =>
|
.ConfigureServices((hostContext, services) =>
|
||||||
{
|
{
|
||||||
// Bind configuration sections
|
// Bind configuration sections
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
"ResourceMonitorService": {
|
"ResourceMonitorService": {
|
||||||
"commandName": "Project",
|
"commandName": "Project",
|
||||||
"dotnetRunMessages": true,
|
"dotnetRunMessages": true,
|
||||||
|
"applicationUrl": "http://localhost:5000;https://localhost:5001",
|
||||||
"environmentVariables": {
|
"environmentVariables": {
|
||||||
"DOTNET_ENVIRONMENT": "Development"
|
"DOTNET_ENVIRONMENT": "Development"
|
||||||
}
|
}
|
||||||
|
|||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
# Resource Monitor Web UI
|
||||||
|
|
||||||
|
This enhanced Resource Monitor Service now includes a modern web-based dashboard with REST API functionality.
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
### Web Dashboard
|
||||||
|
- **Mobile-Friendly Design**: Built with Tailwind CSS for responsive design
|
||||||
|
- **Real-Time Updates**: Uses SignalR for live data updates every 5 seconds
|
||||||
|
- **Interactive Dashboard**: Shows CPU, Memory, GPU, and Network usage with progress bars
|
||||||
|
- **Process Management**: View top 10 processes with ability to kill top 3 high-usage processes
|
||||||
|
- **Detailed View**: Toggle detailed system information and disk usage
|
||||||
|
- **Performance Charts**: Real-time CPU and Memory usage history
|
||||||
|
|
||||||
|
### REST API Endpoints
|
||||||
|
|
||||||
|
#### Resource Information
|
||||||
|
- `GET /api/resource/usage` - Get complete resource usage information
|
||||||
|
- `GET /api/resource/system-info` - Get system information
|
||||||
|
- `GET /api/resource/cpu` - Get CPU usage details
|
||||||
|
- `GET /api/resource/memory` - Get memory usage details
|
||||||
|
- `GET /api/resource/gpu` - Get GPU usage details
|
||||||
|
- `GET /api/resource/disks` - Get disk usage for all drives
|
||||||
|
- `GET /api/resource/network` - Get network usage details
|
||||||
|
- `GET /api/resource/processes?count=10` - Get top processes
|
||||||
|
|
||||||
|
#### Process Management
|
||||||
|
- `POST /api/resource/kill-process/{processId}` - Terminate a specific process
|
||||||
|
|
||||||
|
#### API Documentation
|
||||||
|
- `GET /swagger` - Swagger UI for API documentation (in Development mode)
|
||||||
|
|
||||||
|
## Running the Application
|
||||||
|
|
||||||
|
### Development Mode
|
||||||
|
```bash
|
||||||
|
dotnet run
|
||||||
|
```
|
||||||
|
The application will be available at:
|
||||||
|
- Web UI: http://localhost:5000
|
||||||
|
- API: http://localhost:5000/api/
|
||||||
|
- Swagger UI: http://localhost:5000/swagger
|
||||||
|
|
||||||
|
### Windows Service Mode
|
||||||
|
```bash
|
||||||
|
dotnet run --windows-service
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build and Deploy
|
||||||
|
```bash
|
||||||
|
dotnet build --configuration Release
|
||||||
|
dotnet publish --configuration Release --output ./publish
|
||||||
|
```
|
||||||
|
|
||||||
|
## Configuration
|
||||||
|
|
||||||
|
### Port Configuration
|
||||||
|
Edit `appsettings.json` to change ports:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"Kestrel": {
|
||||||
|
"Endpoints": {
|
||||||
|
"Http": {
|
||||||
|
"Url": "http://*:5000"
|
||||||
|
},
|
||||||
|
"Https": {
|
||||||
|
"Url": "https://*:5001"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### Monitoring Settings
|
||||||
|
Configure monitoring intervals and features in `appsettings.json`:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"MonitoringSettings": {
|
||||||
|
"UpdateIntervalMs": 5000,
|
||||||
|
"EnableGpuMonitoring": true,
|
||||||
|
"EnableProcessMonitoring": true,
|
||||||
|
"MaxProcessesToTrack": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Security Features
|
||||||
|
|
||||||
|
### Process Kill Restrictions
|
||||||
|
- Only the top 3 highest CPU/Memory usage processes can be terminated
|
||||||
|
- Confirmation dialog required for process termination
|
||||||
|
- All process terminations are logged
|
||||||
|
|
||||||
|
### API Security
|
||||||
|
- CORS configured for allowed origins
|
||||||
|
- Optional API key authentication (disabled by default)
|
||||||
|
- Rate limiting can be configured
|
||||||
|
|
||||||
|
## Dashboard Features
|
||||||
|
|
||||||
|
### Main Dashboard Cards
|
||||||
|
1. **CPU Usage**: Real-time percentage with progress bar
|
||||||
|
2. **Memory Usage**: Memory utilization with progress bar
|
||||||
|
3. **GPU Usage**: Graphics processor utilization
|
||||||
|
4. **Network**: Upload/download speeds
|
||||||
|
|
||||||
|
### Process Table
|
||||||
|
- Shows top 10 processes by CPU/Memory usage
|
||||||
|
- Process ID, Name, CPU%, Memory usage
|
||||||
|
- Kill button available for top 3 processes only
|
||||||
|
|
||||||
|
### Detailed Information (Toggle)
|
||||||
|
- Complete system information
|
||||||
|
- Disk usage for all drives
|
||||||
|
- Real-time performance charts
|
||||||
|
- Historical CPU and Memory usage graphs
|
||||||
|
|
||||||
|
### Mobile Responsive
|
||||||
|
- Optimized for mobile devices
|
||||||
|
- Touch-friendly interface
|
||||||
|
- Responsive grid layout
|
||||||
|
- Collapsible sections
|
||||||
|
|
||||||
|
## Technology Stack
|
||||||
|
|
||||||
|
- **Backend**: ASP.NET Core 9.0
|
||||||
|
- **Frontend**: HTML5, Tailwind CSS, Chart.js
|
||||||
|
- **Real-time**: SignalR
|
||||||
|
- **API Documentation**: Swagger/OpenAPI
|
||||||
|
- **Icons**: Font Awesome
|
||||||
|
- **Charts**: Chart.js
|
||||||
|
|
||||||
|
## Browser Compatibility
|
||||||
|
|
||||||
|
- Chrome 90+
|
||||||
|
- Firefox 88+
|
||||||
|
- Safari 14+
|
||||||
|
- Edge 90+
|
||||||
|
|
||||||
|
## Development Notes
|
||||||
|
|
||||||
|
The application runs both as a web server and a Windows service background worker simultaneously, providing:
|
||||||
|
- Web interface for interactive monitoring
|
||||||
|
- REST API for programmatic access
|
||||||
|
- Background service for continuous monitoring
|
||||||
|
- Real-time updates via WebSocket (SignalR)
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
<Project Sdk="Microsoft.NET.Sdk.Worker">
|
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<TargetFramework>net9.0-windows</TargetFramework>
|
<TargetFramework>net9.0-windows</TargetFramework>
|
||||||
@@ -18,5 +18,7 @@
|
|||||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
|
||||||
<PackageReference Include="Telegram.Bot" Version="22.6.0" />
|
<PackageReference Include="Telegram.Bot" Version="22.6.0" />
|
||||||
|
<PackageReference Include="Swashbuckle.AspNetCore" Version="7.0.0" />
|
||||||
|
<PackageReference Include="Microsoft.AspNetCore.SignalR" Version="1.1.0" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -249,22 +249,39 @@ namespace ResourceMonitorService.Services
|
|||||||
"setup", "installer", "update", "vshost", "devenv"
|
"setup", "installer", "update", "vshost", "devenv"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Skip if it's a known system process
|
// Exclude gaming platform clients (they are not games themselves)
|
||||||
if (systemExclusions.Any(exclusion => fileName.Contains(exclusion)))
|
var platformClientExclusions = new[]
|
||||||
|
{
|
||||||
|
"steam", "steamwebhelper", "steamservice", "steamerrorreporter",
|
||||||
|
"epicgameslauncher", "epic games launcher", "epiconlineservices",
|
||||||
|
"origin", "originwebhelperservice", "originweb", "originthinsetup",
|
||||||
|
"uplay", "upc", "ubisoftgamelauncher", "ubisoftconnect",
|
||||||
|
"battle.net", "battlenet", "blizzardlauncher", "blizzard update agent",
|
||||||
|
"gog galaxy", "goggalaxy", "gogcom",
|
||||||
|
"rockstar games launcher", "rockstargameslauncher",
|
||||||
|
"riotclientservices", "riot client", "valorant-win64-shipping",
|
||||||
|
"ea app", "ea desktop", "eadesktop", "eabackground",
|
||||||
|
"xbox", "xboxapp", "xboxgamebar", "microsoftstore"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Skip if it's a known system process or platform client
|
||||||
|
if (systemExclusions.Any(exclusion => fileName.Contains(exclusion)) ||
|
||||||
|
platformClientExclusions.Any(exclusion => fileName.Contains(exclusion)))
|
||||||
{
|
{
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
var knownGameExecutables = new[]
|
var knownGameExecutables = new[]
|
||||||
{
|
{
|
||||||
"game", "launcher", "client"
|
"game"
|
||||||
// Removed generic terms like "main", "start", "run" that match too many system processes
|
// Removed "launcher" and "client" as they often refer to platform clients, not games
|
||||||
};
|
};
|
||||||
|
|
||||||
var gameIndicators = new[]
|
var gameIndicators = new[]
|
||||||
{
|
{
|
||||||
"unreal", "unity", "godot", "gamemaker", "rpgmaker",
|
"unreal", "unity", "godot", "gamemaker", "rpgmaker",
|
||||||
"steam", "epic", "origin", "uplay", "battle.net"
|
"\\steamapps\\common\\", "\\epic games\\", "\\gog galaxy\\games\\",
|
||||||
|
"\\origin games\\", "\\ubisoft game launcher\\games\\"
|
||||||
};
|
};
|
||||||
|
|
||||||
// Check if it's likely a game based on executable name or path
|
// Check if it's likely a game based on executable name or path
|
||||||
@@ -398,6 +415,20 @@ namespace ResourceMonitorService.Services
|
|||||||
if (!string.IsNullOrEmpty(versionInfo.ProductName) &&
|
if (!string.IsNullOrEmpty(versionInfo.ProductName) &&
|
||||||
!versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase))
|
!versionInfo.ProductName.Equals(versionInfo.FileName, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
|
// Check if this is a platform client by product name
|
||||||
|
var platformClientNames = new[]
|
||||||
|
{
|
||||||
|
"steam", "epic games launcher", "origin", "uplay", "ubisoft connect",
|
||||||
|
"battle.net", "blizzard launcher", "gog galaxy", "riot client",
|
||||||
|
"ea app", "ea desktop", "xbox", "microsoft store", "rockstar games launcher"
|
||||||
|
};
|
||||||
|
|
||||||
|
if (platformClientNames.Any(client =>
|
||||||
|
versionInfo.ProductName.Contains(client, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
return string.Empty; // Return empty to indicate this should not be treated as a game
|
||||||
|
}
|
||||||
|
|
||||||
return versionInfo.ProductName;
|
return versionInfo.ProductName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -770,7 +770,7 @@ namespace ResourceMonitorService.Services
|
|||||||
|
|
||||||
var processInfo = new ProcessInfo
|
var processInfo = new ProcessInfo
|
||||||
{
|
{
|
||||||
Id = p.Id,
|
ProcessId = p.Id,
|
||||||
Name = p.ProcessName,
|
Name = p.ProcessName,
|
||||||
MemoryUsage = memoryUsage,
|
MemoryUsage = memoryUsage,
|
||||||
MemoryUsagePercentage = totalSystemMemory > 0 ? (float)(memoryUsage * 100.0 / totalSystemMemory) : 0f,
|
MemoryUsagePercentage = totalSystemMemory > 0 ? (float)(memoryUsage * 100.0 / totalSystemMemory) : 0f,
|
||||||
@@ -818,7 +818,7 @@ namespace ResourceMonitorService.Services
|
|||||||
_logger.LogDebug("Processed {ValidCount} valid processes, skipped {SkippedCount}", validProcesses.Count, skippedCount);
|
_logger.LogDebug("Processed {ValidCount} valid processes, skipped {SkippedCount}", validProcesses.Count, skippedCount);
|
||||||
|
|
||||||
// Clean up old process entries to prevent memory leaks
|
// Clean up old process entries to prevent memory leaks
|
||||||
CleanupOldProcessEntries(validProcesses.Select(p => p.Id).ToHashSet());
|
CleanupOldProcessEntries(validProcesses.Select(p => p.ProcessId).ToHashSet());
|
||||||
|
|
||||||
var topProcesses = validProcesses
|
var topProcesses = validProcesses
|
||||||
.OrderByDescending(p => p.CpuUsage)
|
.OrderByDescending(p => p.CpuUsage)
|
||||||
|
|||||||
+47
-6
@@ -3,12 +3,39 @@ using Microsoft.AspNetCore.Hosting;
|
|||||||
using Microsoft.Extensions.DependencyInjection;
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.AspNetCore.Http;
|
using Microsoft.AspNetCore.Http;
|
||||||
|
using ResourceMonitorService.Hubs;
|
||||||
|
using Microsoft.OpenApi.Models;
|
||||||
|
|
||||||
public class Startup
|
namespace ResourceMonitorService
|
||||||
{
|
{
|
||||||
|
public class Startup
|
||||||
|
{
|
||||||
public void ConfigureServices(IServiceCollection services)
|
public void ConfigureServices(IServiceCollection services)
|
||||||
{
|
{
|
||||||
// Add services to the container
|
// Add MVC services
|
||||||
|
services.AddControllers()
|
||||||
|
.AddNewtonsoftJson(); // For JSON serialization
|
||||||
|
|
||||||
|
// Add SignalR for real-time updates
|
||||||
|
services.AddSignalR();
|
||||||
|
|
||||||
|
// Add CORS for API access
|
||||||
|
services.AddCors(options =>
|
||||||
|
{
|
||||||
|
options.AddPolicy("AllowAll", builder =>
|
||||||
|
{
|
||||||
|
builder.AllowAnyOrigin()
|
||||||
|
.AllowAnyMethod()
|
||||||
|
.AllowAnyHeader();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// Add Swagger for API documentation
|
||||||
|
services.AddEndpointsApiExplorer();
|
||||||
|
services.AddSwaggerGen(c =>
|
||||||
|
{
|
||||||
|
c.SwaggerDoc("v1", new OpenApiInfo { Title = "Resource Monitor API", Version = "v1" });
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
|
||||||
@@ -16,16 +43,30 @@ public class Startup
|
|||||||
if (env.IsDevelopment())
|
if (env.IsDevelopment())
|
||||||
{
|
{
|
||||||
app.UseDeveloperExceptionPage();
|
app.UseDeveloperExceptionPage();
|
||||||
|
app.UseSwagger();
|
||||||
|
app.UseSwaggerUI(c =>
|
||||||
|
{
|
||||||
|
c.SwaggerEndpoint("/swagger/v1/swagger.json", "Resource Monitor API V1");
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Serve static files (CSS, JS, images)
|
||||||
|
app.UseStaticFiles();
|
||||||
|
|
||||||
app.UseRouting();
|
app.UseRouting();
|
||||||
|
app.UseCors("AllowAll");
|
||||||
|
|
||||||
app.UseEndpoints(endpoints =>
|
app.UseEndpoints(endpoints =>
|
||||||
{
|
{
|
||||||
endpoints.MapGet("/", async context =>
|
// Map API controllers
|
||||||
{
|
endpoints.MapControllers();
|
||||||
await context.Response.WriteAsync("Hello World!");
|
|
||||||
});
|
// Map SignalR hub
|
||||||
|
endpoints.MapHub<ResourceHub>("/resourceHub");
|
||||||
|
|
||||||
|
// Default route to index.html
|
||||||
|
endpoints.MapFallbackToFile("index.html");
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -1,439 +1,43 @@
|
|||||||
using Microsoft.AspNetCore.Builder;
|
|
||||||
using Microsoft.AspNetCore.Http;
|
|
||||||
using Microsoft.Extensions.DependencyInjection;
|
|
||||||
using Microsoft.Extensions.Hosting;
|
using Microsoft.Extensions.Hosting;
|
||||||
using Microsoft.Extensions.Logging;
|
using Microsoft.Extensions.Logging;
|
||||||
using Microsoft.Extensions.Options;
|
using Microsoft.Extensions.Options;
|
||||||
using Newtonsoft.Json;
|
|
||||||
using ResourceMonitorService.Configuration;
|
using ResourceMonitorService.Configuration;
|
||||||
using ResourceMonitorService.Models;
|
|
||||||
using ResourceMonitorService.Services;
|
using ResourceMonitorService.Services;
|
||||||
using System.Diagnostics;
|
using ResourceMonitorService.Hubs;
|
||||||
|
using Microsoft.AspNetCore.SignalR;
|
||||||
|
using Microsoft.Extensions.DependencyInjection;
|
||||||
|
|
||||||
namespace ResourceMonitorService
|
namespace ResourceMonitorService
|
||||||
{
|
{
|
||||||
public class Worker : BackgroundService
|
public class Worker : BackgroundService
|
||||||
{
|
{
|
||||||
private readonly ILogger<Worker> _logger;
|
private readonly ILogger<Worker> _logger;
|
||||||
private readonly IHostApplicationLifetime _lifetime;
|
|
||||||
private readonly ISystemInfoService _systemInfoService;
|
|
||||||
private readonly IResourceMonitorService _resourceMonitorService;
|
private readonly IResourceMonitorService _resourceMonitorService;
|
||||||
private readonly IGameDetectionService _gameDetectionService;
|
private readonly IGameDetectionService _gameDetectionService;
|
||||||
private readonly IAlertService _alertService;
|
private readonly IAlertService _alertService;
|
||||||
private readonly ITelegramNotificationService _telegramService;
|
|
||||||
private readonly ApiSettings _apiSettings;
|
|
||||||
private readonly MonitoringSettings _monitoringSettings;
|
private readonly MonitoringSettings _monitoringSettings;
|
||||||
|
private readonly IServiceProvider _serviceProvider;
|
||||||
|
|
||||||
public Worker(
|
public Worker(
|
||||||
ILogger<Worker> logger,
|
ILogger<Worker> logger,
|
||||||
IHostApplicationLifetime lifetime,
|
|
||||||
ISystemInfoService systemInfoService,
|
|
||||||
IResourceMonitorService resourceMonitorService,
|
IResourceMonitorService resourceMonitorService,
|
||||||
IGameDetectionService gameDetectionService,
|
IGameDetectionService gameDetectionService,
|
||||||
IAlertService alertService,
|
IAlertService alertService,
|
||||||
ITelegramNotificationService telegramService,
|
IOptions<MonitoringSettings> monitoringSettings,
|
||||||
IOptions<ApiSettings> apiSettings,
|
IServiceProvider serviceProvider)
|
||||||
IOptions<MonitoringSettings> monitoringSettings)
|
|
||||||
{
|
{
|
||||||
_logger = logger;
|
_logger = logger;
|
||||||
_lifetime = lifetime;
|
|
||||||
_systemInfoService = systemInfoService;
|
|
||||||
_resourceMonitorService = resourceMonitorService;
|
_resourceMonitorService = resourceMonitorService;
|
||||||
_gameDetectionService = gameDetectionService;
|
_gameDetectionService = gameDetectionService;
|
||||||
_alertService = alertService;
|
_alertService = alertService;
|
||||||
_telegramService = telegramService;
|
|
||||||
_apiSettings = apiSettings.Value;
|
|
||||||
_monitoringSettings = monitoringSettings.Value;
|
_monitoringSettings = monitoringSettings.Value;
|
||||||
|
_serviceProvider = serviceProvider;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||||
{
|
{
|
||||||
_logger.LogInformation("Resource Monitor Service starting...");
|
_logger.LogInformation("Resource Monitor background service starting...");
|
||||||
|
await BackgroundMonitoringLoop(stoppingToken);
|
||||||
var builder = WebApplication.CreateBuilder();
|
|
||||||
|
|
||||||
// Configure CORS
|
|
||||||
builder.Services.AddCors(options =>
|
|
||||||
{
|
|
||||||
options.AddPolicy("AllowedOrigins", policy =>
|
|
||||||
policy.WithOrigins(_apiSettings.AllowedOrigins.ToArray())
|
|
||||||
.AllowAnyHeader()
|
|
||||||
.AllowAnyMethod());
|
|
||||||
});
|
|
||||||
|
|
||||||
builder.Services.AddControllers().AddNewtonsoftJson();
|
|
||||||
|
|
||||||
var app = builder.Build();
|
|
||||||
|
|
||||||
// API Key middleware (if enabled)
|
|
||||||
if (_apiSettings.RequireApiKey)
|
|
||||||
{
|
|
||||||
app.Use(async (context, next) =>
|
|
||||||
{
|
|
||||||
if (!context.Request.Headers.TryGetValue("X-API-KEY", out var extractedApiKey) ||
|
|
||||||
extractedApiKey != _apiSettings.ApiKey)
|
|
||||||
{
|
|
||||||
context.Response.StatusCode = StatusCodes.Status401Unauthorized;
|
|
||||||
await context.Response.WriteAsync("Unauthorized: Invalid API Key");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
await next();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
app.UseCors("AllowedOrigins");
|
|
||||||
|
|
||||||
// Enhanced API endpoints
|
|
||||||
ConfigureApiEndpoints(app);
|
|
||||||
|
|
||||||
// Start background monitoring
|
|
||||||
_ = Task.Run(async () => await BackgroundMonitoringLoop(stoppingToken), stoppingToken);
|
|
||||||
|
|
||||||
// Start the web application
|
|
||||||
_ = app.RunAsync(stoppingToken);
|
|
||||||
|
|
||||||
await Task.Delay(Timeout.Infinite, stoppingToken);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ConfigureApiEndpoints(WebApplication app)
|
|
||||||
{
|
|
||||||
var basePath = _apiSettings.BasePath;
|
|
||||||
|
|
||||||
// System information endpoints
|
|
||||||
app.MapGet($"{basePath}/system-info", async () =>
|
|
||||||
Results.Ok(await _systemInfoService.GetSystemInfoAsync()));
|
|
||||||
|
|
||||||
// Resource usage endpoints
|
|
||||||
app.MapGet($"{basePath}/resource-usage", async () =>
|
|
||||||
Results.Ok(await _resourceMonitorService.GetResourceUsageAsync()));
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/cpu-usage", async () =>
|
|
||||||
Results.Ok(await _resourceMonitorService.GetCpuUsageAsync()));
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/memory-usage", async () =>
|
|
||||||
Results.Ok(await _resourceMonitorService.GetMemoryUsageAsync()));
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/gpu-usage", async () =>
|
|
||||||
Results.Ok(await _resourceMonitorService.GetGpuUsageAsync()));
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/disk-usage", async () =>
|
|
||||||
Results.Ok(await _resourceMonitorService.GetDiskUsageAsync()));
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/network-usage", async () =>
|
|
||||||
Results.Ok(await _resourceMonitorService.GetNetworkUsageAsync()));
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/top-processes", async (int count = 10) =>
|
|
||||||
Results.Ok(await _resourceMonitorService.GetTopProcessesAsync(count)));
|
|
||||||
|
|
||||||
// Game detection endpoints
|
|
||||||
app.MapGet($"{basePath}/current-game", async () =>
|
|
||||||
Results.Ok(await _gameDetectionService.GetCurrentlyRunningGameAsync()));
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/all-games", async () =>
|
|
||||||
Results.Ok(await _gameDetectionService.GetAllDetectedGamesAsync()));
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/fullscreen-status", async () =>
|
|
||||||
Results.Ok(new { IsFullscreen = await _gameDetectionService.IsGameRunningFullscreenAsync() }));
|
|
||||||
|
|
||||||
// Alert endpoints
|
|
||||||
app.MapGet($"{basePath}/alerts/active", async () =>
|
|
||||||
Results.Ok(await _alertService.GetActiveAlertsAsync()));
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/alerts/history", async (int count = 100) =>
|
|
||||||
Results.Ok(await _alertService.GetAlertHistoryAsync(count)));
|
|
||||||
|
|
||||||
app.MapPost($"{basePath}/alerts/{{alertId}}/resolve", async (string alertId) =>
|
|
||||||
{
|
|
||||||
await _alertService.ResolveAlertAsync(alertId);
|
|
||||||
return Results.Ok(new { Message = "Alert resolved successfully" });
|
|
||||||
});
|
|
||||||
|
|
||||||
app.MapGet($"{basePath}/alerts/enabled", async () =>
|
|
||||||
Results.Ok(new { Enabled = await _alertService.IsAlertingEnabledAsync() }));
|
|
||||||
|
|
||||||
// Telegram endpoints
|
|
||||||
app.MapGet($"{basePath}/telegram/status", async () =>
|
|
||||||
Results.Ok(new {
|
|
||||||
Enabled = await _telegramService.IsEnabledAsync(),
|
|
||||||
Connected = await _telegramService.TestConnectionAsync()
|
|
||||||
}));
|
|
||||||
|
|
||||||
app.MapPost($"{basePath}/telegram/test", async () =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var testAlert = new Alert
|
|
||||||
{
|
|
||||||
Timestamp = DateTime.Now,
|
|
||||||
Component = "Test",
|
|
||||||
Level = "Warning",
|
|
||||||
Message = "This is a test alert from Resource Monitor Service",
|
|
||||||
CurrentValue = 100,
|
|
||||||
ThresholdValue = 90,
|
|
||||||
IsResolved = false
|
|
||||||
};
|
|
||||||
|
|
||||||
await _telegramService.SendAlertAsync(testAlert);
|
|
||||||
return Results.Ok(new { Message = "Test alert sent successfully" });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
return Results.Problem($"Failed to send test alert: {ex.Message}");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Process management endpoints (enhanced)
|
|
||||||
app.MapPost($"{basePath}/process/kill", async (HttpContext context) =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
|
||||||
var request = JsonConvert.DeserializeObject<dynamic>(body);
|
|
||||||
|
|
||||||
int processId = request?.ProcessId ?? 0;
|
|
||||||
bool force = request?.Force ?? false;
|
|
||||||
|
|
||||||
if (processId <= 0)
|
|
||||||
{
|
|
||||||
return Results.BadRequest("Invalid process ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
var processes = Process.GetProcesses().Where(p => p.Id == processId).ToArray();
|
|
||||||
|
|
||||||
if (processes.Length == 0)
|
|
||||||
{
|
|
||||||
return Results.NotFound($"No process found with ID {processId}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var process = processes[0];
|
|
||||||
var processName = process.ProcessName;
|
|
||||||
|
|
||||||
if (force)
|
|
||||||
{
|
|
||||||
process.Kill(true); // Force kill entire process tree
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
process.CloseMainWindow(); // Try graceful close first
|
|
||||||
|
|
||||||
// Wait a bit for graceful close
|
|
||||||
await Task.Delay(3000);
|
|
||||||
|
|
||||||
if (!process.HasExited)
|
|
||||||
{
|
|
||||||
process.Kill();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_logger.LogWarning("Process {ProcessName} (ID: {ProcessId}) was terminated", processName, processId);
|
|
||||||
return Results.Ok(new { Message = $"Process {processName} (ID: {processId}) has been terminated." });
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error terminating process");
|
|
||||||
return Results.Problem(ex.Message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Enhanced shutdown/restart endpoints
|
|
||||||
app.MapPost($"{basePath}/system/shutdown", async (HttpContext context) =>
|
|
||||||
{
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var body = await new StreamReader(context.Request.Body).ReadToEndAsync();
|
|
||||||
var request = JsonConvert.DeserializeObject<dynamic>(body);
|
|
||||||
|
|
||||||
string action = request?.Action?.ToString()?.ToLower() ?? "shutdown";
|
|
||||||
int delaySeconds = request?.DelaySeconds ?? 0;
|
|
||||||
string message = request?.Message?.ToString() ?? "System shutdown initiated by Resource Monitor";
|
|
||||||
|
|
||||||
if (action != "shutdown" && action != "restart" && action != "cancel")
|
|
||||||
{
|
|
||||||
return Results.BadRequest("Invalid action. Use 'shutdown', 'restart', or 'cancel'.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (action == "cancel")
|
|
||||||
{
|
|
||||||
var cancelProcess = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "shutdown",
|
|
||||||
Arguments = "/a",
|
|
||||||
CreateNoWindow = true,
|
|
||||||
UseShellExecute = false
|
|
||||||
};
|
|
||||||
Process.Start(cancelProcess);
|
|
||||||
|
|
||||||
_logger.LogWarning("System shutdown cancelled");
|
|
||||||
return Results.Ok(new { Message = "Shutdown cancelled." });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delaySeconds < 0)
|
|
||||||
{
|
|
||||||
return Results.BadRequest("Delay must be a non-negative integer.");
|
|
||||||
}
|
|
||||||
|
|
||||||
string shutdownCommand = action == "shutdown"
|
|
||||||
? $"/s /f /t {delaySeconds} /c \"{message}\""
|
|
||||||
: $"/r /f /t {delaySeconds} /c \"{message}\"";
|
|
||||||
|
|
||||||
var processStartInfo = new ProcessStartInfo
|
|
||||||
{
|
|
||||||
FileName = "shutdown",
|
|
||||||
Arguments = shutdownCommand,
|
|
||||||
CreateNoWindow = true,
|
|
||||||
UseShellExecute = false
|
|
||||||
};
|
|
||||||
|
|
||||||
Process.Start(processStartInfo);
|
|
||||||
|
|
||||||
_logger.LogWarning("System {Action} initiated with {Delay} seconds delay", action, delaySeconds);
|
|
||||||
return Results.Ok(new {
|
|
||||||
Message = $"{action.ToUpper()} command executed with a delay of {delaySeconds} seconds.",
|
|
||||||
Action = action,
|
|
||||||
DelaySeconds = delaySeconds,
|
|
||||||
Timestamp = DateTime.Now
|
|
||||||
});
|
|
||||||
}
|
|
||||||
catch (Exception ex)
|
|
||||||
{
|
|
||||||
_logger.LogError(ex, "Error executing system command");
|
|
||||||
return Results.Problem(ex.Message);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// VM-specific endpoints for Unraid
|
|
||||||
app.MapGet($"{basePath}/vm/info", async () =>
|
|
||||||
{
|
|
||||||
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
|
||||||
return Results.Ok(new
|
|
||||||
{
|
|
||||||
IsVirtualMachine = systemInfo.IsVirtualMachine,
|
|
||||||
HypervisorVendor = systemInfo.HypervisorVendor,
|
|
||||||
Uptime = systemInfo.Uptime,
|
|
||||||
BootTime = systemInfo.BootTime,
|
|
||||||
MachineName = systemInfo.MachineName,
|
|
||||||
Domain = systemInfo.Domain
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Performance history endpoint (simple in-memory storage)
|
|
||||||
app.MapGet($"{basePath}/performance/history", async (int minutes = 60) =>
|
|
||||||
{
|
|
||||||
// This would ideally be stored in a database or time-series database
|
|
||||||
// For now, return current snapshot with timestamp
|
|
||||||
var usage = await _resourceMonitorService.GetResourceUsageAsync();
|
|
||||||
return Results.Ok(new {
|
|
||||||
Current = usage,
|
|
||||||
Message = "Historical data not implemented yet - showing current values"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Health check endpoint
|
|
||||||
app.MapGet($"{basePath}/health", async () =>
|
|
||||||
{
|
|
||||||
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
|
||||||
var alerts = await _alertService.GetActiveAlertsAsync();
|
|
||||||
|
|
||||||
return Results.Ok(new
|
|
||||||
{
|
|
||||||
Status = "Healthy",
|
|
||||||
Timestamp = DateTime.Now,
|
|
||||||
Uptime = systemInfo.Uptime,
|
|
||||||
ActiveAlerts = alerts.Count,
|
|
||||||
MonitoringEnabled = new
|
|
||||||
{
|
|
||||||
GPU = _monitoringSettings.EnableGpuMonitoring,
|
|
||||||
Disk = _monitoringSettings.EnableDiskMonitoring,
|
|
||||||
Network = _monitoringSettings.EnableNetworkMonitoring,
|
|
||||||
Temperature = _monitoringSettings.EnableTemperatureMonitoring,
|
|
||||||
Processes = _monitoringSettings.EnableProcessMonitoring,
|
|
||||||
Games = _monitoringSettings.EnableGameDetection,
|
|
||||||
Alerts = _monitoringSettings.EnableAlerts
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Service control endpoints
|
|
||||||
app.MapPost($"{basePath}/service/stop", () =>
|
|
||||||
{
|
|
||||||
_logger.LogWarning("Service stop requested via API");
|
|
||||||
_lifetime.StopApplication();
|
|
||||||
return Results.Ok(new { Message = "Stopping the service..." });
|
|
||||||
});
|
|
||||||
|
|
||||||
// Configuration endpoint
|
|
||||||
app.MapGet($"{basePath}/config", () => Results.Ok(new
|
|
||||||
{
|
|
||||||
MonitoringSettings = new
|
|
||||||
{
|
|
||||||
UpdateInterval = _monitoringSettings.UpdateIntervalMs,
|
|
||||||
EnableGpuMonitoring = _monitoringSettings.EnableGpuMonitoring,
|
|
||||||
EnableDiskMonitoring = _monitoringSettings.EnableDiskMonitoring,
|
|
||||||
EnableNetworkMonitoring = _monitoringSettings.EnableNetworkMonitoring,
|
|
||||||
EnableTemperatureMonitoring = _monitoringSettings.EnableTemperatureMonitoring,
|
|
||||||
EnableProcessMonitoring = _monitoringSettings.EnableProcessMonitoring,
|
|
||||||
EnableGameDetection = _monitoringSettings.EnableGameDetection,
|
|
||||||
EnableAlerts = _monitoringSettings.EnableAlerts
|
|
||||||
},
|
|
||||||
ApiSettings = new
|
|
||||||
{
|
|
||||||
BasePath = _apiSettings.BasePath,
|
|
||||||
RequireApiKey = _apiSettings.RequireApiKey,
|
|
||||||
AllowedOrigins = _apiSettings.AllowedOrigins
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
// Service metrics endpoint
|
|
||||||
app.MapGet($"{basePath}/metrics", async () =>
|
|
||||||
{
|
|
||||||
var usage = await _resourceMonitorService.GetResourceUsageAsync();
|
|
||||||
var systemInfo = await _systemInfoService.GetSystemInfoAsync();
|
|
||||||
return Results.Ok(new
|
|
||||||
{
|
|
||||||
Service = "Resource Monitor Service",
|
|
||||||
Version = "2.0.0",
|
|
||||||
Status = "Running",
|
|
||||||
Timestamp = DateTime.Now,
|
|
||||||
Uptime = systemInfo.Uptime,
|
|
||||||
LastUpdate = usage.Timestamp,
|
|
||||||
Performance = new
|
|
||||||
{
|
|
||||||
CPU = usage.CPU.Usage,
|
|
||||||
Memory = usage.Memory.UsagePercentage,
|
|
||||||
GPU = usage.GPU.Usage,
|
|
||||||
ActiveProcesses = usage.TopProcesses.Count
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// Root endpoint
|
|
||||||
app.MapGet("/", () => Results.Ok(new
|
|
||||||
{
|
|
||||||
Service = "Resource Monitor Service for Unraid VM",
|
|
||||||
Version = "2.0.0",
|
|
||||||
Status = "Running",
|
|
||||||
Timestamp = DateTime.Now,
|
|
||||||
ApiBasePath = _apiSettings.BasePath,
|
|
||||||
Endpoints = new[]
|
|
||||||
{
|
|
||||||
$"{_apiSettings.BasePath}/health",
|
|
||||||
$"{_apiSettings.BasePath}/system-info",
|
|
||||||
$"{_apiSettings.BasePath}/resource-usage",
|
|
||||||
$"{_apiSettings.BasePath}/cpu-usage",
|
|
||||||
$"{_apiSettings.BasePath}/memory-usage",
|
|
||||||
$"{_apiSettings.BasePath}/gpu-usage",
|
|
||||||
$"{_apiSettings.BasePath}/disk-usage",
|
|
||||||
$"{_apiSettings.BasePath}/network-usage",
|
|
||||||
$"{_apiSettings.BasePath}/top-processes",
|
|
||||||
$"{_apiSettings.BasePath}/current-game",
|
|
||||||
$"{_apiSettings.BasePath}/alerts/active",
|
|
||||||
$"{_apiSettings.BasePath}/vm/info",
|
|
||||||
$"{_apiSettings.BasePath}/config",
|
|
||||||
$"{_apiSettings.BasePath}/metrics"
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
|
|
||||||
_logger.LogInformation("API endpoints configured. Base path: {BasePath}", _apiSettings.BasePath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private async Task BackgroundMonitoringLoop(CancellationToken cancellationToken)
|
private async Task BackgroundMonitoringLoop(CancellationToken cancellationToken)
|
||||||
@@ -472,10 +76,25 @@ namespace ResourceMonitorService
|
|||||||
await _alertService.CheckAndGenerateAlertsAsync(resourceUsage);
|
await _alertService.CheckAndGenerateAlertsAsync(resourceUsage);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Send real-time updates via SignalR
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var scope = _serviceProvider.CreateScope();
|
||||||
|
var hubContext = scope.ServiceProvider.GetService<IHubContext<ResourceHub>>();
|
||||||
|
if (hubContext != null)
|
||||||
|
{
|
||||||
|
await hubContext.Clients.All.SendAsync("ResourceUpdate", resourceUsage, cancellationToken);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
_logger.LogDebug("SignalR broadcast error: {Message}", ex.Message);
|
||||||
|
}
|
||||||
|
|
||||||
successfulCycles++;
|
successfulCycles++;
|
||||||
|
|
||||||
// Log performance metrics occasionally
|
// Log performance metrics occasionally
|
||||||
if (successfulCycles % 6 == 0) // Every 30 seconds with 5-second intervals
|
if (successfulCycles % 4 == 0) // Every 60 seconds with 15-second intervals
|
||||||
{
|
{
|
||||||
_logger.LogDebug("Performance: CPU: {CpuUsage:F1}%, Memory: {MemoryUsage:F1}%, GPU: {GpuUsage}%",
|
_logger.LogDebug("Performance: CPU: {CpuUsage:F1}%, Memory: {MemoryUsage:F1}%, GPU: {GpuUsage}%",
|
||||||
resourceUsage.CPU.Usage,
|
resourceUsage.CPU.Usage,
|
||||||
|
|||||||
+4
-1
@@ -12,7 +12,10 @@
|
|||||||
"Kestrel": {
|
"Kestrel": {
|
||||||
"Endpoints": {
|
"Endpoints": {
|
||||||
"Http": {
|
"Http": {
|
||||||
"Url": "http://*:2414"
|
"Url": "http://*:5000"
|
||||||
|
},
|
||||||
|
"Https": {
|
||||||
|
"Url": "https://*:5001"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,9 +0,0 @@
|
|||||||
import dynamic from 'next/dynamic';
|
|
||||||
|
|
||||||
const components = {
|
|
||||||
// other components
|
|
||||||
resourceUsage: dynamic(() => import('./resourceUsage/component')),
|
|
||||||
yourwidget: dynamic(() => import("./yourwidget/component"))
|
|
||||||
};
|
|
||||||
|
|
||||||
export default components;
|
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
import resourceUsage from "./resourceUsage/widget";
|
|
||||||
import yourwidget from "./yourwidget/widget";
|
|
||||||
const widgets = {
|
|
||||||
// other widgets
|
|
||||||
resourceUsage: resourceUsage,
|
|
||||||
yourwidget: yourwidget
|
|
||||||
};
|
|
||||||
|
|
||||||
export default widgets;
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
|
||||||
import Container from "components/services/widget/container";
|
|
||||||
import Block from "components/services/widget/block";
|
|
||||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
|
||||||
|
|
||||||
export default function Component({ service }) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { widget } = service;
|
|
||||||
const { data, error } = useWidgetAPI(widget, "info");
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <Container service={service} error={error} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<Container service={service}>
|
|
||||||
<Block label="Loading..." />
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container service={service}>
|
|
||||||
<Block label="Machine Name" value={data.ComputerInfo.MachineName} />
|
|
||||||
<Block label="OS Version" value={data.ComputerInfo.OSVersion} />
|
|
||||||
<Block label="OS Architecture" value={data.ComputerInfo.OSArchitecture} />
|
|
||||||
<Block label="Processor Count" value={data.ComputerInfo.ProcessorCount} />
|
|
||||||
<Block label="CPU Usage" value={data.CPU} />
|
|
||||||
<Block label="RAM Usage" value={data.RAM} />
|
|
||||||
<Block label="GPU Usage" value={data.GPU.Usage} />
|
|
||||||
<Block label="GPU Temperature" value={data.GPU.Temperature} />
|
|
||||||
<Block label="GPU Fan Speed" value={data.GPU.FanSpeed} />
|
|
||||||
<Block label="Currently Running Game" value={data.CurrentlyRunningGame} />
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
|
||||||
|
|
||||||
const widget = {
|
|
||||||
api: "http://192.168.50.201:5000/api/resource-usage",
|
|
||||||
proxyHandler: genericProxyHandler,
|
|
||||||
mappings: {
|
|
||||||
info: {
|
|
||||||
endpoint: ""
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
export default widget;
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
import { useTranslation } from "next-i18next";
|
|
||||||
|
|
||||||
import Container from "components/services/widget/container";
|
|
||||||
import Block from "components/services/widget/block";
|
|
||||||
import useWidgetAPI from "utils/proxy/use-widget-api";
|
|
||||||
|
|
||||||
export default function Component({ service }) {
|
|
||||||
const { t } = useTranslation();
|
|
||||||
const { widget } = service;
|
|
||||||
const { data, error } = useWidgetAPI(widget, "info");
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return <Container service={service} error={error} />;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!data) {
|
|
||||||
return (
|
|
||||||
<Container service={service}>
|
|
||||||
<Block label="yourwidget.key1" />
|
|
||||||
<Block label="yourwidget.key2" />
|
|
||||||
<Block label="yourwidget.key3" />
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Container service={service}>
|
|
||||||
<Block label="yourwidget.key1" value={t("common.number", { value: data.key1 })} />
|
|
||||||
<Block label="yourwidget.key2" value={t("common.number", { value: data.key2 })} />
|
|
||||||
<Block label="yourwidget.key3" value={t("common.number", { value: data.key3 })} />
|
|
||||||
</Container>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
import genericProxyHandler from "utils/proxy/handlers/generic";
|
|
||||||
|
|
||||||
const widget = {
|
|
||||||
api: "{url}/{endpoint}" ,
|
|
||||||
proxyHandler: genericProxyHandler ,
|
|
||||||
|
|
||||||
mappings: {
|
|
||||||
info: {
|
|
||||||
endpoint: "v1/info" ,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
export default widget;
|
|
||||||
@@ -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