Compare commits
7 Commits
master
...
eceec1b72d
| Author | SHA1 | Date | |
|---|---|---|---|
| eceec1b72d | |||
| 1129f9a2b1 | |||
| 5ece1fbe27 | |||
| 35828f189c | |||
| e842b7d73e | |||
| 96b6e3dcd9 | |||
| 3d47fc1439 |
@@ -2,11 +2,10 @@ 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;
|
||||
public bool EnableNetworkMonitoring { get; set; } = true;
|
||||
public bool EnableTemperatureMonitoring { get; set; } = true;
|
||||
public bool EnableProcessMonitoring { get; set; } = true;
|
||||
public bool EnableGameDetection { get; set; } = true;
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
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("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");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("system-control")]
|
||||
public ActionResult SystemControl([FromBody] SystemControlRequest request)
|
||||
{
|
||||
try
|
||||
{
|
||||
if (request == null)
|
||||
{
|
||||
return BadRequest("Invalid request");
|
||||
}
|
||||
|
||||
// Validate action
|
||||
if (request.Action != "shutdown" && request.Action != "restart")
|
||||
{
|
||||
return BadRequest("Invalid action. Use 'shutdown' or 'restart'");
|
||||
}
|
||||
|
||||
// Validate timer
|
||||
if (request.Timer < 0 || request.Timer > 86400)
|
||||
{
|
||||
return BadRequest("Timer must be between 0 and 86400 seconds (24 hours)");
|
||||
}
|
||||
|
||||
// Build the shutdown command
|
||||
var arguments = request.Action == "shutdown" ? "/s" : "/r";
|
||||
|
||||
if (request.Force)
|
||||
{
|
||||
arguments += " /f";
|
||||
}
|
||||
|
||||
if (request.Timer > 0)
|
||||
{
|
||||
arguments += $" /t {request.Timer}";
|
||||
}
|
||||
|
||||
// Execute the shutdown command
|
||||
var processInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "shutdown",
|
||||
Arguments = arguments,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true
|
||||
};
|
||||
|
||||
var process = Process.Start(processInfo);
|
||||
|
||||
string message;
|
||||
if (request.Timer > 0)
|
||||
{
|
||||
var minutes = request.Timer / 60;
|
||||
var seconds = request.Timer % 60;
|
||||
var timeString = minutes > 0
|
||||
? $"{minutes} minute{(minutes != 1 ? "s" : "")}" + (seconds > 0 ? $" and {seconds} second{(seconds != 1 ? "s" : "")}" : "")
|
||||
: $"{seconds} second{(seconds != 1 ? "s" : "")}";
|
||||
|
||||
message = $"System {request.Action} scheduled in {timeString}";
|
||||
}
|
||||
else
|
||||
{
|
||||
message = $"System {request.Action} initiated immediately";
|
||||
}
|
||||
|
||||
_logger.LogWarning($"System {request.Action} command executed by user. Timer: {request.Timer}s, Force: {request.Force}");
|
||||
|
||||
return Ok(new { message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, $"Error executing system {request?.Action} command");
|
||||
return StatusCode(500, "Internal server error");
|
||||
}
|
||||
}
|
||||
|
||||
[HttpPost("cancel-shutdown")]
|
||||
public ActionResult CancelShutdown()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Execute the shutdown abort command
|
||||
var processInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = "shutdown",
|
||||
Arguments = "/a",
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true
|
||||
};
|
||||
|
||||
var process = Process.Start(processInfo);
|
||||
process?.WaitForExit();
|
||||
|
||||
var exitCode = process?.ExitCode ?? -1;
|
||||
|
||||
string message;
|
||||
if (exitCode == 0)
|
||||
{
|
||||
message = "Shutdown/restart canceled successfully";
|
||||
_logger.LogInformation("Shutdown/restart canceled by user");
|
||||
}
|
||||
else
|
||||
{
|
||||
message = "No shutdown/restart was scheduled to cancel, or cancellation failed";
|
||||
_logger.LogInformation("Attempted to cancel shutdown but none was scheduled");
|
||||
}
|
||||
|
||||
return Ok(new { message });
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error canceling shutdown command");
|
||||
return StatusCode(500, "Internal server error");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public class SystemControlRequest
|
||||
{
|
||||
public string Action { get; set; } = string.Empty;
|
||||
public int Timer { get; set; } = 0;
|
||||
public bool Force { get; set; } = true;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,226 @@
|
||||
# ResourceMonitorService - Packaging and VM Deployment Guide
|
||||
|
||||
This guide explains how to package the ResourceMonitorService for release and deploy it to a VM.
|
||||
|
||||
## 📦 Packaging for Release
|
||||
|
||||
### Quick Packaging
|
||||
```powershell
|
||||
# Simple packaging - creates a ZIP file with all necessary components
|
||||
.\create-package.ps1
|
||||
|
||||
# Specify version
|
||||
.\create-package.ps1 -Version "2.1.1"
|
||||
```
|
||||
|
||||
### What Gets Packaged
|
||||
The packaging script includes:
|
||||
- ✅ Compiled .NET binaries (Release build)
|
||||
- ✅ Installation scripts (`install-service.ps1`, `start-service.bat`)
|
||||
- ✅ Configuration files (`appsettings.json`)
|
||||
- ✅ Documentation files (`README.md`, etc.)
|
||||
- ✅ Deployment instructions (`DEPLOYMENT.txt`)
|
||||
|
||||
### Output
|
||||
- **Location**: `.\release-packages\`
|
||||
- **Format**: `ResourceMonitorService-v{VERSION}-{TIMESTAMP}.zip`
|
||||
- **Size**: ~2.3 MB
|
||||
- **Example**: `ResourceMonitorService-v2.1.0-20250807-2322.zip`
|
||||
|
||||
## 🚀 VM Deployment Options
|
||||
|
||||
### Option 1: Manual Deployment (Recommended)
|
||||
|
||||
1. **Transfer the ZIP file to your VM**:
|
||||
- Copy via RDP shared folders
|
||||
- Use network file share
|
||||
- Download from cloud storage
|
||||
- USB transfer
|
||||
|
||||
2. **On the VM**:
|
||||
```powershell
|
||||
# Extract the ZIP file to a temporary directory
|
||||
Expand-Archive -Path "ResourceMonitorService-v2.1.0-*.zip" -DestinationPath "C:\Temp\ResourceMonitor"
|
||||
|
||||
# Navigate to extracted directory
|
||||
cd "C:\Temp\ResourceMonitor"
|
||||
|
||||
# Run installation as Administrator
|
||||
.\install-service.ps1
|
||||
```
|
||||
|
||||
3. **Access the service**:
|
||||
- Web Dashboard: `http://VM-IP:5000`
|
||||
- API Health: `http://VM-IP:5000/api/health`
|
||||
|
||||
### Option 2: Automated Deployment (Advanced)
|
||||
|
||||
If your VM has PowerShell Remoting enabled:
|
||||
|
||||
```powershell
|
||||
# Deploy using WinRM (requires setup)
|
||||
.\deploy-to-vm.ps1 -VMAddress "192.168.1.100" -UseWinRM
|
||||
|
||||
# Copy only (no auto-install)
|
||||
.\deploy-to-vm.ps1 -VMAddress "192.168.1.100" -UseWinRM -CopyOnly
|
||||
```
|
||||
|
||||
### Option 3: Complete Build & Deploy
|
||||
|
||||
```powershell
|
||||
# Build, package, and deploy in one command
|
||||
.\build-and-deploy.ps1 -VMAddress "192.168.1.100" -DeployToVM -UseWinRM
|
||||
```
|
||||
|
||||
## 🔧 VM Prerequisites
|
||||
|
||||
### Required Software
|
||||
- **Windows 10/11 or Windows Server 2019+**
|
||||
- **.NET 9.0 Runtime** ([Download](https://dotnet.microsoft.com/download/dotnet/9.0))
|
||||
- **PowerShell 5.1+** (Built into Windows)
|
||||
|
||||
### Administrator Privileges
|
||||
The installation requires Administrator privileges to:
|
||||
- Create Windows Service
|
||||
- Configure firewall rules
|
||||
- Create directories in Program Files
|
||||
- Set service permissions
|
||||
|
||||
### Network Requirements
|
||||
- **Port 5000**: Web Dashboard access
|
||||
- **Port 5001**: HTTPS access (optional)
|
||||
- Firewall rule is automatically created during installation
|
||||
|
||||
## 📋 Installation Process
|
||||
|
||||
The `install-service.ps1` script automatically:
|
||||
|
||||
1. ✅ **Creates installation directory** (`C:\Services\ResourceMonitor`)
|
||||
2. ✅ **Copies all service files**
|
||||
3. ✅ **Registers Windows Service** (`ResourceMonitorService`)
|
||||
4. ✅ **Configures auto-start** (starts with Windows)
|
||||
5. ✅ **Creates firewall rule** (port 5000)
|
||||
6. ✅ **Starts the service**
|
||||
7. ✅ **Tests web dashboard** availability
|
||||
|
||||
## 🎯 Post-Installation
|
||||
|
||||
### Service Management
|
||||
```powershell
|
||||
# Check service status
|
||||
Get-Service ResourceMonitorService
|
||||
|
||||
# Start/Stop/Restart service
|
||||
Start-Service ResourceMonitorService
|
||||
Stop-Service ResourceMonitorService
|
||||
Restart-Service ResourceMonitorService
|
||||
|
||||
# Uninstall service
|
||||
.\install-service.ps1 -Uninstall
|
||||
```
|
||||
|
||||
### Access Points
|
||||
- **Web Dashboard**: `http://VM-IP:5000`
|
||||
- **API Documentation**: `http://VM-IP:5000/swagger` (if enabled)
|
||||
- **Health Check**: `http://VM-IP:5000/api/health`
|
||||
- **Logs**: `C:\Services\ResourceMonitor\logs\`
|
||||
|
||||
### Configuration
|
||||
Edit `C:\Services\ResourceMonitor\appsettings.json` to customize:
|
||||
- Monitoring intervals
|
||||
- Alert thresholds
|
||||
- Telegram notifications
|
||||
- API settings
|
||||
- Logging levels
|
||||
|
||||
## 🔍 Troubleshooting
|
||||
|
||||
### Common Issues
|
||||
|
||||
**Service won't start**:
|
||||
```powershell
|
||||
# Check Windows Event Log
|
||||
Get-EventLog -LogName Application -Source "ResourceMonitorService" -Newest 10
|
||||
|
||||
# Check service logs
|
||||
Get-Content "C:\Services\ResourceMonitor\logs\*.txt" -Tail 50
|
||||
```
|
||||
|
||||
**Port 5000 not accessible**:
|
||||
```powershell
|
||||
# Manually create firewall rule
|
||||
New-NetFirewallRule -DisplayName "Resource Monitor Service" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow
|
||||
|
||||
# Check if port is listening
|
||||
netstat -an | findstr :5000
|
||||
```
|
||||
|
||||
**.NET Runtime not found**:
|
||||
```powershell
|
||||
# Check .NET installation
|
||||
dotnet --version
|
||||
dotnet --list-runtimes
|
||||
|
||||
# Download from: https://dotnet.microsoft.com/download/dotnet/9.0
|
||||
```
|
||||
|
||||
### Log Locations
|
||||
- **Service Logs**: `C:\Services\ResourceMonitor\logs\`
|
||||
- **Windows Event Log**: Application > ResourceMonitorService
|
||||
- **Installation Logs**: Console output during installation
|
||||
|
||||
## 📝 File Structure
|
||||
|
||||
After installation, the service directory contains:
|
||||
```
|
||||
C:\Services\ResourceMonitor\
|
||||
├── ResourceMonitorService.exe # Main service executable
|
||||
├── ResourceMonitorService.dll # Application library
|
||||
├── appsettings.json # Configuration file
|
||||
├── appsettings.Development.json # Development settings
|
||||
├── install-service.ps1 # Installation script
|
||||
├── start-service.bat # Manual start script
|
||||
├── DEPLOYMENT.txt # Deployment instructions
|
||||
├── logs\ # Log files directory
|
||||
├── wwwroot\ # Web dashboard files
|
||||
│ ├── index.html
|
||||
│ ├── css\
|
||||
│ └── js\
|
||||
└── [various .NET runtime files]
|
||||
```
|
||||
|
||||
## 🚀 Quick Reference
|
||||
|
||||
### Essential Commands
|
||||
```powershell
|
||||
# Package for deployment
|
||||
.\create-package.ps1
|
||||
|
||||
# Install on VM (as Administrator)
|
||||
.\install-service.ps1
|
||||
|
||||
# Check service status
|
||||
Get-Service ResourceMonitorService
|
||||
|
||||
# Access dashboard
|
||||
Start-Process "http://localhost:5000"
|
||||
|
||||
# Uninstall
|
||||
.\install-service.ps1 -Uninstall
|
||||
```
|
||||
|
||||
### Network Access
|
||||
Replace `localhost` with your VM's IP address to access remotely:
|
||||
- `http://192.168.1.100:5000` (Web Dashboard)
|
||||
- `http://192.168.1.100:5000/api/health` (Health Check)
|
||||
|
||||
---
|
||||
|
||||
## 🔗 Additional Resources
|
||||
|
||||
- **Main README**: `README.md`
|
||||
- **Web UI Guide**: `README_WebUI.md`
|
||||
- **Telegram Setup**: `TELEGRAM_SETUP.md`
|
||||
- **Project Repository**: [GitHub/ResourceMonitorService]
|
||||
|
||||
For support or issues, check the troubleshooting section above or review the service logs.
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+1
-23
@@ -24,7 +24,6 @@ namespace ResourceMonitorService.Models
|
||||
public MemoryUsage Memory { get; set; } = new();
|
||||
public GpuUsage GPU { get; set; } = new();
|
||||
public List<DiskUsage> Disks { get; set; } = new();
|
||||
public NetworkUsage Network { get; set; } = new();
|
||||
public List<ProcessInfo> TopProcesses { get; set; } = new();
|
||||
public TemperatureInfo Temperature { get; set; } = new();
|
||||
public GameInfo? RunningGame { get; set; }
|
||||
@@ -84,30 +83,9 @@ namespace ResourceMonitorService.Models
|
||||
public long WriteOperations { get; set; }
|
||||
}
|
||||
|
||||
public class NetworkUsage
|
||||
{
|
||||
public float UploadSpeed { get; set; } // MB/s
|
||||
public float DownloadSpeed { get; set; } // MB/s
|
||||
public ulong BytesSent { get; set; }
|
||||
public ulong BytesReceived { get; set; }
|
||||
public List<NetworkAdapter> Adapters { get; set; } = new();
|
||||
}
|
||||
|
||||
public class NetworkAdapter
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
public string Description { get; set; } = string.Empty;
|
||||
public bool IsOperational { get; set; }
|
||||
public long Speed { get; set; }
|
||||
public float UploadSpeed { get; set; }
|
||||
public float DownloadSpeed { get; set; }
|
||||
public string IPAddress { get; set; } = string.Empty;
|
||||
public string MACAddress { get; set; } = string.Empty;
|
||||
}
|
||||
|
||||
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; }
|
||||
|
||||
@@ -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<Startup>();
|
||||
webBuilder.UseUrls("http://localhost:5000", "https://localhost:5001");
|
||||
})
|
||||
.ConfigureServices((hostContext, services) =>
|
||||
{
|
||||
// Bind configuration sections
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
"ResourceMonitorService": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"applicationUrl": "http://localhost:5000;https://localhost:5001",
|
||||
"environmentVariables": {
|
||||
"DOTNET_ENVIRONMENT": "Development"
|
||||
}
|
||||
|
||||
@@ -1,9 +1,33 @@
|
||||
# Resource Monitor Service for Unraid VM
|
||||
# Resource Monitor Service
|
||||
|
||||
A comprehensive system monitoring service specifically designed for Windows VMs running on Unraid servers. This service provides real-time monitoring of CPU, memory, GPU, disk, network, and system resources through a RESTful API.
|
||||
A comprehensive system monitoring service with a modern web dashboard for real-time resource monitoring. Originally designed for Windows VMs running on Unraid servers, now featuring a responsive web interface for easy monitoring and management.
|
||||
|
||||
## 🌟 New: Web Dashboard
|
||||
|
||||
Access the interactive web dashboard at `http://localhost:5000` featuring:
|
||||
|
||||
- **Real-time Monitoring**: Live updates every 15 seconds via SignalR
|
||||
- **Responsive Design**: Mobile-friendly interface built with Tailwind CSS
|
||||
- **Interactive Controls**: Toggle auto-refresh, show/hide sections, manual refresh
|
||||
- **Game Detection**: Prominent game monitoring with process termination
|
||||
- **Process Management**: View and terminate top processes
|
||||
- **System Details**: Comprehensive system information and disk usage
|
||||
- **Performance Charts**: Historical CPU and memory usage graphs
|
||||
- **API Documentation**: Built-in Swagger/OpenAPI interface at `/swagger`
|
||||
|
||||
## 🚀 Features
|
||||
|
||||
### Web Dashboard
|
||||
- **Modern Interface**: Clean, responsive design with dark/light themes
|
||||
- **Real-time Updates**: SignalR-powered live data updates
|
||||
- **Auto-refresh Control**: Toggle between automatic and manual refresh modes
|
||||
- **Game Detection Section**: Monitor running games with termination capability
|
||||
- **Process Management**: View top processes with one-click termination
|
||||
- **System Information**: Detailed hardware and software information
|
||||
- **Disk Usage Visualization**: Visual disk space utilization
|
||||
- **Performance Charts**: Historical data visualization with Chart.js
|
||||
- **Mobile Responsive**: Works seamlessly on phones, tablets, and desktop
|
||||
|
||||
### Core Monitoring
|
||||
- **CPU Monitoring**: Per-core usage, frequency, temperature, and throttling detection
|
||||
- **Memory Monitoring**: RAM usage, available memory, committed memory, and paging
|
||||
@@ -23,29 +47,73 @@ A comprehensive system monitoring service specifically designed for Windows VMs
|
||||
- **Process Management**: View top processes with CPU/memory percentages, terminate processes via API
|
||||
- **Smart Alerting**: Duration-based alerting to prevent false positives
|
||||
- **Telegram Bot Integration**: Real-time alerts via Telegram bot with customizable notifications
|
||||
- **System Control**: Remote shutdown/restart capabilities
|
||||
- **System Control**: Remote shutdown/restart capabilities with timer support (hidden feature)
|
||||
|
||||
### 🔐 Hidden System Control Feature
|
||||
|
||||
For security reasons, the system control feature is hidden by default. To access it:
|
||||
|
||||
**Mobile Device Access Methods:**
|
||||
1. **Long press** (2 seconds) on the "Resource Monitor" title in the navigation bar
|
||||
2. **Triple tap** quickly on the "Resource Monitor" title
|
||||
3. **Five quick taps** on the chart icon (📊) next to the title
|
||||
|
||||
**Desktop Access Methods:**
|
||||
1. **Triple-click** on the "Resource Monitor" title in the navigation bar
|
||||
2. **Keyboard shortcut**: `Ctrl + Shift + P`
|
||||
3. **Long press** (2 seconds) on the title with mouse
|
||||
|
||||
**Features:**
|
||||
- **Shutdown**: Graceful or forced system shutdown
|
||||
- **Restart**: System restart with optional force
|
||||
- **Timer Support**: Delay execution from 0 to 86400 seconds (24 hours) - Default: 15 seconds
|
||||
- **Cancel Function**: Cancel any pending shutdown/restart (similar to `shutdown -a`)
|
||||
- **Safety Confirmations**: Multiple confirmation dialogs before execution
|
||||
- **Auto-lock**: System control auto-locks after 30 seconds for security
|
||||
|
||||
**Usage:**
|
||||
1. Activate the system control button using one of the methods above
|
||||
2. Look for the red pulsing "System" button that appears in the navigation
|
||||
3. Tap/click the "System" button to open the control panel
|
||||
4. Adjust timer if needed (defaults to 15 seconds for safety)
|
||||
5. Choose shutdown, restart, or cancel any pending operation
|
||||
6. Confirm the action
|
||||
|
||||
📱 **Mobile Tips:**
|
||||
- Device will vibrate when system control is unlocked (if supported)
|
||||
- Touch-friendly interface optimized for mobile screens
|
||||
- All gestures work with touchscreens
|
||||
|
||||
⚠️ **Warning**: Use this feature with extreme caution as it will shut down or restart the entire system.
|
||||
- **Health Monitoring**: Comprehensive health checks and uptime tracking
|
||||
- **Real-time Metrics**: CPU usage calculation and memory percentage tracking for processes
|
||||
|
||||
## 📡 API Endpoints
|
||||
|
||||
The service runs on `http://localhost:5000` by default and provides the following endpoints:
|
||||
The service runs a web server on `http://localhost:5000` providing:
|
||||
|
||||
### Web Interface
|
||||
- `GET /` - **Main Dashboard** - Interactive web interface for monitoring
|
||||
- `GET /swagger` - **API Documentation** - Interactive API explorer and documentation
|
||||
|
||||
### REST API
|
||||
All API endpoints are available at `http://localhost:5000/api/[endpoint]`:
|
||||
|
||||
### System Information
|
||||
- `GET /api/system-info` - Complete system information including VM details
|
||||
- `GET /api/vm/info` - VM-specific information (hypervisor, uptime, etc.)
|
||||
- `GET /api/health` - Service health status and monitoring capabilities
|
||||
- `GET /api/metrics` - Service metrics and performance overview
|
||||
- `GET /api/config` - Current configuration settings
|
||||
- `GET /api/resource/usage` - **Complete resource overview** - All monitoring data in one call
|
||||
- `GET /api/resource/system-info` - Complete system information including VM details
|
||||
- `GET /api/resource/cpu` - Detailed CPU metrics with per-core data
|
||||
- `GET /api/resource/memory` - Memory utilization and statistics
|
||||
- `GET /api/resource/gpu` - NVIDIA GPU usage, memory, temperature, fan speed, and power consumption
|
||||
- `GET /api/resource/disks` - Disk I/O and space usage for all drives
|
||||
- `GET /api/resource/network` - Network interface statistics
|
||||
- `GET /api/resource/processes?count=10` - Top processes by CPU/memory usage with percentage data
|
||||
|
||||
### Resource Monitoring
|
||||
- `GET /api/resource-usage` - Complete resource usage overview
|
||||
- `GET /api/cpu-usage` - Detailed CPU metrics with per-core data
|
||||
- `GET /api/memory-usage` - Memory utilization and statistics
|
||||
- `GET /api/gpu-usage` - NVIDIA GPU usage, memory, temperature, fan speed, and power consumption
|
||||
- `GET /api/disk-usage` - Disk I/O and space usage for all drives
|
||||
- `GET /api/network-usage` - Network interface statistics
|
||||
- `GET /api/top-processes?count=10` - Top processes by CPU/memory usage with percentage data
|
||||
### Process Management
|
||||
- `POST /api/resource/kill-process/{processId}` - **Terminate Process** - End any process by ID
|
||||
|
||||
### Real-time Updates
|
||||
- **SignalR Hub**: `/resourceHub` - Real-time data updates every 15 seconds
|
||||
|
||||
### Game Detection
|
||||
- `GET /api/current-game` - Currently running game information
|
||||
@@ -76,25 +144,97 @@ The service runs on `http://localhost:5000` by default and provides the followin
|
||||
|
||||
## 🛠️ Installation & Usage
|
||||
|
||||
### Option 1: Console Application (Development/Testing)
|
||||
### Option 1: Web Dashboard (Recommended)
|
||||
```powershell
|
||||
cd C:\Work\DEV\ResourceUsageAPI
|
||||
dotnet run --configuration Release
|
||||
```
|
||||
Then open your browser to `http://localhost:5000` for the interactive dashboard.
|
||||
|
||||
### Option 2: Windows Service (Production)
|
||||
```powershell
|
||||
# Run as Administrator
|
||||
cd C:\Work\DEV\ResourceUsageAPI\publish
|
||||
.\install-service.bat
|
||||
cd C:\Work\DEV\ResourceUsageAPI
|
||||
.\install-service.ps1
|
||||
```
|
||||
|
||||
### Option 3: Standalone Executable
|
||||
### Option 3: Linux Service (if running on Linux)
|
||||
```bash
|
||||
cd /path/to/ResourceUsageAPI
|
||||
chmod +x install-service.sh
|
||||
sudo ./install-service.sh
|
||||
```
|
||||
|
||||
### Option 4: Standalone Executable
|
||||
```powershell
|
||||
cd C:\Work\DEV\ResourceUsageAPI\publish
|
||||
cd C:\Work\DEV\ResourceUsageAPI
|
||||
dotnet build --configuration Release
|
||||
dotnet publish --configuration Release
|
||||
cd bin\Release\net9.0-windows\publish
|
||||
.\ResourceMonitorService.exe
|
||||
```
|
||||
|
||||
## 🎮 Web Dashboard Features
|
||||
|
||||
### Dashboard Overview
|
||||
- **Resource Cards**: CPU, Memory, GPU, and Network usage with visual progress bars
|
||||
- **Game Detection**: Prominent section showing currently running games
|
||||
- **Auto-refresh Toggle**: Control automatic updates (15-second intervals)
|
||||
- **Manual Refresh**: Force immediate data updates
|
||||
- **Responsive Design**: Works on desktop, tablet, and mobile devices
|
||||
|
||||
### Interactive Sections
|
||||
- **Processes**: View and terminate top CPU/memory consuming processes
|
||||
- **Details**: System information, disk usage, and performance charts
|
||||
- **Game Management**: Monitor and terminate running games
|
||||
- **Real-time Charts**: Historical CPU and memory usage visualization
|
||||
|
||||
### Controls
|
||||
- **Auto: ON/OFF** - Toggle automatic data updates
|
||||
- **Processes** - Show/hide process management table
|
||||
- **Details** - Show/hide system information and charts
|
||||
- **Refresh** - Manually update all data immediately
|
||||
|
||||
## 📊 Service Management
|
||||
|
||||
### Starting and Stopping the Service
|
||||
|
||||
```powershell
|
||||
# Start the service
|
||||
Start-Service "ResourceMonitorService"
|
||||
|
||||
# Stop the service
|
||||
Stop-Service "ResourceMonitorService"
|
||||
|
||||
# Get service status
|
||||
Get-Service "ResourceMonitorService"
|
||||
|
||||
# Restart the service
|
||||
Restart-Service "ResourceMonitorService"
|
||||
```
|
||||
|
||||
### Development Mode
|
||||
|
||||
For development and testing:
|
||||
|
||||
```powershell
|
||||
# Run in development mode with hot reload
|
||||
dotnet run --environment Development
|
||||
|
||||
# Access the dashboard at http://localhost:5000
|
||||
# Access Swagger API documentation at http://localhost:5000/swagger
|
||||
```
|
||||
|
||||
### Troubleshooting
|
||||
|
||||
- **Service won't start**: Check the logs in the `logs/` directory
|
||||
- **No GPU data**: Make sure you have an NVIDIA GPU and drivers installed
|
||||
- **High CPU usage**: Adjust monitoring intervals in `appsettings.json`
|
||||
- **Web dashboard not accessible**: Verify firewall settings and ensure port 5000 is available
|
||||
- **Game detection issues**: Check if games are running from standard installation directories
|
||||
- **API errors**: Verify endpoints using Swagger documentation at `/swagger`
|
||||
- **Performance issues**: Consider increasing `UpdateIntervalMs` in configuration
|
||||
|
||||
## ⚙️ Configuration
|
||||
|
||||
Configuration is managed through `appsettings.json`:
|
||||
@@ -102,7 +242,7 @@ Configuration is managed through `appsettings.json`:
|
||||
```json
|
||||
{
|
||||
"MonitoringSettings": {
|
||||
"UpdateIntervalMs": 5000,
|
||||
"UpdateIntervalMs": 15000,
|
||||
"EnableGpuMonitoring": true,
|
||||
"EnableDiskMonitoring": true,
|
||||
"EnableNetworkMonitoring": true,
|
||||
@@ -123,18 +263,15 @@ Configuration is managed through `appsettings.json`:
|
||||
"E:\\Games"
|
||||
]
|
||||
},
|
||||
"AlertThresholds": {
|
||||
"CpuUsageThreshold": 80.0,
|
||||
"MemoryUsageThreshold": 85.0,
|
||||
"GpuUsageThreshold": 90.0,
|
||||
"DiskUsageThreshold": 90.0,
|
||||
"TemperatureThreshold": 80.0,
|
||||
"AlertDurationSeconds": 30
|
||||
},
|
||||
"ApiSettings": {
|
||||
"RequireApiKey": false,
|
||||
"AllowedOrigins": ["http://localhost:4200", "http://unraid:4200"],
|
||||
"BasePath": "/api"
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Url": "http://localhost:5000"
|
||||
},
|
||||
"Https": {
|
||||
"Url": "https://localhost:5001"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -331,68 +468,40 @@ The service supports real-time alert notifications via Telegram bot. To set up T
|
||||
## 🔧 PowerShell Usage Examples
|
||||
|
||||
```powershell
|
||||
# Get system health
|
||||
$health = Invoke-RestMethod -Uri "http://localhost:5000/api/health"
|
||||
Write-Host "System Status: $($health.status)"
|
||||
# Access the web dashboard
|
||||
Start-Process "http://localhost:5000"
|
||||
|
||||
# Get CPU usage
|
||||
$cpu = Invoke-RestMethod -Uri "http://localhost:5000/api/cpu-usage"
|
||||
Write-Host "CPU Usage: $($cpu.usage)%"
|
||||
# Get complete resource overview
|
||||
$resources = Invoke-RestMethod -Uri "http://localhost:5000/api/resource/usage"
|
||||
Write-Host "CPU: $($resources.cpu.usage.ToString('F1'))%"
|
||||
Write-Host "Memory: $($resources.memory.usagePercentage.ToString('F1'))%"
|
||||
Write-Host "GPU: $($resources.gpu.usage)%"
|
||||
|
||||
# Get GPU usage
|
||||
$gpu = Invoke-RestMethod -Uri "http://localhost:5000/api/gpu-usage"
|
||||
if ($gpu.isAvailable) {
|
||||
Write-Host "GPU: $($gpu.name)"
|
||||
Write-Host "GPU Usage: $($gpu.usage)%"
|
||||
Write-Host "GPU Memory: $([math]::Round($gpu.memoryUsed / 1GB, 2))GB / $([math]::Round($gpu.memoryTotal / 1GB, 2))GB ($($gpu.memoryUsage)%)"
|
||||
Write-Host "GPU Temperature: $($gpu.temperature)°C"
|
||||
} else {
|
||||
Write-Host "GPU not available: $($gpu.error)"
|
||||
# Get system information
|
||||
$systemInfo = Invoke-RestMethod -Uri "http://localhost:5000/api/resource/system-info"
|
||||
Write-Host "Machine: $($systemInfo.machineName)"
|
||||
Write-Host "OS: $($systemInfo.osVersion)"
|
||||
Write-Host "CPU: $($systemInfo.cpuName)"
|
||||
|
||||
# Get disk usage
|
||||
$disks = Invoke-RestMethod -Uri "http://localhost:5000/api/resource/disks"
|
||||
foreach ($disk in $disks) {
|
||||
$freeGB = [math]::Round($disk.freeSpace / 1GB, 1)
|
||||
$totalGB = [math]::Round($disk.totalSize / 1GB, 1)
|
||||
Write-Host "$($disk.driveLetter): $freeGB GB free of $totalGB GB"
|
||||
}
|
||||
|
||||
# Get current game (enhanced with root folder detection)
|
||||
$game = Invoke-RestMethod -Uri "http://localhost:5000/api/current-game"
|
||||
if ($game) {
|
||||
Write-Host "Currently playing: $($game.gameName)"
|
||||
Write-Host "Platform: $($game.platform)"
|
||||
Write-Host "Executable: $($game.executableName)"
|
||||
if ($game.isFullscreen) {
|
||||
Write-Host "Running in fullscreen mode"
|
||||
}
|
||||
} else {
|
||||
Write-Host "No game currently detected"
|
||||
}
|
||||
|
||||
# Get all detected games
|
||||
$allGames = Invoke-RestMethod -Uri "http://localhost:5000/api/all-games"
|
||||
Write-Host "Detected games on system:"
|
||||
foreach ($gameItem in $allGames) {
|
||||
Write-Host " $($gameItem.gameName) ($($gameItem.platform)) - Memory: $([math]::Round($gameItem.memoryUsage / 1MB, 0))MB"
|
||||
}
|
||||
|
||||
# Get top processes by CPU usage
|
||||
$processes = Invoke-RestMethod -Uri "http://localhost:5000/api/top-processes?count=5"
|
||||
# Get top processes
|
||||
$processes = Invoke-RestMethod -Uri "http://localhost:5000/api/resource/processes?count=5"
|
||||
Write-Host "Top 5 processes by CPU usage:"
|
||||
foreach ($proc in $processes.value) {
|
||||
Write-Host " $($proc.name): $($proc.cpuUsage.ToString('F2'))% CPU, $($proc.memoryUsagePercentage.ToString('F2'))% Memory"
|
||||
foreach ($proc in $processes) {
|
||||
if ($proc.cpuUsage) {
|
||||
Write-Host " $($proc.name): $($proc.cpuUsage.ToString('F1'))% CPU"
|
||||
}
|
||||
}
|
||||
|
||||
# Terminate a process (example - be careful!)
|
||||
$killRequest = @{
|
||||
ProcessId = 1234
|
||||
Force = $false
|
||||
} | ConvertTo-Json
|
||||
|
||||
# Invoke-RestMethod -Uri "http://localhost:5000/api/process/kill" -Method Post -Body $killRequest -ContentType "application/json"
|
||||
|
||||
# Shutdown system with 60-second delay
|
||||
$shutdownRequest = @{
|
||||
Action = "shutdown"
|
||||
DelaySeconds = 60
|
||||
Message = "Scheduled maintenance shutdown"
|
||||
} | ConvertTo-Json
|
||||
|
||||
Invoke-RestMethod -Uri "http://localhost:5000/api/system/shutdown" -Method Post -Body $shutdownRequest -ContentType "application/json"
|
||||
# Invoke-RestMethod -Uri "http://localhost:5000/api/resource/kill-process/1234" -Method Post
|
||||
```
|
||||
|
||||
## 🚨 Known Warnings (Non-Critical)
|
||||
@@ -435,7 +544,7 @@ The service uses Serilog for structured logging:
|
||||
|
||||
## 📈 Performance
|
||||
|
||||
- Lightweight background monitoring (5-second intervals by default)
|
||||
- Lightweight background monitoring (15-second intervals by default)
|
||||
- Efficient memory usage with smart caching and cleanup of old process data
|
||||
- Non-blocking async operations
|
||||
- Real-time CPU usage calculation for individual processes
|
||||
|
||||
+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>
|
||||
<TargetFramework>net9.0-windows</TargetFramework>
|
||||
@@ -18,5 +18,7 @@
|
||||
<PackageReference Include="Serilog.Sinks.Console" Version="5.0.1" />
|
||||
<PackageReference Include="Serilog.Sinks.File" Version="5.0.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>
|
||||
</Project>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -14,7 +14,6 @@ namespace ResourceMonitorService.Services
|
||||
Task<MemoryUsage> GetMemoryUsageAsync();
|
||||
Task<GpuUsage> GetGpuUsageAsync();
|
||||
Task<List<DiskUsage>> GetDiskUsageAsync();
|
||||
Task<NetworkUsage> GetNetworkUsageAsync();
|
||||
Task<List<ProcessInfo>> GetTopProcessesAsync(int count = 10);
|
||||
}
|
||||
|
||||
@@ -23,8 +22,6 @@ namespace ResourceMonitorService.Services
|
||||
private readonly ILogger<ResourceMonitorService> _logger;
|
||||
private readonly MonitoringSettings _settings;
|
||||
private readonly Dictionary<string, PerformanceCounter> _counters;
|
||||
private readonly Dictionary<string, long> _previousNetworkBytes;
|
||||
private readonly Dictionary<string, DateTime> _previousNetworkTime;
|
||||
private readonly Dictionary<string, long> _previousDiskBytes;
|
||||
private readonly Dictionary<string, DateTime> _previousDiskTime;
|
||||
private readonly Dictionary<string, int> _errorCounts;
|
||||
@@ -35,8 +32,6 @@ namespace ResourceMonitorService.Services
|
||||
_logger = logger;
|
||||
_settings = settings.Value;
|
||||
_counters = new Dictionary<string, PerformanceCounter>();
|
||||
_previousNetworkBytes = new Dictionary<string, long>();
|
||||
_previousNetworkTime = new Dictionary<string, DateTime>();
|
||||
_previousDiskBytes = new Dictionary<string, long>();
|
||||
_previousDiskTime = new Dictionary<string, DateTime>();
|
||||
_errorCounts = new Dictionary<string, int>();
|
||||
@@ -52,12 +47,6 @@ namespace ResourceMonitorService.Services
|
||||
_counters["cpu"] = new PerformanceCounter("Processor", "% Processor Time", "_Total");
|
||||
_counters["memory_available"] = new PerformanceCounter("Memory", "Available MBytes");
|
||||
|
||||
if (_settings.EnableNetworkMonitoring)
|
||||
{
|
||||
_counters["network_bytes_sent"] = new PerformanceCounter("Network Interface", "Bytes Sent/sec", "*");
|
||||
_counters["network_bytes_received"] = new PerformanceCounter("Network Interface", "Bytes Received/sec", "*");
|
||||
}
|
||||
|
||||
if (_settings.EnableDiskMonitoring)
|
||||
{
|
||||
_counters["disk_read"] = new PerformanceCounter("PhysicalDisk", "Disk Read Bytes/sec", "_Total");
|
||||
@@ -119,9 +108,6 @@ namespace ResourceMonitorService.Services
|
||||
if (_settings.EnableDiskMonitoring)
|
||||
tasks.Add(Task.Run(async () => await GetDiskUsageAsync()));
|
||||
|
||||
if (_settings.EnableNetworkMonitoring)
|
||||
tasks.Add(Task.Run(async () => await GetNetworkUsageAsync()));
|
||||
|
||||
if (_settings.EnableProcessMonitoring)
|
||||
tasks.Add(Task.Run(async () => await GetTopProcessesAsync(_settings.MaxProcessesToTrack)));
|
||||
|
||||
@@ -134,7 +120,6 @@ namespace ResourceMonitorService.Services
|
||||
Memory = await GetMemoryUsageAsync(),
|
||||
GPU = _settings.EnableGpuMonitoring ? await GetGpuUsageAsync() : new GpuUsage(),
|
||||
Disks = _settings.EnableDiskMonitoring ? await GetDiskUsageAsync() : new List<DiskUsage>(),
|
||||
Network = _settings.EnableNetworkMonitoring ? await GetNetworkUsageAsync() : new NetworkUsage(),
|
||||
TopProcesses = _settings.EnableProcessMonitoring ? await GetTopProcessesAsync(_settings.MaxProcessesToTrack) : new List<ProcessInfo>(),
|
||||
Temperature = _settings.EnableTemperatureMonitoring ? await GetTemperatureInfoAsync() : new TemperatureInfo()
|
||||
};
|
||||
@@ -654,91 +639,6 @@ namespace ResourceMonitorService.Services
|
||||
return false;
|
||||
}
|
||||
|
||||
public async Task<NetworkUsage> GetNetworkUsageAsync()
|
||||
{
|
||||
try
|
||||
{
|
||||
return await Task.Run(() =>
|
||||
{
|
||||
var networkUsage = new NetworkUsage();
|
||||
var adapters = new List<NetworkAdapter>();
|
||||
|
||||
try
|
||||
{
|
||||
#pragma warning disable CA1416 // Validate platform compatibility
|
||||
using var searcher = new ManagementObjectSearcher("SELECT * FROM Win32_PerfRawData_Tcpip_NetworkInterface WHERE Name != 'Loopback'");
|
||||
using var collection = searcher.Get();
|
||||
|
||||
foreach (ManagementObject obj in collection)
|
||||
{
|
||||
var name = obj["Name"]?.ToString() ?? "";
|
||||
if (name.Contains("Loopback") || name.Contains("Isatap") || name.Contains("Teredo"))
|
||||
continue;
|
||||
|
||||
var bytesSent = Convert.ToInt64(obj["BytesSentPerSec"] ?? 0);
|
||||
var bytesReceived = Convert.ToInt64(obj["BytesReceivedPerSec"] ?? 0);
|
||||
var timestamp = DateTime.Now;
|
||||
|
||||
var adapter = new NetworkAdapter
|
||||
{
|
||||
Name = name,
|
||||
IsOperational = true
|
||||
};
|
||||
|
||||
// Calculate speeds if we have previous data
|
||||
var key = $"{name}_sent";
|
||||
if (_previousNetworkBytes.ContainsKey(key) && _previousNetworkTime.ContainsKey(key))
|
||||
{
|
||||
var timeDiff = (timestamp - _previousNetworkTime[key]).TotalSeconds;
|
||||
if (timeDiff > 0)
|
||||
{
|
||||
var bytesDiff = bytesSent - _previousNetworkBytes[key];
|
||||
adapter.UploadSpeed = (float)(bytesDiff / timeDiff / (1024 * 1024)); // MB/s
|
||||
networkUsage.UploadSpeed += adapter.UploadSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
key = $"{name}_received";
|
||||
if (_previousNetworkBytes.ContainsKey(key) && _previousNetworkTime.ContainsKey(key))
|
||||
{
|
||||
var timeDiff = (timestamp - _previousNetworkTime[key]).TotalSeconds;
|
||||
if (timeDiff > 0)
|
||||
{
|
||||
var bytesDiff = bytesReceived - _previousNetworkBytes[key];
|
||||
adapter.DownloadSpeed = (float)(bytesDiff / timeDiff / (1024 * 1024)); // MB/s
|
||||
networkUsage.DownloadSpeed += adapter.DownloadSpeed;
|
||||
}
|
||||
}
|
||||
|
||||
// Store current values for next calculation
|
||||
_previousNetworkBytes[$"{name}_sent"] = bytesSent;
|
||||
_previousNetworkBytes[$"{name}_received"] = bytesReceived;
|
||||
_previousNetworkTime[$"{name}_sent"] = timestamp;
|
||||
_previousNetworkTime[$"{name}_received"] = timestamp;
|
||||
|
||||
networkUsage.BytesSent += (ulong)bytesSent;
|
||||
networkUsage.BytesReceived += (ulong)bytesReceived;
|
||||
|
||||
adapters.Add(adapter);
|
||||
}
|
||||
#pragma warning restore CA1416 // Validate platform compatibility
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogWarning(ex, "Could not get network performance data");
|
||||
}
|
||||
|
||||
networkUsage.Adapters = adapters;
|
||||
return networkUsage;
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
_logger.LogError(ex, "Error getting network usage");
|
||||
return new NetworkUsage();
|
||||
}
|
||||
}
|
||||
|
||||
public async Task<List<ProcessInfo>> GetTopProcessesAsync(int count = 10)
|
||||
{
|
||||
try
|
||||
@@ -770,7 +670,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 +718,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)
|
||||
|
||||
+56
-15
@@ -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>("/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.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<Worker> _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<Worker> logger,
|
||||
IHostApplicationLifetime lifetime,
|
||||
ISystemInfoService systemInfoService,
|
||||
IResourceMonitorService resourceMonitorService,
|
||||
IGameDetectionService gameDetectionService,
|
||||
IAlertService alertService,
|
||||
ITelegramNotificationService telegramService,
|
||||
IOptions<ApiSettings> apiSettings,
|
||||
IOptions<MonitoringSettings> monitoringSettings)
|
||||
IOptions<MonitoringSettings> 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<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);
|
||||
_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<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++;
|
||||
|
||||
// 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,
|
||||
|
||||
+5
-2
@@ -12,7 +12,10 @@
|
||||
"Kestrel": {
|
||||
"Endpoints": {
|
||||
"Http": {
|
||||
"Url": "http://*:2414"
|
||||
"Url": "http://*:5000"
|
||||
},
|
||||
"Https": {
|
||||
"Url": "https://*:5001"
|
||||
}
|
||||
}
|
||||
},
|
||||
@@ -29,7 +32,7 @@
|
||||
"BasePath": "/api"
|
||||
},
|
||||
"MonitoringSettings": {
|
||||
"UpdateIntervalMs": 5000,
|
||||
"UpdateIntervalMs": 120000,
|
||||
"DataRetentionDays": 7,
|
||||
"EnableGpuMonitoring": true,
|
||||
"EnableDiskMonitoring": true,
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
# Resource Monitor Service - Complete Build and Deploy Script
|
||||
# Builds, packages, and optionally deploys to VM in one step
|
||||
|
||||
param(
|
||||
[string]$VMAddress,
|
||||
[string]$Version = "2.1.0",
|
||||
[switch]$DeployToVM,
|
||||
[switch]$UseWinRM,
|
||||
[switch]$CopyOnly
|
||||
)
|
||||
|
||||
Write-Host "=== Resource Monitor Service - Build & Deploy ===" -ForegroundColor Cyan
|
||||
Write-Host
|
||||
|
||||
# Step 1: Package the release
|
||||
Write-Host "Step 1: Building and packaging release..." -ForegroundColor Green
|
||||
try {
|
||||
& ".\package-release.ps1" -Version $Version
|
||||
if ($LASTEXITCODE -ne 0) {
|
||||
throw "Packaging failed"
|
||||
}
|
||||
} catch {
|
||||
Write-Host "✗ Packaging failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Step 2: Deploy if requested
|
||||
if ($DeployToVM -and $VMAddress) {
|
||||
Write-Host
|
||||
Write-Host "Step 2: Deploying to VM..." -ForegroundColor Green
|
||||
|
||||
$deployArgs = @("-VMAddress", $VMAddress)
|
||||
if ($UseWinRM) { $deployArgs += "-UseWinRM" }
|
||||
if ($CopyOnly) { $deployArgs += "-CopyOnly" }
|
||||
|
||||
try {
|
||||
& ".\deploy-to-vm.ps1" @deployArgs
|
||||
} catch {
|
||||
Write-Host "✗ Deployment failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
} else {
|
||||
Write-Host
|
||||
Write-Host "Package created successfully!" -ForegroundColor Green
|
||||
Write-Host "To deploy to VM, run:" -ForegroundColor Yellow
|
||||
Write-Host " .\deploy-to-vm.ps1 -VMAddress YOUR_VM_IP" -ForegroundColor Cyan
|
||||
Write-Host " .\deploy-to-vm.ps1 -VMAddress YOUR_VM_IP -UseWinRM" -ForegroundColor Cyan
|
||||
}
|
||||
|
||||
Write-Host
|
||||
Write-Host "=== Build & Deploy Complete ===" -ForegroundColor Cyan
|
||||
@@ -0,0 +1,53 @@
|
||||
# Simple Release Packaging Script
|
||||
param(
|
||||
[string]$Version = "2.1.0"
|
||||
)
|
||||
|
||||
$PACKAGE_NAME = "ResourceMonitorService-v$Version-$(Get-Date -Format 'yyyyMMdd-HHmm')"
|
||||
$TEMP_PATH = ".\temp-release"
|
||||
$OUTPUT_PATH = ".\release-packages"
|
||||
|
||||
Write-Host "Creating release package: $PACKAGE_NAME" -ForegroundColor Green
|
||||
|
||||
# Clean up
|
||||
if (Test-Path $TEMP_PATH) { Remove-Item $TEMP_PATH -Recurse -Force }
|
||||
if (-not (Test-Path $OUTPUT_PATH)) { New-Item -ItemType Directory -Path $OUTPUT_PATH -Force | Out-Null }
|
||||
New-Item -ItemType Directory -Path $TEMP_PATH -Force | Out-Null
|
||||
|
||||
# Build release
|
||||
Write-Host "Building release..." -ForegroundColor Yellow
|
||||
dotnet publish --configuration Release --output $TEMP_PATH --verbosity minimal
|
||||
|
||||
# Copy additional files
|
||||
Start-Sleep -Seconds 2
|
||||
Copy-Item "install-service.ps1" $TEMP_PATH
|
||||
Copy-Item "start-service.bat" $TEMP_PATH
|
||||
Copy-Item "appsettings.json" $TEMP_PATH
|
||||
Copy-Item "README.md" $TEMP_PATH -ErrorAction SilentlyContinue
|
||||
|
||||
# Create deployment readme
|
||||
@"
|
||||
# ResourceMonitorService v$Version Deployment
|
||||
|
||||
## Installation
|
||||
1. Extract ZIP to temporary folder
|
||||
2. Run PowerShell as Administrator
|
||||
3. Execute: .\install-service.ps1
|
||||
|
||||
## Access
|
||||
- Web Dashboard: http://localhost:5000
|
||||
- API Health: http://localhost:5000/api/health
|
||||
|
||||
Generated: $(Get-Date)
|
||||
"@ | Out-File "$TEMP_PATH\DEPLOYMENT.txt" -Encoding UTF8
|
||||
|
||||
# Create ZIP
|
||||
Start-Sleep -Seconds 2
|
||||
$zipPath = "$OUTPUT_PATH\$PACKAGE_NAME.zip"
|
||||
Compress-Archive -Path "$TEMP_PATH\*" -DestinationPath $zipPath -Force
|
||||
|
||||
# Clean up
|
||||
Remove-Item $TEMP_PATH -Recurse -Force
|
||||
|
||||
$size = [math]::Round((Get-Item $zipPath).Length / 1MB, 2)
|
||||
Write-Host "Package created: $zipPath ($size MB)" -ForegroundColor Green
|
||||
@@ -0,0 +1,138 @@
|
||||
# Resource Monitor Service - Remote VM Deployment Script
|
||||
# Deploys the packaged service to a remote VM
|
||||
|
||||
param(
|
||||
[Parameter(Mandatory=$true)]
|
||||
[string]$VMAddress,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$Username,
|
||||
|
||||
[Parameter(Mandatory=$false)]
|
||||
[string]$PackagePath,
|
||||
|
||||
[switch]$UseWinRM,
|
||||
[switch]$CopyOnly
|
||||
)
|
||||
|
||||
Write-Host "=== Resource Monitor Service - Remote VM Deployment ===" -ForegroundColor Cyan
|
||||
Write-Host
|
||||
|
||||
# Find the latest package if not specified
|
||||
if (-not $PackagePath) {
|
||||
$latestPackage = Get-ChildItem -Path ".\release-packages\ResourceMonitorService-v*.zip" | Sort-Object LastWriteTime -Descending | Select-Object -First 1
|
||||
if ($latestPackage) {
|
||||
$PackagePath = $latestPackage.FullName
|
||||
Write-Host "Using latest package: $($latestPackage.Name)" -ForegroundColor Green
|
||||
} else {
|
||||
Write-Host "✗ No packages found. Run .\package-release.ps1 first" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
|
||||
if (-not (Test-Path $PackagePath)) {
|
||||
Write-Host "✗ Package not found: $PackagePath" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
$packageName = [System.IO.Path]::GetFileNameWithoutExtension($PackagePath)
|
||||
Write-Host "Package: $packageName" -ForegroundColor White
|
||||
Write-Host "Target VM: $VMAddress" -ForegroundColor White
|
||||
Write-Host
|
||||
|
||||
if ($UseWinRM) {
|
||||
Write-Host "Using WinRM for deployment..." -ForegroundColor Yellow
|
||||
|
||||
# Test WinRM connection
|
||||
Write-Host "Testing WinRM connection to $VMAddress..." -ForegroundColor Yellow
|
||||
try {
|
||||
$session = New-PSSession -ComputerName $VMAddress -Credential (Get-Credential -Message "Enter credentials for $VMAddress")
|
||||
Write-Host "✓ WinRM connection successful" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "✗ WinRM connection failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||
Write-Host "Make sure WinRM is enabled on the target VM:" -ForegroundColor Yellow
|
||||
Write-Host " winrm quickconfig" -ForegroundColor Gray
|
||||
Write-Host " Enable-PSRemoting -Force" -ForegroundColor Gray
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Copy package to VM
|
||||
Write-Host "Copying package to VM..." -ForegroundColor Yellow
|
||||
try {
|
||||
$remoteTemp = "C:\Temp\$packageName"
|
||||
Invoke-Command -Session $session -ScriptBlock {
|
||||
param($remotePath)
|
||||
if (Test-Path $remotePath) { Remove-Item $remotePath -Recurse -Force }
|
||||
New-Item -ItemType Directory -Path $remotePath -Force | Out-Null
|
||||
} -ArgumentList $remoteTemp
|
||||
|
||||
Copy-Item -Path $PackagePath -Destination "$remoteTemp.zip" -ToSession $session
|
||||
Write-Host "✓ Package copied to $remoteTemp.zip" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "✗ Failed to copy package: $($_.Exception.Message)" -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
|
||||
if (-not $CopyOnly) {
|
||||
# Extract and install on VM
|
||||
Write-Host "Extracting and installing on VM..." -ForegroundColor Yellow
|
||||
try {
|
||||
$installResult = Invoke-Command -Session $session -ScriptBlock {
|
||||
param($remotePath, $packageName)
|
||||
|
||||
# Extract package
|
||||
Expand-Archive -Path "$remotePath.zip" -DestinationPath $remotePath -Force
|
||||
Set-Location $remotePath
|
||||
|
||||
# Run installation
|
||||
$installOutput = & ".\install-service.ps1" 2>&1
|
||||
return @{
|
||||
Success = $LASTEXITCODE -eq 0
|
||||
Output = $installOutput -join "`n"
|
||||
ExitCode = $LASTEXITCODE
|
||||
}
|
||||
} -ArgumentList $remoteTemp, $packageName
|
||||
|
||||
if ($installResult.Success) {
|
||||
Write-Host "✓ Installation completed successfully!" -ForegroundColor Green
|
||||
Write-Host "Web Dashboard: http://$VMAddress:5000" -ForegroundColor Cyan
|
||||
} else {
|
||||
Write-Host "✗ Installation failed (Exit Code: $($installResult.ExitCode))" -ForegroundColor Red
|
||||
Write-Host "Output:" -ForegroundColor Yellow
|
||||
Write-Host $installResult.Output -ForegroundColor Gray
|
||||
}
|
||||
} catch {
|
||||
Write-Host "✗ Installation failed: $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
}
|
||||
|
||||
# Clean up session
|
||||
Remove-PSSession $session
|
||||
|
||||
} else {
|
||||
# Manual deployment instructions
|
||||
Write-Host "Manual Deployment Instructions:" -ForegroundColor Yellow
|
||||
Write-Host
|
||||
Write-Host "1. Copy the package to your VM:" -ForegroundColor White
|
||||
Write-Host " - Use RDP, shared folders, or network copy" -ForegroundColor Gray
|
||||
Write-Host " - Package location: $PackagePath" -ForegroundColor Gray
|
||||
Write-Host
|
||||
Write-Host "2. On the VM, extract the ZIP file to a temporary directory" -ForegroundColor White
|
||||
Write-Host
|
||||
Write-Host "3. Open PowerShell as Administrator and navigate to the extracted directory" -ForegroundColor White
|
||||
Write-Host
|
||||
Write-Host "4. Run the installation:" -ForegroundColor White
|
||||
Write-Host " .\install-service.ps1" -ForegroundColor Cyan
|
||||
Write-Host " OR" -ForegroundColor Gray
|
||||
Write-Host " .\INSTALL.bat" -ForegroundColor Cyan
|
||||
Write-Host
|
||||
Write-Host "5. Access the web dashboard:" -ForegroundColor White
|
||||
Write-Host " http://$VMAddress:5000" -ForegroundColor Cyan
|
||||
Write-Host
|
||||
Write-Host "Alternative deployment methods:" -ForegroundColor Yellow
|
||||
Write-Host " PowerShell Remoting: .\deploy-to-vm.ps1 -VMAddress $VMAddress -UseWinRM" -ForegroundColor Gray
|
||||
Write-Host " Copy only: .\deploy-to-vm.ps1 -VMAddress $VMAddress -UseWinRM -CopyOnly" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
Write-Host
|
||||
Write-Host "=== Deployment Script Complete ===" -ForegroundColor Cyan
|
||||
@@ -0,0 +1,194 @@
|
||||
#!/bin/bash
|
||||
# Resource Monitor Service - Installation Script for Linux systemd service
|
||||
|
||||
SERVICE_NAME="resource-monitor"
|
||||
SERVICE_DISPLAY_NAME="Resource Monitor Service v2.1"
|
||||
SERVICE_DESCRIPTION="Monitors system resources with web dashboard"
|
||||
INSTALL_PATH="/opt/resource-monitor"
|
||||
SERVICE_FILE="/etc/systemd/system/${SERVICE_NAME}.service"
|
||||
|
||||
echo "=== Resource Monitor Service - Linux systemd Installer ==="
|
||||
echo
|
||||
|
||||
# Check if running as root
|
||||
if [[ $EUID -ne 0 ]]; then
|
||||
echo "ERROR: This script must be run as root (use sudo)"
|
||||
echo "Please run: sudo ./install-service-linux.sh"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Function to uninstall service
|
||||
uninstall_service() {
|
||||
echo "Uninstalling Resource Monitor Service..."
|
||||
|
||||
# Stop the service
|
||||
echo "Stopping service..."
|
||||
systemctl stop $SERVICE_NAME 2>/dev/null
|
||||
echo "Service stopped"
|
||||
|
||||
# Disable the service
|
||||
echo "Disabling service..."
|
||||
systemctl disable $SERVICE_NAME 2>/dev/null
|
||||
echo "Service disabled"
|
||||
|
||||
# Remove service file
|
||||
echo "Removing service file..."
|
||||
rm -f $SERVICE_FILE
|
||||
echo "Service file removed"
|
||||
|
||||
# Reload systemd
|
||||
systemctl daemon-reload
|
||||
|
||||
# Optionally remove installation directory
|
||||
read -p "Remove installation files from $INSTALL_PATH? (y/N): " removeFiles
|
||||
if [[ "$removeFiles" == "y" || "$removeFiles" == "Y" ]]; then
|
||||
rm -rf $INSTALL_PATH
|
||||
echo "Installation files removed"
|
||||
fi
|
||||
|
||||
echo "Uninstallation complete!"
|
||||
exit 0
|
||||
}
|
||||
|
||||
# Check for uninstall flag
|
||||
if [[ "$1" == "--uninstall" || "$1" == "-u" ]]; then
|
||||
uninstall_service
|
||||
fi
|
||||
|
||||
echo "Installing Resource Monitor Service as systemd service..."
|
||||
|
||||
# Check if .NET is installed
|
||||
if ! command -v dotnet &> /dev/null; then
|
||||
echo "ERROR: .NET runtime is not installed"
|
||||
echo "Please install .NET 8.0 or later runtime"
|
||||
echo "See: https://docs.microsoft.com/en-us/dotnet/core/install/linux"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create installation directory
|
||||
echo "Creating installation directory..."
|
||||
mkdir -p "$INSTALL_PATH"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "ERROR: Failed to create installation directory"
|
||||
exit 1
|
||||
fi
|
||||
echo "Installation directory created: $INSTALL_PATH"
|
||||
|
||||
# Build the service in release mode
|
||||
echo "Building service..."
|
||||
dotnet publish --configuration Release --output "$INSTALL_PATH"
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "ERROR: Build failed"
|
||||
exit 1
|
||||
fi
|
||||
echo "Service built successfully"
|
||||
|
||||
# Stop existing service if running
|
||||
echo "Stopping existing service (if running)..."
|
||||
systemctl stop $SERVICE_NAME 2>/dev/null || echo "No existing service found"
|
||||
|
||||
# Create systemd service file
|
||||
echo "Creating systemd service file..."
|
||||
cat > $SERVICE_FILE << EOF
|
||||
[Unit]
|
||||
Description=$SERVICE_DESCRIPTION
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
Type=notify
|
||||
ExecStart=/usr/bin/dotnet $INSTALL_PATH/ResourceMonitorService.dll
|
||||
Restart=always
|
||||
RestartSec=5
|
||||
SyslogIdentifier=resource-monitor
|
||||
User=www-data
|
||||
Environment=ASPNETCORE_ENVIRONMENT=Production
|
||||
Environment=ASPNETCORE_URLS=http://localhost:5000
|
||||
WorkingDirectory=$INSTALL_PATH
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
EOF
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "ERROR: Failed to create service file"
|
||||
exit 1
|
||||
fi
|
||||
echo "Service file created"
|
||||
|
||||
# Set proper permissions
|
||||
chmod 644 $SERVICE_FILE
|
||||
chown root:root $SERVICE_FILE
|
||||
|
||||
# Reload systemd
|
||||
echo "Reloading systemd daemon..."
|
||||
systemctl daemon-reload
|
||||
|
||||
# Enable the service
|
||||
echo "Enabling service..."
|
||||
systemctl enable $SERVICE_NAME
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "ERROR: Failed to enable service"
|
||||
exit 1
|
||||
fi
|
||||
echo "Service enabled"
|
||||
|
||||
# Start the service
|
||||
echo "Starting service..."
|
||||
systemctl start $SERVICE_NAME
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "ERROR: Failed to start service"
|
||||
echo "Check service status with: systemctl status $SERVICE_NAME"
|
||||
exit 1
|
||||
fi
|
||||
echo "Service started successfully"
|
||||
|
||||
# Wait a moment and check service status
|
||||
sleep 3
|
||||
SERVICE_STATUS=$(systemctl is-active $SERVICE_NAME)
|
||||
echo "Service Status: $SERVICE_STATUS"
|
||||
|
||||
# Configure firewall (if ufw is available)
|
||||
if command -v ufw &> /dev/null; then
|
||||
echo "Configuring firewall for web dashboard..."
|
||||
ufw allow 5000/tcp comment "Resource Monitor Service"
|
||||
echo "Firewall rule created for port 5000"
|
||||
fi
|
||||
|
||||
echo
|
||||
echo "=== Installation Complete ==="
|
||||
echo "Service Name: $SERVICE_NAME"
|
||||
echo "Installation Path: $INSTALL_PATH"
|
||||
echo "Web Dashboard: http://localhost:5000"
|
||||
echo "API Documentation: http://localhost:5000/swagger"
|
||||
echo "API Health Check: http://localhost:5000/api/health"
|
||||
echo
|
||||
echo "The service is now running and will start automatically with the system."
|
||||
echo "You can manage it using systemctl commands:"
|
||||
echo " - Stop: sudo systemctl stop $SERVICE_NAME"
|
||||
echo " - Start: sudo systemctl start $SERVICE_NAME"
|
||||
echo " - Status: systemctl status $SERVICE_NAME"
|
||||
echo " - Restart: sudo systemctl restart $SERVICE_NAME"
|
||||
echo " - Logs: journalctl -u $SERVICE_NAME -f"
|
||||
echo
|
||||
echo "To uninstall: sudo ./install-service-linux.sh --uninstall"
|
||||
|
||||
# Test the web dashboard
|
||||
echo
|
||||
echo "Testing web dashboard..."
|
||||
sleep 5
|
||||
if command -v curl &> /dev/null; then
|
||||
RESPONSE=$(curl -s -o /dev/null -w "%{http_code}" http://localhost:5000/api/health)
|
||||
if [[ "$RESPONSE" == "200" ]]; then
|
||||
echo "Web Dashboard Test: SUCCESS"
|
||||
echo
|
||||
echo "🎉 Web Dashboard is ready at: http://localhost:5000"
|
||||
echo "📖 API Documentation at: http://localhost:5000/swagger"
|
||||
else
|
||||
echo "Web Dashboard Test: FAILED (HTTP $RESPONSE)"
|
||||
echo "The service may still be starting up. Wait a few minutes and try accessing:"
|
||||
echo "http://localhost:5000"
|
||||
fi
|
||||
else
|
||||
echo "curl not available for testing. Please check manually:"
|
||||
echo "http://localhost:5000"
|
||||
fi
|
||||
+25
-17
@@ -6,8 +6,8 @@ param(
|
||||
)
|
||||
|
||||
$SERVICE_NAME = "ResourceMonitorService"
|
||||
$SERVICE_DISPLAY_NAME = "Resource Monitor Service v2.0"
|
||||
$SERVICE_DESCRIPTION = "Monitors VM resources for Unraid integration"
|
||||
$SERVICE_DISPLAY_NAME = "Resource Monitor Service v2.1"
|
||||
$SERVICE_DESCRIPTION = "Monitors system resources with web dashboard for Unraid integration"
|
||||
$INSTALL_PATH = "C:\Services\ResourceMonitor"
|
||||
|
||||
Write-Host "=== Resource Monitor Service - Windows Service Installer ===" -ForegroundColor Cyan
|
||||
@@ -135,14 +135,18 @@ try {
|
||||
Write-Host "WARNING: Failed to configure service recovery options" -ForegroundColor Yellow
|
||||
}
|
||||
|
||||
# Configure firewall rule
|
||||
Write-Host "Configuring Windows Firewall..."
|
||||
# Configure firewall rule for web dashboard
|
||||
Write-Host "Configuring Windows Firewall for web dashboard..."
|
||||
try {
|
||||
New-NetFirewallRule -DisplayName "Resource Monitor Service" -Direction Inbound -Protocol TCP -LocalPort 2414 -Action Allow -Profile Any -ErrorAction Stop
|
||||
Write-Host "Firewall rule created" -ForegroundColor Green
|
||||
# Remove old rule if it exists
|
||||
Remove-NetFirewallRule -DisplayName "Resource Monitor Service" -ErrorAction SilentlyContinue
|
||||
|
||||
# Create new rule for port 5000 (web dashboard)
|
||||
New-NetFirewallRule -DisplayName "Resource Monitor Service" -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow -Profile Any -ErrorAction Stop
|
||||
Write-Host "Firewall rule created for web dashboard (port 5000)" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "WARNING: Failed to create firewall rule. You may need to configure manually." -ForegroundColor Yellow
|
||||
Write-Host "Manual command: New-NetFirewallRule -DisplayName 'Resource Monitor Service' -Direction Inbound -Protocol TCP -LocalPort 2414 -Action Allow" -ForegroundColor Gray
|
||||
Write-Host "Manual command: New-NetFirewallRule -DisplayName 'Resource Monitor Service' -Direction Inbound -Protocol TCP -LocalPort 5000 -Action Allow" -ForegroundColor Gray
|
||||
}
|
||||
|
||||
# Start the service
|
||||
@@ -165,8 +169,9 @@ Write-Host
|
||||
Write-Host "=== Installation Complete ===" -ForegroundColor Cyan
|
||||
Write-Host "Service Name: $SERVICE_NAME" -ForegroundColor White
|
||||
Write-Host "Installation Path: $INSTALL_PATH" -ForegroundColor White
|
||||
Write-Host "Service URL: http://localhost:2414" -ForegroundColor White
|
||||
Write-Host "API Health Check: http://localhost:2414/api/health" -ForegroundColor White
|
||||
Write-Host "Web Dashboard: http://localhost:5000" -ForegroundColor Yellow
|
||||
Write-Host "API Documentation: http://localhost:5000/swagger" -ForegroundColor Yellow
|
||||
Write-Host "API Health Check: http://localhost:5000/api/health" -ForegroundColor White
|
||||
Write-Host
|
||||
Write-Host "The service is now running and will start automatically with Windows." -ForegroundColor Green
|
||||
Write-Host "You can manage it through Services.msc or using PowerShell commands:" -ForegroundColor White
|
||||
@@ -177,17 +182,20 @@ Write-Host " - Restart: Restart-Service -Name $SERVICE_NAME" -ForegroundColor G
|
||||
Write-Host
|
||||
Write-Host "To uninstall: .\install-service.ps1 -Uninstall" -ForegroundColor Yellow
|
||||
|
||||
# Test the API endpoint
|
||||
# Test the web dashboard
|
||||
Write-Host
|
||||
Write-Host "Testing API endpoint..." -ForegroundColor Yellow
|
||||
Write-Host "Testing web dashboard..." -ForegroundColor Yellow
|
||||
Start-Sleep -Seconds 5
|
||||
try {
|
||||
$response = Invoke-RestMethod -Uri "http://localhost:2414/" -TimeoutSec 10
|
||||
Write-Host "API Test Result: SUCCESS" -ForegroundColor Green
|
||||
Write-Host "Service Version: $($response.Service) v$($response.Version)" -ForegroundColor White
|
||||
Write-Host "Status: $($response.Status)" -ForegroundColor White
|
||||
$response = Invoke-RestMethod -Uri "http://localhost:5000/api/health" -TimeoutSec 10
|
||||
Write-Host "Web Dashboard Test: SUCCESS" -ForegroundColor Green
|
||||
Write-Host "Service Status: $($response.status)" -ForegroundColor White
|
||||
Write-Host "Service Uptime: $($response.uptime)" -ForegroundColor White
|
||||
Write-Host
|
||||
Write-Host "🎉 Web Dashboard is ready at: http://localhost:5000" -ForegroundColor Green
|
||||
Write-Host "📖 API Documentation at: http://localhost:5000/swagger" -ForegroundColor Green
|
||||
} catch {
|
||||
Write-Host "API Test Result: FAILED" -ForegroundColor Red
|
||||
Write-Host "Web Dashboard Test: FAILED" -ForegroundColor Red
|
||||
Write-Host "The service may still be starting up. Wait a few minutes and try accessing:" -ForegroundColor Yellow
|
||||
Write-Host "http://localhost:2414/api/health" -ForegroundColor White
|
||||
Write-Host "http://localhost:5000" -ForegroundColor White
|
||||
}
|
||||
|
||||
@@ -1,86 +0,0 @@
|
||||
#!/bin/bash
|
||||
# Resource Monitor Service - Installation Script for Windows Service
|
||||
|
||||
SERVICE_NAME="ResourceMonitorService"
|
||||
SERVICE_DISPLAY_NAME="Resource Monitor Service v2.0"
|
||||
SERVICE_DESCRIPTION="Monitors VM resources for Unraid integration"
|
||||
INSTALL_PATH="C:\Services\ResourceMonitor"
|
||||
|
||||
echo "=== Resource Monitor Service - Windows Service Installer ==="
|
||||
echo
|
||||
|
||||
# Check if running as administrator
|
||||
if [[ ! $(id -u) -eq 0 ]]; then
|
||||
echo "ERROR: This script must be run as Administrator"
|
||||
echo "Please run PowerShell as Administrator and try again"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "Installing Resource Monitor Service as Windows Service..."
|
||||
|
||||
# Create installation directory
|
||||
echo "Creating installation directory..."
|
||||
mkdir -p "$INSTALL_PATH"
|
||||
|
||||
# Build the service in release mode
|
||||
echo "Building service..."
|
||||
dotnet publish --configuration Release --output "$INSTALL_PATH"
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "ERROR: Build failed"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop existing service if running
|
||||
echo "Stopping existing service (if running)..."
|
||||
sc stop "$SERVICE_NAME" 2>/dev/null
|
||||
|
||||
# Remove existing service if it exists
|
||||
echo "Removing existing service (if exists)..."
|
||||
sc delete "$SERVICE_NAME" 2>/dev/null
|
||||
|
||||
# Install the service
|
||||
echo "Installing Windows Service..."
|
||||
sc create "$SERVICE_NAME" \
|
||||
binPath="\"$INSTALL_PATH\\ResourceMonitorService.exe\" --windows-service" \
|
||||
DisplayName="$SERVICE_DISPLAY_NAME" \
|
||||
Description="$SERVICE_DESCRIPTION" \
|
||||
start=auto
|
||||
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo "ERROR: Failed to create Windows Service"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Configure service recovery options
|
||||
echo "Configuring service recovery options..."
|
||||
sc failure "$SERVICE_NAME" reset=300 actions=restart/5000/restart/5000/restart/10000
|
||||
|
||||
# Configure firewall rule
|
||||
echo "Configuring Windows Firewall..."
|
||||
powershell -Command "New-NetFirewallRule -DisplayName 'Resource Monitor Service' -Direction Inbound -Protocol TCP -LocalPort 2414 -Action Allow -Profile Any" 2>/dev/null
|
||||
|
||||
# Start the service
|
||||
echo "Starting service..."
|
||||
sc start "$SERVICE_NAME"
|
||||
|
||||
if [[ $? -eq 0 ]]; then
|
||||
echo
|
||||
echo "=== Installation Complete ==="
|
||||
echo "Service Name: $SERVICE_NAME"
|
||||
echo "Installation Path: $INSTALL_PATH"
|
||||
echo "Service URL: http://localhost:2414"
|
||||
echo "API Health Check: http://localhost:2414/api/health"
|
||||
echo
|
||||
echo "The service is now running and will start automatically with Windows."
|
||||
echo "You can manage it through Services.msc or using sc commands:"
|
||||
echo " - Stop: sc stop $SERVICE_NAME"
|
||||
echo " - Start: sc start $SERVICE_NAME"
|
||||
echo " - Status: sc query $SERVICE_NAME"
|
||||
echo
|
||||
echo "To uninstall: sc stop $SERVICE_NAME && sc delete $SERVICE_NAME"
|
||||
else
|
||||
echo "ERROR: Failed to start service"
|
||||
echo "Check Windows Event Log for details"
|
||||
exit 1
|
||||
fi
|
||||
Binary file not shown.
@@ -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,136 @@
|
||||
/* 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);
|
||||
}
|
||||
}
|
||||
|
||||
/* System Control Button Animations */
|
||||
@keyframes pulse {
|
||||
0% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0.7);
|
||||
}
|
||||
50% {
|
||||
transform: scale(1.05);
|
||||
box-shadow: 0 0 0 10px rgba(220, 38, 38, 0);
|
||||
}
|
||||
100% {
|
||||
transform: scale(1);
|
||||
box-shadow: 0 0 0 0 rgba(220, 38, 38, 0);
|
||||
}
|
||||
}
|
||||
|
||||
/* Make title elements more touch-friendly on mobile */
|
||||
@media (max-width: 768px) {
|
||||
nav h1 {
|
||||
padding: 8px;
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
user-select: none;
|
||||
-webkit-user-select: none;
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
nav h1:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
|
||||
nav .fas.fa-chart-line {
|
||||
padding: 8px;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
-webkit-tap-highlight-color: rgba(255, 255, 255, 0.2);
|
||||
}
|
||||
|
||||
nav .fas.fa-chart-line:active {
|
||||
background-color: rgba(255, 255, 255, 0.1);
|
||||
}
|
||||
}
|
||||
|
||||
/* 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,257 @@
|
||||
<!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>
|
||||
<!-- Hidden System Control Button (requires triple-click to show) -->
|
||||
<button id="systemControl" class="hidden bg-red-600 hover:bg-red-800 px-4 py-2 rounded-lg transition-colors" title="System Control">
|
||||
<i class="fas fa-power-off mr-2"></i>System
|
||||
</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-3 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 class="mt-2 flex items-center justify-between text-sm">
|
||||
<span class="text-gray-500">
|
||||
<i class="fas fa-thermometer-half mr-1"></i>
|
||||
<span id="gpuTemp" class="font-medium">0°C</span>
|
||||
</span>
|
||||
<span id="gpuTempStatus" class="text-green-600">Normal</span>
|
||||
</div>
|
||||
</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>
|
||||
|
||||
<!-- System Control Modal -->
|
||||
<div id="systemControlModal" class="hidden fixed inset-0 bg-black bg-opacity-50 flex items-center justify-center z-50">
|
||||
<div class="bg-white rounded-lg p-6 max-w-md w-full mx-4">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h3 class="text-lg font-bold text-gray-800">
|
||||
<i class="fas fa-power-off mr-2 text-red-600"></i>System Control
|
||||
</h3>
|
||||
<button id="closeSystemModal" class="text-gray-400 hover:text-gray-600">
|
||||
<i class="fas fa-times text-xl"></i>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="space-y-4">
|
||||
<!-- Timer Input -->
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-2">
|
||||
Timer (seconds) - Default: 15 seconds
|
||||
</label>
|
||||
<input type="number" id="systemTimer"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
placeholder="15" min="0" max="86400" value="15">
|
||||
<p class="text-xs text-gray-500 mt-1">Maximum: 24 hours (86400 seconds)</p>
|
||||
</div>
|
||||
|
||||
<!-- Action Buttons -->
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<button id="shutdownBtn" class="bg-red-600 hover:bg-red-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
|
||||
<i class="fas fa-power-off mr-2"></i>Shutdown
|
||||
</button>
|
||||
<button id="restartBtn" class="bg-orange-600 hover:bg-orange-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
|
||||
<i class="fas fa-redo mr-2"></i>Restart
|
||||
</button>
|
||||
<button id="cancelBtn" class="bg-yellow-600 hover:bg-yellow-700 text-white px-4 py-3 rounded-lg transition-colors flex items-center justify-center">
|
||||
<i class="fas fa-ban mr-2"></i>Cancel
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- Force Shutdown Option -->
|
||||
<div class="flex items-center">
|
||||
<input type="checkbox" id="forceShutdown" class="mr-2" checked>
|
||||
<label for="forceShutdown" class="text-sm text-gray-700">Force shutdown (close applications without saving)</label>
|
||||
</div>
|
||||
|
||||
<!-- Warning -->
|
||||
<div class="bg-yellow-50 border border-yellow-200 rounded-lg p-3">
|
||||
<div class="flex">
|
||||
<i class="fas fa-exclamation-triangle text-yellow-600 mr-2 mt-0.5"></i>
|
||||
<div class="text-sm text-yellow-800">
|
||||
<strong>Warning:</strong> This will shut down or restart the entire system.
|
||||
Make sure to save any unsaved work first. Use the Cancel button to abort any pending shutdown/restart.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script src="js/dashboard.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,968 @@
|
||||
// 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();
|
||||
});
|
||||
|
||||
// System control event listeners
|
||||
document.getElementById('systemControl').addEventListener('click', () => {
|
||||
this.showSystemControlModal();
|
||||
});
|
||||
|
||||
document.getElementById('closeSystemModal').addEventListener('click', () => {
|
||||
this.hideSystemControlModal();
|
||||
});
|
||||
|
||||
document.getElementById('shutdownBtn').addEventListener('click', () => {
|
||||
this.executeSystemCommand('shutdown');
|
||||
});
|
||||
|
||||
document.getElementById('restartBtn').addEventListener('click', () => {
|
||||
this.executeSystemCommand('restart');
|
||||
});
|
||||
|
||||
document.getElementById('cancelBtn').addEventListener('click', () => {
|
||||
this.cancelSystemCommand();
|
||||
});
|
||||
|
||||
// Hidden system control activation methods
|
||||
this.setupHiddenSystemControlAccess();
|
||||
|
||||
// Close modal when clicking outside
|
||||
document.getElementById('systemControlModal').addEventListener('click', (e) => {
|
||||
if (e.target.id === 'systemControlModal') {
|
||||
this.hideSystemControlModal();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
setupHiddenSystemControlAccess() {
|
||||
// Method 1: Triple click on title (works on mobile)
|
||||
let clickCount = 0;
|
||||
let clickTimer = null;
|
||||
const titleElement = document.querySelector('h1');
|
||||
|
||||
titleElement.addEventListener('click', () => {
|
||||
clickCount++;
|
||||
if (clickCount === 1) {
|
||||
clickTimer = setTimeout(() => {
|
||||
clickCount = 0;
|
||||
}, 2000); // Reset after 2 seconds
|
||||
} else if (clickCount === 3) {
|
||||
clearTimeout(clickTimer);
|
||||
clickCount = 0;
|
||||
this.toggleSystemControlButton();
|
||||
}
|
||||
});
|
||||
|
||||
// Method 2: Long press on title (mobile-friendly)
|
||||
let pressTimer = null;
|
||||
let pressStartTime = 0;
|
||||
|
||||
titleElement.addEventListener('touchstart', (e) => {
|
||||
pressStartTime = Date.now();
|
||||
pressTimer = setTimeout(() => {
|
||||
// Vibrate if supported (mobile feedback)
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate(200);
|
||||
}
|
||||
this.toggleSystemControlButton();
|
||||
}, 2000); // 2 second long press
|
||||
});
|
||||
|
||||
titleElement.addEventListener('touchend', () => {
|
||||
if (pressTimer) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
titleElement.addEventListener('touchmove', () => {
|
||||
if (pressTimer) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Method 3: Mouse long press (desktop fallback)
|
||||
titleElement.addEventListener('mousedown', (e) => {
|
||||
if (e.button === 0) { // Left mouse button
|
||||
pressStartTime = Date.now();
|
||||
pressTimer = setTimeout(() => {
|
||||
this.toggleSystemControlButton();
|
||||
}, 2000);
|
||||
}
|
||||
});
|
||||
|
||||
titleElement.addEventListener('mouseup', () => {
|
||||
if (pressTimer) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
titleElement.addEventListener('mouseleave', () => {
|
||||
if (pressTimer) {
|
||||
clearTimeout(pressTimer);
|
||||
pressTimer = null;
|
||||
}
|
||||
});
|
||||
|
||||
// Method 4: Secret tap sequence on the favicon
|
||||
let tapSequence = [];
|
||||
const favicon = document.querySelector('nav .fas.fa-chart-line');
|
||||
|
||||
favicon.addEventListener('click', () => {
|
||||
tapSequence.push(Date.now());
|
||||
|
||||
// Keep only last 5 taps within 3 seconds
|
||||
const now = Date.now();
|
||||
tapSequence = tapSequence.filter(time => now - time < 3000);
|
||||
|
||||
// Check for specific pattern: 5 quick taps
|
||||
if (tapSequence.length >= 5) {
|
||||
const timeDiffs = [];
|
||||
for (let i = 1; i < tapSequence.length; i++) {
|
||||
timeDiffs.push(tapSequence[i] - tapSequence[i-1]);
|
||||
}
|
||||
|
||||
// All taps should be within 200ms of each other
|
||||
const isQuickSequence = timeDiffs.every(diff => diff < 500);
|
||||
if (isQuickSequence) {
|
||||
if (navigator.vibrate) {
|
||||
navigator.vibrate([100, 50, 100]);
|
||||
}
|
||||
this.toggleSystemControlButton();
|
||||
tapSequence = [];
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Method 5: Keyboard shortcut (desktop)
|
||||
document.addEventListener('keydown', (e) => {
|
||||
if (e.ctrlKey && e.shiftKey && e.key === 'P') {
|
||||
e.preventDefault();
|
||||
this.toggleSystemControlButton();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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();
|
||||
}
|
||||
}, 300000); // 300 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;
|
||||
const gpuTemp = data.gpu.temperature || 0;
|
||||
|
||||
document.getElementById('gpuUsage').textContent = `${gpuUsage.toFixed(1)}%`;
|
||||
document.getElementById('gpuBar').style.width = `${gpuUsage}%`;
|
||||
|
||||
// Update GPU temperature
|
||||
document.getElementById('gpuTemp').textContent = `${gpuTemp}°C`;
|
||||
|
||||
// Set temperature status color based on temperature ranges
|
||||
const tempStatusElement = document.getElementById('gpuTempStatus');
|
||||
if (gpuTemp <= 60) {
|
||||
tempStatusElement.textContent = 'Cool';
|
||||
tempStatusElement.className = 'text-green-600';
|
||||
} else if (gpuTemp <= 75) {
|
||||
tempStatusElement.textContent = 'Normal';
|
||||
tempStatusElement.className = 'text-yellow-600';
|
||||
} else if (gpuTemp <= 85) {
|
||||
tempStatusElement.textContent = 'Warm';
|
||||
tempStatusElement.className = 'text-orange-600';
|
||||
} else {
|
||||
tempStatusElement.textContent = 'Hot';
|
||||
tempStatusElement.className = 'text-red-600';
|
||||
}
|
||||
}
|
||||
|
||||
// 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);
|
||||
}
|
||||
|
||||
// Update System Info GPU details (only if details section is visible and we have system info)
|
||||
if (!document.getElementById('detailsSection').classList.contains('hidden') && this.lastSystemInfo) {
|
||||
this.updateSystemInfo(this.lastSystemInfo);
|
||||
}
|
||||
} 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;
|
||||
|
||||
// Get GPU info from the latest resource data if available
|
||||
const gpuInfo = this.lastResourceData?.gpu;
|
||||
|
||||
let gpuSection = '';
|
||||
if (gpuInfo && gpuInfo.isAvailable) {
|
||||
gpuSection = `
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h4 class="font-semibold text-gray-700">GPU</h4>
|
||||
<p class="text-gray-600">${gpuInfo.name || 'Unknown GPU'}</p>
|
||||
<div class="mt-2 space-y-1">
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">Usage:</span>
|
||||
<span class="text-gray-700">${gpuInfo.usage || 0}%</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">Temperature:</span>
|
||||
<span class="text-gray-700 ${gpuInfo.temperature > 85 ? 'text-red-600' : gpuInfo.temperature > 75 ? 'text-orange-600' : gpuInfo.temperature > 60 ? 'text-yellow-600' : 'text-green-600'}">${gpuInfo.temperature || 0}°C</span>
|
||||
</div>
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">Memory:</span>
|
||||
<span class="text-gray-700">${gpuInfo.memoryUsed && gpuInfo.memoryTotal ? ((gpuInfo.memoryUsed / gpuInfo.memoryTotal) * 100).toFixed(1) : 0}%</span>
|
||||
</div>
|
||||
${gpuInfo.fanSpeed ? `
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">Fan Speed:</span>
|
||||
<span class="text-gray-700">${gpuInfo.fanSpeed}%</span>
|
||||
</div>` : ''}
|
||||
${gpuInfo.powerUsage ? `
|
||||
<div class="flex justify-between text-sm">
|
||||
<span class="text-gray-500">Power:</span>
|
||||
<span class="text-gray-700">${gpuInfo.powerUsage}W</span>
|
||||
</div>` : ''}
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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>
|
||||
${gpuSection}
|
||||
`;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
// System Control Methods
|
||||
toggleSystemControlButton() {
|
||||
const systemButton = document.getElementById('systemControl');
|
||||
if (systemButton.classList.contains('hidden')) {
|
||||
systemButton.classList.remove('hidden');
|
||||
|
||||
// Add visual feedback with pulsing effect
|
||||
systemButton.style.animation = 'pulse 1s ease-in-out 3';
|
||||
|
||||
// Show mobile-friendly notification
|
||||
this.showNotification('🔓 System control unlocked! Tap the red System button to access shutdown/restart options.', 'info');
|
||||
|
||||
// Auto-hide after 30 seconds for security
|
||||
setTimeout(() => {
|
||||
if (!systemButton.classList.contains('hidden')) {
|
||||
systemButton.classList.add('hidden');
|
||||
this.hideSystemControlModal();
|
||||
this.showNotification('System control auto-locked for security', 'info');
|
||||
}
|
||||
}, 30000);
|
||||
} else {
|
||||
systemButton.classList.add('hidden');
|
||||
systemButton.style.animation = '';
|
||||
this.hideSystemControlModal();
|
||||
this.showNotification('System control locked', 'info');
|
||||
}
|
||||
}
|
||||
|
||||
showSystemControlModal() {
|
||||
document.getElementById('systemControlModal').classList.remove('hidden');
|
||||
document.getElementById('systemTimer').value = '15'; // Default 15 seconds
|
||||
document.getElementById('forceShutdown').checked = true;
|
||||
}
|
||||
|
||||
hideSystemControlModal() {
|
||||
document.getElementById('systemControlModal').classList.add('hidden');
|
||||
}
|
||||
|
||||
async executeSystemCommand(action) {
|
||||
const timer = document.getElementById('systemTimer').value;
|
||||
const force = document.getElementById('forceShutdown').checked;
|
||||
|
||||
// Validate timer input - default to 15 if empty
|
||||
const timerSeconds = parseInt(timer) || 15;
|
||||
if (timerSeconds < 0 || timerSeconds > 86400) {
|
||||
this.showNotification('Timer must be between 0 and 86400 seconds (24 hours)', 'error');
|
||||
return;
|
||||
}
|
||||
|
||||
// Build confirmation message
|
||||
let confirmMessage = `Are you sure you want to ${action} the system`;
|
||||
if (timerSeconds > 0) {
|
||||
const minutes = Math.floor(timerSeconds / 60);
|
||||
const seconds = timerSeconds % 60;
|
||||
if (minutes > 0) {
|
||||
confirmMessage += ` in ${minutes} minute${minutes !== 1 ? 's' : ''}`;
|
||||
if (seconds > 0) {
|
||||
confirmMessage += ` and ${seconds} second${seconds !== 1 ? 's' : ''}`;
|
||||
}
|
||||
} else {
|
||||
confirmMessage += ` in ${seconds} second${seconds !== 1 ? 's' : ''}`;
|
||||
}
|
||||
} else {
|
||||
confirmMessage += ' immediately';
|
||||
}
|
||||
confirmMessage += '?\n\nNote: You can cancel this action using the Cancel button.';
|
||||
|
||||
if (!confirm(confirmMessage)) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
// Prepare the command data
|
||||
const commandData = {
|
||||
action: action,
|
||||
timer: timerSeconds,
|
||||
force: force
|
||||
};
|
||||
|
||||
const response = await fetch('/api/resource/system-control', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
},
|
||||
body: JSON.stringify(commandData)
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
this.showNotification(result.message, 'success');
|
||||
this.hideSystemControlModal();
|
||||
|
||||
// If immediate action, warn user
|
||||
if (timerSeconds === 0) {
|
||||
setTimeout(() => {
|
||||
this.showNotification(`System ${action} initiated! Connection will be lost.`, 'info');
|
||||
}, 1000);
|
||||
} else {
|
||||
// Show reminder about cancel option
|
||||
setTimeout(() => {
|
||||
this.showNotification(`Reminder: Use the Cancel button to abort the ${action} if needed.`, 'info');
|
||||
}, 2000);
|
||||
}
|
||||
} else {
|
||||
const error = await response.text();
|
||||
this.showNotification(`Failed to ${action} system: ${error}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error(`Error executing ${action}:`, error);
|
||||
this.showNotification(`Error executing ${action} command`, 'error');
|
||||
}
|
||||
}
|
||||
|
||||
async cancelSystemCommand() {
|
||||
if (!confirm('Are you sure you want to cancel any pending shutdown/restart?')) {
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
const response = await fetch('/api/resource/cancel-shutdown', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json'
|
||||
}
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const result = await response.json();
|
||||
this.showNotification(result.message, 'success');
|
||||
} else {
|
||||
const error = await response.text();
|
||||
this.showNotification(`Failed to cancel shutdown: ${error}`, 'error');
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error canceling shutdown:', error);
|
||||
this.showNotification('Error canceling shutdown command', 'error');
|
||||
}
|
||||
}
|
||||
|
||||
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