From 3d47fc14396de7322f21422cd14aca0d27a1bb4d Mon Sep 17 00:00:00 2001 From: Phoenix Date: Thu, 7 Aug 2025 23:02:03 +0800 Subject: [PATCH] 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. --- Configuration/MonitoringSettings.cs | 2 +- Controllers/ResourceController.cs | 173 +++++++ Hubs/ResourceHub.cs | 17 + Models/SystemInfo.cs | 2 +- Program.cs | 6 + Properties/launchSettings.json | 1 + README_WebUI.md | 146 ++++++ ResourceMonitorService.csproj | 4 +- Services/GameDetectionService.cs | 41 +- Services/ResourceMonitorService.cs | 4 +- Startup .cs | 71 ++- Worker.cs | 431 +--------------- appsettings.json | 5 +- src/components.js | 9 - src/widgets.js | 9 - src/widgets/resourceUsage/component.jsx | 37 -- src/widgets/resourceUsage/widget.js | 13 - src/widgets/yourwidget/component.jsx | 33 -- src/widgets/yourwidget/widget.js | 14 - wwwroot/css/dashboard.css | 93 ++++ wwwroot/favicon.ico | 3 + wwwroot/index.html | 205 ++++++++ wwwroot/js/dashboard.js | 633 ++++++++++++++++++++++++ 23 files changed, 1405 insertions(+), 547 deletions(-) create mode 100644 Controllers/ResourceController.cs create mode 100644 Hubs/ResourceHub.cs create mode 100644 README_WebUI.md delete mode 100644 src/components.js delete mode 100644 src/widgets.js delete mode 100644 src/widgets/resourceUsage/component.jsx delete mode 100644 src/widgets/resourceUsage/widget.js delete mode 100644 src/widgets/yourwidget/component.jsx delete mode 100644 src/widgets/yourwidget/widget.js create mode 100644 wwwroot/css/dashboard.css create mode 100644 wwwroot/favicon.ico create mode 100644 wwwroot/index.html create mode 100644 wwwroot/js/dashboard.js diff --git a/Configuration/MonitoringSettings.cs b/Configuration/MonitoringSettings.cs index cbdb1b8..340d42f 100644 --- a/Configuration/MonitoringSettings.cs +++ b/Configuration/MonitoringSettings.cs @@ -2,7 +2,7 @@ namespace ResourceMonitorService.Configuration { 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 bool EnableGpuMonitoring { get; set; } = true; public bool EnableDiskMonitoring { get; set; } = true; diff --git a/Controllers/ResourceController.cs b/Controllers/ResourceController.cs new file mode 100644 index 0000000..dc1b9e5 --- /dev/null +++ b/Controllers/ResourceController.cs @@ -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 _logger; + + public ResourceController( + IResourceMonitorService resourceMonitorService, + ISystemInfoService systemInfoService, + ILogger logger) + { + _resourceMonitorService = resourceMonitorService; + _systemInfoService = systemInfoService; + _logger = logger; + } + + [HttpGet("usage")] + public async Task> 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> 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> 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> 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> 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>> 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> 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>> 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"); + } + } + } +} diff --git a/Hubs/ResourceHub.cs b/Hubs/ResourceHub.cs new file mode 100644 index 0000000..53a914e --- /dev/null +++ b/Hubs/ResourceHub.cs @@ -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); + } + } +} diff --git a/Models/SystemInfo.cs b/Models/SystemInfo.cs index 691f5e7..cbe63ad 100644 --- a/Models/SystemInfo.cs +++ b/Models/SystemInfo.cs @@ -107,7 +107,7 @@ namespace ResourceMonitorService.Models public class ProcessInfo { - public int Id { get; set; } + public int ProcessId { get; set; } public string Name { get; set; } = string.Empty; public float CpuUsage { get; set; } public ulong MemoryUsage { get; set; } diff --git a/Program.cs b/Program.cs index a9d2c98..69cfb0d 100644 --- a/Program.cs +++ b/Program.cs @@ -1,3 +1,4 @@ +using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Configuration; @@ -37,6 +38,11 @@ namespace ResourceMonitorService { var builder = Host.CreateDefaultBuilder(args) .UseSerilog() + .ConfigureWebHostDefaults(webBuilder => + { + webBuilder.UseStartup(); + webBuilder.UseUrls("http://localhost:5000", "https://localhost:5001"); + }) .ConfigureServices((hostContext, services) => { // Bind configuration sections diff --git a/Properties/launchSettings.json b/Properties/launchSettings.json index 371886f..2ae32af 100644 --- a/Properties/launchSettings.json +++ b/Properties/launchSettings.json @@ -4,6 +4,7 @@ "ResourceMonitorService": { "commandName": "Project", "dotnetRunMessages": true, + "applicationUrl": "http://localhost:5000;https://localhost:5001", "environmentVariables": { "DOTNET_ENVIRONMENT": "Development" } diff --git a/README_WebUI.md b/README_WebUI.md new file mode 100644 index 0000000..06bfd76 --- /dev/null +++ b/README_WebUI.md @@ -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) diff --git a/ResourceMonitorService.csproj b/ResourceMonitorService.csproj index 8eb14ba..af39174 100644 --- a/ResourceMonitorService.csproj +++ b/ResourceMonitorService.csproj @@ -1,4 +1,4 @@ - + net9.0-windows @@ -18,5 +18,7 @@ + + diff --git a/Services/GameDetectionService.cs b/Services/GameDetectionService.cs index 080a38e..826b7f3 100644 --- a/Services/GameDetectionService.cs +++ b/Services/GameDetectionService.cs @@ -249,22 +249,39 @@ namespace ResourceMonitorService.Services "setup", "installer", "update", "vshost", "devenv" }; - // Skip if it's a known system process - if (systemExclusions.Any(exclusion => fileName.Contains(exclusion))) + // Exclude gaming platform clients (they are not games themselves) + 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; } var knownGameExecutables = new[] { - "game", "launcher", "client" - // Removed generic terms like "main", "start", "run" that match too many system processes + "game" + // Removed "launcher" and "client" as they often refer to platform clients, not games }; var gameIndicators = new[] { "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 @@ -398,6 +415,20 @@ namespace ResourceMonitorService.Services if (!string.IsNullOrEmpty(versionInfo.ProductName) && !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; } diff --git a/Services/ResourceMonitorService.cs b/Services/ResourceMonitorService.cs index 3ac0dfe..36f9fdc 100644 --- a/Services/ResourceMonitorService.cs +++ b/Services/ResourceMonitorService.cs @@ -770,7 +770,7 @@ namespace ResourceMonitorService.Services var processInfo = new ProcessInfo { - Id = p.Id, + ProcessId = p.Id, Name = p.ProcessName, MemoryUsage = memoryUsage, 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); // 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 .OrderByDescending(p => p.CpuUsage) diff --git a/Startup .cs b/Startup .cs index a80e807..7b1cbbf 100644 --- a/Startup .cs +++ b/Startup .cs @@ -3,29 +3,70 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.AspNetCore.Http; +using ResourceMonitorService.Hubs; +using Microsoft.OpenApi.Models; -public class Startup +namespace ResourceMonitorService { - public void ConfigureServices(IServiceCollection services) + public class Startup { - // Add services to the container - } - - public void Configure(IApplicationBuilder app, IWebHostEnvironment env) - { - if (env.IsDevelopment()) + public void ConfigureServices(IServiceCollection services) { - app.UseDeveloperExceptionPage(); + // 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" }); + }); } - app.UseRouting(); - - app.UseEndpoints(endpoints => + public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { - endpoints.MapGet("/", async context => + if (env.IsDevelopment()) { - await context.Response.WriteAsync("Hello World!"); + 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.UseCors("AllowAll"); + + app.UseEndpoints(endpoints => + { + // Map API controllers + endpoints.MapControllers(); + + // Map SignalR hub + endpoints.MapHub("/resourceHub"); + + // Default route to index.html + endpoints.MapFallbackToFile("index.html"); }); - }); + } } } \ No newline at end of file diff --git a/Worker.cs b/Worker.cs index 9d5d715..7925f08 100644 --- a/Worker.cs +++ b/Worker.cs @@ -1,439 +1,43 @@ -using Microsoft.AspNetCore.Builder; -using Microsoft.AspNetCore.Http; -using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Hosting; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; -using Newtonsoft.Json; using ResourceMonitorService.Configuration; -using ResourceMonitorService.Models; using ResourceMonitorService.Services; -using System.Diagnostics; +using ResourceMonitorService.Hubs; +using Microsoft.AspNetCore.SignalR; +using Microsoft.Extensions.DependencyInjection; namespace ResourceMonitorService { public class Worker : BackgroundService { private readonly ILogger _logger; - private readonly IHostApplicationLifetime _lifetime; - private readonly ISystemInfoService _systemInfoService; private readonly IResourceMonitorService _resourceMonitorService; private readonly IGameDetectionService _gameDetectionService; private readonly IAlertService _alertService; - private readonly ITelegramNotificationService _telegramService; - private readonly ApiSettings _apiSettings; private readonly MonitoringSettings _monitoringSettings; + private readonly IServiceProvider _serviceProvider; public Worker( ILogger logger, - IHostApplicationLifetime lifetime, - ISystemInfoService systemInfoService, IResourceMonitorService resourceMonitorService, IGameDetectionService gameDetectionService, IAlertService alertService, - ITelegramNotificationService telegramService, - IOptions apiSettings, - IOptions monitoringSettings) + IOptions monitoringSettings, + IServiceProvider serviceProvider) { _logger = logger; - _lifetime = lifetime; - _systemInfoService = systemInfoService; _resourceMonitorService = resourceMonitorService; _gameDetectionService = gameDetectionService; _alertService = alertService; - _telegramService = telegramService; - _apiSettings = apiSettings.Value; _monitoringSettings = monitoringSettings.Value; + _serviceProvider = serviceProvider; } protected override async Task ExecuteAsync(CancellationToken stoppingToken) { - _logger.LogInformation("Resource Monitor Service starting..."); - - 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(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(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); + _logger.LogInformation("Resource Monitor background service starting..."); + await BackgroundMonitoringLoop(stoppingToken); } private async Task BackgroundMonitoringLoop(CancellationToken cancellationToken) @@ -472,10 +76,25 @@ namespace ResourceMonitorService await _alertService.CheckAndGenerateAlertsAsync(resourceUsage); } + // Send real-time updates via SignalR + try + { + using var scope = _serviceProvider.CreateScope(); + var hubContext = scope.ServiceProvider.GetService>(); + if (hubContext != null) + { + await hubContext.Clients.All.SendAsync("ResourceUpdate", resourceUsage, cancellationToken); + } + } + catch (Exception ex) + { + _logger.LogDebug("SignalR broadcast error: {Message}", ex.Message); + } + successfulCycles++; // 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}%", resourceUsage.CPU.Usage, diff --git a/appsettings.json b/appsettings.json index ccf3d10..b1d401b 100644 --- a/appsettings.json +++ b/appsettings.json @@ -12,7 +12,10 @@ "Kestrel": { "Endpoints": { "Http": { - "Url": "http://*:2414" + "Url": "http://*:5000" + }, + "Https": { + "Url": "https://*:5001" } } }, diff --git a/src/components.js b/src/components.js deleted file mode 100644 index 84f41d9..0000000 --- a/src/components.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/src/widgets.js b/src/widgets.js deleted file mode 100644 index e405a6b..0000000 --- a/src/widgets.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/src/widgets/resourceUsage/component.jsx b/src/widgets/resourceUsage/component.jsx deleted file mode 100644 index 17dcb80..0000000 --- a/src/widgets/resourceUsage/component.jsx +++ /dev/null @@ -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 ; - } - - if (!data) { - return ( - - - - ); - } - - return ( - - - - - - - - - - - - - ); -} \ No newline at end of file diff --git a/src/widgets/resourceUsage/widget.js b/src/widgets/resourceUsage/widget.js deleted file mode 100644 index eb2e4dc..0000000 --- a/src/widgets/resourceUsage/widget.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/src/widgets/yourwidget/component.jsx b/src/widgets/yourwidget/component.jsx deleted file mode 100644 index e496ab2..0000000 --- a/src/widgets/yourwidget/component.jsx +++ /dev/null @@ -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 ; - } - - if (!data) { - return ( - - - - - - ); - } - - return ( - - - - - - ); -} \ No newline at end of file diff --git a/src/widgets/yourwidget/widget.js b/src/widgets/yourwidget/widget.js deleted file mode 100644 index d30edbe..0000000 --- a/src/widgets/yourwidget/widget.js +++ /dev/null @@ -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; \ No newline at end of file diff --git a/wwwroot/css/dashboard.css b/wwwroot/css/dashboard.css new file mode 100644 index 0000000..81f3064 --- /dev/null +++ b/wwwroot/css/dashboard.css @@ -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; + } +} diff --git a/wwwroot/favicon.ico b/wwwroot/favicon.ico new file mode 100644 index 0000000..ccfc309 --- /dev/null +++ b/wwwroot/favicon.ico @@ -0,0 +1,3 @@ + + + diff --git a/wwwroot/index.html b/wwwroot/index.html new file mode 100644 index 0000000..d11096b --- /dev/null +++ b/wwwroot/index.html @@ -0,0 +1,205 @@ + + + + + + Resource Monitor Dashboard + + + + + + + + + + + + +
+ +
+ +
+
+
+

CPU Usage

+

0%

+
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+

Memory Usage

+

0%

+
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+

GPU Usage

+

0%

+
+
+ +
+
+
+
+
+
+
+
+ + +
+
+
+

Network

+

0 MB/s

+
+
+ +
+
+
+

↑ 0 MB/s ↓ 0 MB/s

+
+
+
+ + +
+
+

+ Game Detection +

+ No game detected +
+
+
+ +
+

No game currently running

+
+
+ + + + + + +
+ + +
+
+
+ Loading... +
+
+ + + + diff --git a/wwwroot/js/dashboard.js b/wwwroot/js/dashboard.js new file mode 100644 index 0000000..68656b3 --- /dev/null +++ b/wwwroot/js/dashboard.js @@ -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 = 'Hide Details'; + // Refresh disk usage when details section becomes visible + this.refreshDetailsData(); + } else { + detailsSection.classList.add('hidden'); + toggleButton.innerHTML = 'Details'; + } + } + + toggleProcessesSection() { + const processesSection = document.getElementById('processesSection'); + const toggleButton = document.getElementById('toggleProcesses'); + + if (processesSection.classList.contains('hidden')) { + processesSection.classList.remove('hidden'); + toggleButton.innerHTML = 'Hide Processes'; + // Refresh processes when section becomes visible + this.refreshProcessesData(); + } else { + processesSection.classList.add('hidden'); + toggleButton.innerHTML = '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 = 'Auto: ON'; + toggleButton.className = 'bg-yellow-500 hover:bg-yellow-700 px-4 py-2 rounded-lg transition-colors'; + this.startAutoRefresh(); + } else { + toggleButton.innerHTML = '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 = 'No process data available'; + 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 = ` + +
+
${processName}
+
PID: ${processId}
+
+ + + ${cpuUsage.toFixed(1)}% + + + ${(memoryUsage / 1024 / 1024).toFixed(1)} MB + + + + + `; + 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 = ` +
+

Machine Name

+

${systemInfo.machineName || 'N/A'}

+
+
+

OS Version

+

${systemInfo.osVersion || 'N/A'}

+
+
+

CPU

+

${systemInfo.cpuName || 'N/A'}

+
+
+

Processor Count

+

${systemInfo.processorCount || 0} cores

+
+
+

Total Memory

+

${systemInfo.totalPhysicalMemory ? (systemInfo.totalPhysicalMemory / 1024 / 1024 / 1024).toFixed(1) : 'N/A'} GB

+
+
+

Uptime

+

${systemInfo.uptime ? this.formatUptime(systemInfo.uptime) : 'N/A'}

+
+ `; + } + + updateDiskUsage(disks) { + const diskUsageDiv = document.getElementById('diskUsage'); + diskUsageDiv.innerHTML = ''; + + if (!disks || !Array.isArray(disks)) { + diskUsageDiv.innerHTML = '

No disk information available

'; + 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 = ` +
+

${diskName}

+ ${usagePercentage.toFixed(1)}% used +
+
+
+
+
+ Free: ${(disk.freeSpace / 1024 / 1024 / 1024).toFixed(1)} GB + Total: ${(disk.totalSize / 1024 / 1024 / 1024).toFixed(1)} GB +
+ `; + 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 = ` +
+
+ +
+

No game currently running

+
+ `; + return; + } + + console.log('Game detected:', gameInfo.gameName); // Debug log + gameStatusSpan.textContent = 'Game detected'; + gameStatusSpan.className = 'text-sm text-green-600 font-semibold'; + + gameInfoDiv.innerHTML = ` +
+
+
+
+ +
+
+

${gameInfo.gameName || 'Unknown Game'}

+

Running since ${this.formatGameStartTime(gameInfo.startTime)}

+
+
+
+
+ ACTIVE +
+ +
+
+ +
+
+

Process ID

+

${gameInfo.processId || 'N/A'}

+
+
+

Memory Usage

+

${gameInfo.memoryUsage ? (gameInfo.memoryUsage / 1024 / 1024).toFixed(1) + ' MB' : 'N/A'}

+
+
+

CPU Usage

+

${gameInfo.cpuUsage ? gameInfo.cpuUsage.toFixed(1) + '%' : 'N/A'}

+
+
+
+ `; + } + + 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(); +});