mirror of
https://github.com/Stone-Red-Code/RemoteExec.git
synced 2026-09-04 00:56:17 +02:00
Add dashboard
This commit is contained in:
@@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<base href="/" />
|
||||
<link href="https://fonts.googleapis.com/css?family=Roboto:300,400,500,700&display=swap" rel="stylesheet" />
|
||||
<link href="_content/MudBlazor/MudBlazor.min.css" rel="stylesheet" />
|
||||
<link rel="icon" type="image/png" href="favicon.png" />
|
||||
<HeadOutlet @rendermode="InteractiveServer" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<Routes @rendermode="InteractiveServer" />
|
||||
<script src="_framework/blazor.web.js"></script>
|
||||
<script src="_content/MudBlazor/MudBlazor.min.js"></script>
|
||||
</body>
|
||||
|
||||
</html>
|
||||
@@ -0,0 +1,46 @@
|
||||
@inherits LayoutComponentBase
|
||||
|
||||
<MudThemeProvider Theme="customTheme" IsDarkMode="true" />
|
||||
<MudPopoverProvider />
|
||||
<MudDialogProvider />
|
||||
<MudSnackbarProvider />
|
||||
|
||||
<MudLayout>
|
||||
<MudAppBar Elevation="1">
|
||||
<MudIconButton Icon="@Icons.Material.Filled.Dashboard" Color="Color.Inherit" Edge="Edge.Start" />
|
||||
<MudText Typo="Typo.h5" Class="ml-3">RemoteExec Dashboard</MudText>
|
||||
<MudSpacer />
|
||||
<MudText Typo="Typo.body2">Real-time Server Metrics</MudText>
|
||||
</MudAppBar>
|
||||
<MudMainContent>
|
||||
<MudContainer MaxWidth="MaxWidth.False" Class="mt-4">
|
||||
@Body
|
||||
</MudContainer>
|
||||
</MudMainContent>
|
||||
</MudLayout>
|
||||
|
||||
@code {
|
||||
private MudTheme customTheme = new MudTheme()
|
||||
{
|
||||
PaletteLight = new PaletteLight()
|
||||
{
|
||||
Primary = "#594ae2",
|
||||
Secondary = "#ff4081",
|
||||
AppbarBackground = "#594ae2",
|
||||
},
|
||||
PaletteDark = new PaletteDark()
|
||||
{
|
||||
Primary = "#776be7",
|
||||
Secondary = "#ff4081",
|
||||
AppbarBackground = "#27272f",
|
||||
Background = "#1e1e2d",
|
||||
BackgroundGray = "#27272f",
|
||||
Surface = "#27272f",
|
||||
DrawerBackground = "#27272f",
|
||||
DrawerText = "rgba(255,255,255, 0.7)",
|
||||
ActionDefault = "#adadb1",
|
||||
ActionDisabled = "rgba(255,255,255, 0.26)",
|
||||
ActionDisabledBackground = "rgba(255,255,255, 0.12)",
|
||||
}
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
@page "/"
|
||||
@using RemoteExec.Dashboard.Services
|
||||
@using RemoteExec.Dashboard.Models
|
||||
@using RemoteExec.Shared.Models
|
||||
@inject MetricsCollectorService MetricsCollector
|
||||
@implements IDisposable
|
||||
|
||||
<PageTitle>RemoteExec Dashboard</PageTitle>
|
||||
|
||||
<MudGrid>
|
||||
@foreach (ServerConnection server in servers.Values.OrderBy(s => s.Name))
|
||||
{
|
||||
<MudItem xs="12" md="6" xl="4">
|
||||
<MudCard Elevation="3" Class="mb-4" Style="height: 100%;">
|
||||
<MudCardHeader>
|
||||
<MudStack Style="width: 100%; min-width: 0;" Spacing="0">
|
||||
<div class="d-flex align-center justify-space-between" style="width: 100%; min-width: 0;">
|
||||
<MudText Typo="Typo.h6" Style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;">
|
||||
@server.Name
|
||||
</MudText>
|
||||
@if (server.IsConnected)
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Success" Icon="@Icons.Material.Filled.CheckCircle" Style="flex-shrink: 0; margin-left: 8px;">Connected</MudChip>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudChip T="string" Size="Size.Small" Color="Color.Error" Icon="@Icons.Material.Filled.Cancel" Style="flex-shrink: 0; margin-left: 8px;">Disconnected</MudChip>
|
||||
}
|
||||
</div>
|
||||
|
||||
<MudText Typo="Typo.body2" Style="overflow: hidden; text-overflow: ellipsis; white-space: nowrap; display: block;">@server.Url</MudText>
|
||||
</MudStack>
|
||||
</MudCardHeader>
|
||||
<MudCardContent>
|
||||
@if (server.CurrentMetrics != null)
|
||||
{
|
||||
<MudGrid>
|
||||
<MudItem xs="6">
|
||||
<MudPaper Elevation="0" Class="d-flex flex-column align-center pa-2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Link" Size="Size.Large" Color="Color.Primary" />
|
||||
<MudText Typo="Typo.h6">@server.CurrentMetrics.ActiveConnections</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">Connections</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudPaper Elevation="0" Class="d-flex flex-column align-center pa-2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.PlayArrow" Size="Size.Large" Color="Color.Success" />
|
||||
<MudText Typo="Typo.h6">@server.CurrentMetrics.ActiveTasks</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">Active Tasks</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudPaper Elevation="0" Class="d-flex flex-column align-center pa-2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Memory" Size="Size.Large" Color="Color.Info" />
|
||||
<MudText Typo="Typo.h6">@FormatBytes(server.CurrentMetrics.TotalMemoryUsage)</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">Memory</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
<MudItem xs="6">
|
||||
<MudPaper Elevation="0" Class="d-flex flex-column align-center pa-2">
|
||||
<MudIcon Icon="@Icons.Material.Filled.Speed" Size="Size.Large" Color="Color.Warning" />
|
||||
<MudText Typo="Typo.h6">@server.CurrentMetrics.CpuUsage.ToString("F1")%</MudText>
|
||||
<MudText Typo="Typo.caption" Color="Color.Secondary">CPU Usage</MudText>
|
||||
</MudPaper>
|
||||
</MudItem>
|
||||
</MudGrid>
|
||||
|
||||
<MudText Typo="Typo.caption" Class="mt-3">
|
||||
Last updated: @server.CurrentMetrics.Timestamp.ToLocalTime().ToString("HH:mm:ss")
|
||||
</MudText>
|
||||
}
|
||||
else if (!string.IsNullOrEmpty(server.LastError))
|
||||
{
|
||||
<MudAlert Severity="Severity.Error">@server.LastError</MudAlert>
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudProgressCircular Indeterminate="true" />
|
||||
<MudText>Connecting...</MudText>
|
||||
}
|
||||
|
||||
<MudDivider Class="my-4" />
|
||||
|
||||
<MudExpansionPanels>
|
||||
<MudExpansionPanel Text="Historical Charts" IsInitiallyExpanded="false">
|
||||
@if (server.MetricsHistory.Count > 1)
|
||||
{
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-2">CPU Usage</MudText>
|
||||
<MudChart ChartType="ChartType.Line"
|
||||
ChartSeries="@GetCpuChartSeries(server)"
|
||||
XAxisLabels="@GetChartLabels(server)"
|
||||
Width="100%"
|
||||
Height="350px"
|
||||
ChartOptions="@chartOptions" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">Memory Usage</MudText>
|
||||
<MudChart ChartType="ChartType.Line"
|
||||
ChartSeries="@GetMemoryChartSeries(server)"
|
||||
XAxisLabels="@GetChartLabels(server)"
|
||||
Width="100%"
|
||||
Height="350px"
|
||||
ChartOptions="@chartOptions" />
|
||||
|
||||
<MudText Typo="Typo.subtitle2" Class="mb-2 mt-4">Active Tasks</MudText>
|
||||
<MudChart ChartType="ChartType.Line"
|
||||
ChartSeries="@GetTasksChartSeries(server)"
|
||||
XAxisLabels="@GetChartLabels(server)"
|
||||
Width="100%"
|
||||
Height="350px"
|
||||
ChartOptions="@chartOptions" />
|
||||
}
|
||||
else
|
||||
{
|
||||
<MudText Typo="Typo.body2" Color="Color.Secondary" Class="pa-4">Waiting for historical data...</MudText>
|
||||
}
|
||||
</MudExpansionPanel>
|
||||
</MudExpansionPanels>
|
||||
</MudCardContent>
|
||||
</MudCard>
|
||||
</MudItem>
|
||||
}
|
||||
</MudGrid>
|
||||
|
||||
@if (!servers.Any())
|
||||
{
|
||||
<MudAlert Severity="Severity.Warning">No servers configured. Please add server configurations to appsettings.json.</MudAlert>
|
||||
}
|
||||
|
||||
@code {
|
||||
private Dictionary<string, ServerConnection> servers = new();
|
||||
private readonly ChartOptions chartOptions = new ChartOptions
|
||||
{
|
||||
YAxisTicks = 5,
|
||||
MaxNumYAxisTicks = 10,
|
||||
YAxisLines = true,
|
||||
XAxisLines = false,
|
||||
};
|
||||
|
||||
protected override void OnInitialized()
|
||||
{
|
||||
servers = MetricsCollector.GetServers().ToDictionary(kvp => kvp.Key, kvp => kvp.Value);
|
||||
MetricsCollector.MetricsUpdated += OnMetricsUpdated;
|
||||
}
|
||||
|
||||
private void OnMetricsUpdated(object? sender, ServerConnection server)
|
||||
{
|
||||
InvokeAsync(StateHasChanged);
|
||||
}
|
||||
|
||||
private List<ChartSeries> GetCpuChartSeries(ServerConnection server)
|
||||
{
|
||||
return new List<ChartSeries>
|
||||
{
|
||||
new ChartSeries
|
||||
{
|
||||
Name = "CPU %",
|
||||
Data = server.MetricsHistory.Select(m => m.CpuUsage).ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<ChartSeries> GetMemoryChartSeries(ServerConnection server)
|
||||
{
|
||||
return new List<ChartSeries>
|
||||
{
|
||||
new ChartSeries
|
||||
{
|
||||
Name = "Memory MB",
|
||||
Data = server.MetricsHistory.Select(m => m.TotalMemoryUsage / 1024.0 / 1024.0).ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private List<ChartSeries> GetTasksChartSeries(ServerConnection server)
|
||||
{
|
||||
return new List<ChartSeries>
|
||||
{
|
||||
new ChartSeries
|
||||
{
|
||||
Name = "Active",
|
||||
Data = server.MetricsHistory.Select(m => (double)m.ActiveTasks).ToArray()
|
||||
},
|
||||
new ChartSeries
|
||||
{
|
||||
Name = "Max",
|
||||
Data = server.MetricsHistory.Select(m => (double)m.MaxConcurrentTasks).ToArray()
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
private string[] GetChartLabels(ServerConnection server)
|
||||
{
|
||||
return server.MetricsHistory.Select((m, i) =>
|
||||
i % Math.Max(1, server.MetricsHistory.Count / 10) == 0
|
||||
? m.Timestamp.ToLocalTime().ToString("HH:mm:ss")
|
||||
: string.Empty
|
||||
).ToArray();
|
||||
}
|
||||
|
||||
private string FormatBytes(long bytes)
|
||||
{
|
||||
string[] sizes = { "B", "KB", "MB", "GB", "TB" };
|
||||
double len = bytes;
|
||||
int order = 0;
|
||||
while (len >= 1024 && order < sizes.Length - 1)
|
||||
{
|
||||
order++;
|
||||
len /= 1024;
|
||||
}
|
||||
return $"{len:0.##} {sizes[order]}";
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
MetricsCollector.MetricsUpdated -= OnMetricsUpdated;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
<Router AppAssembly="typeof(Program).Assembly">
|
||||
<Found Context="routeData">
|
||||
<RouteView RouteData="routeData" DefaultLayout="typeof(Layout.MainLayout)" />
|
||||
<FocusOnNavigate RouteData="routeData" Selector="h1" />
|
||||
</Found>
|
||||
</Router>
|
||||
@@ -0,0 +1,10 @@
|
||||
@using System.Net.Http.Headers
|
||||
@using Microsoft.AspNetCore.Components.Routing
|
||||
@using Microsoft.AspNetCore.Components.Web
|
||||
@using static Microsoft.AspNetCore.Components.Web.RenderMode
|
||||
@using Microsoft.AspNetCore.Components.Forms
|
||||
@using Microsoft.JSInterop
|
||||
@using RemoteExec.Dashboard
|
||||
@using RemoteExec.Dashboard.Components
|
||||
@using RemoteExec.Dashboard.Components.Layout
|
||||
@using MudBlazor
|
||||
@@ -0,0 +1,17 @@
|
||||
namespace RemoteExec.Dashboard.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for the dashboard.
|
||||
/// </summary>
|
||||
public class DashboardConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the list of servers to monitor.
|
||||
/// </summary>
|
||||
public List<ServerConfiguration> Servers { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the maximum number of metrics history entries to keep per server.
|
||||
/// </summary>
|
||||
public int MaxMetricsHistoryCount { get; set; } = 100;
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
namespace RemoteExec.Dashboard.Configuration;
|
||||
|
||||
/// <summary>
|
||||
/// Configuration for a single server connection.
|
||||
/// </summary>
|
||||
public class ServerConfiguration
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the server.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of the server.
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the API key for authentication.
|
||||
/// </summary>
|
||||
public string ApiKey { get; set; } = string.Empty;
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
using RemoteExec.Shared.Models;
|
||||
|
||||
namespace RemoteExec.Dashboard.Models;
|
||||
|
||||
/// <summary>
|
||||
/// Represents a server connection with its metrics history.
|
||||
/// </summary>
|
||||
public class ServerConnection
|
||||
{
|
||||
/// <summary>
|
||||
/// Gets or sets the display name of the server.
|
||||
/// </summary>
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the URL of the server.
|
||||
/// </summary>
|
||||
public string Url { get; set; } = string.Empty;
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets whether the server is currently connected.
|
||||
/// </summary>
|
||||
public bool IsConnected { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the current metrics for the server.
|
||||
/// </summary>
|
||||
public ServerMetrics? CurrentMetrics { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the historical metrics data.
|
||||
/// </summary>
|
||||
public List<ServerMetrics> MetricsHistory { get; set; } = [];
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the last error message.
|
||||
/// </summary>
|
||||
public string? LastError { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
using MudBlazor.Services;
|
||||
using RemoteExec.Dashboard.Configuration;
|
||||
using RemoteExec.Dashboard.Services;
|
||||
|
||||
WebApplicationBuilder builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
builder.Services.AddRazorComponents()
|
||||
.AddInteractiveServerComponents();
|
||||
|
||||
builder.Services.AddMudServices();
|
||||
|
||||
builder.Services.Configure<DashboardConfiguration>(builder.Configuration.GetSection("Dashboard"));
|
||||
|
||||
builder.Services.AddSingleton<MetricsCollectorService>();
|
||||
builder.Services.AddHostedService(sp => sp.GetRequiredService<MetricsCollectorService>());
|
||||
|
||||
WebApplication app = builder.Build();
|
||||
|
||||
if (!app.Environment.IsDevelopment())
|
||||
{
|
||||
_ = app.UseExceptionHandler("/Error");
|
||||
_ = app.UseHsts();
|
||||
}
|
||||
|
||||
app.UseHttpsRedirection();
|
||||
app.UseAntiforgery();
|
||||
|
||||
app.MapStaticAssets();
|
||||
app.MapRazorComponents<RemoteExec.Dashboard.Components.App>()
|
||||
.AddInteractiveServerRenderMode();
|
||||
|
||||
await app.RunAsync();
|
||||
@@ -0,0 +1,22 @@
|
||||
{
|
||||
"profiles": {
|
||||
"http": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "http://localhost:5100",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
},
|
||||
"https": {
|
||||
"commandName": "Project",
|
||||
"dotnetRunMessages": true,
|
||||
"launchBrowser": true,
|
||||
"applicationUrl": "https://localhost:7100;http://localhost:5100",
|
||||
"environmentVariables": {
|
||||
"ASPNETCORE_ENVIRONMENT": "Development"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.SignalR.Client" Version="10.0.1" />
|
||||
<PackageReference Include="MudBlazor" Version="8.15.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\RemoteExec.Shared\RemoteExec.Shared.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,150 @@
|
||||
using Microsoft.AspNetCore.SignalR.Client;
|
||||
using Microsoft.Extensions.Options;
|
||||
|
||||
using RemoteExec.Dashboard.Configuration;
|
||||
using RemoteExec.Dashboard.Models;
|
||||
using RemoteExec.Shared.Models;
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
namespace RemoteExec.Dashboard.Services;
|
||||
|
||||
/// <summary>
|
||||
/// Background service that collects metrics from configured servers.
|
||||
/// </summary>
|
||||
public class MetricsCollectorService : BackgroundService
|
||||
{
|
||||
private readonly ILogger<MetricsCollectorService> logger;
|
||||
private readonly DashboardConfiguration configuration;
|
||||
private readonly ConcurrentDictionary<string, ServerConnection> servers = new();
|
||||
private readonly ConcurrentDictionary<string, HubConnection> connections = new();
|
||||
|
||||
public event EventHandler<ServerConnection>? MetricsUpdated;
|
||||
|
||||
public MetricsCollectorService(ILogger<MetricsCollectorService> logger, IOptions<DashboardConfiguration> options)
|
||||
{
|
||||
this.logger = logger;
|
||||
configuration = options.Value;
|
||||
}
|
||||
|
||||
public IReadOnlyDictionary<string, ServerConnection> GetServers()
|
||||
{
|
||||
return servers;
|
||||
}
|
||||
|
||||
protected override async Task ExecuteAsync(CancellationToken stoppingToken)
|
||||
{
|
||||
foreach (ServerConfiguration serverConfig in configuration.Servers)
|
||||
{
|
||||
ServerConnection server = new ServerConnection
|
||||
{
|
||||
Name = serverConfig.Name,
|
||||
Url = serverConfig.Url,
|
||||
IsConnected = false
|
||||
};
|
||||
|
||||
_ = servers.TryAdd(serverConfig.Url, server);
|
||||
_ = Task.Run(() => ConnectToServerAsync(serverConfig, server, stoppingToken), stoppingToken);
|
||||
}
|
||||
|
||||
await Task.Delay(Timeout.Infinite, stoppingToken);
|
||||
}
|
||||
|
||||
private async Task ConnectToServerAsync(Configuration.ServerConfiguration serverConfig, ServerConnection server, CancellationToken cancellationToken)
|
||||
{
|
||||
while (!cancellationToken.IsCancellationRequested)
|
||||
{
|
||||
try
|
||||
{
|
||||
HubConnection? connection = connections.GetOrAdd(serverConfig.Url, __ =>
|
||||
{
|
||||
HubConnection conn = new HubConnectionBuilder()
|
||||
.WithUrl(serverConfig.Url, options =>
|
||||
{
|
||||
options.Headers.Add("X-API-Key", serverConfig.ApiKey);
|
||||
})
|
||||
.WithAutomaticReconnect()
|
||||
.Build();
|
||||
|
||||
_ = conn.On<ServerMetrics>("MetricsUpdated", metrics =>
|
||||
{
|
||||
UpdateServerMetrics(server, metrics);
|
||||
});
|
||||
|
||||
conn.Closed += async error =>
|
||||
{
|
||||
server.IsConnected = false;
|
||||
server.LastError = error?.Message;
|
||||
MetricsUpdated?.Invoke(this, server);
|
||||
logger.LogWarning("Connection to {ServerName} closed. Error: {Error}", server.Name, error?.Message);
|
||||
};
|
||||
|
||||
conn.Reconnecting += error =>
|
||||
{
|
||||
server.IsConnected = false;
|
||||
server.LastError = error?.Message;
|
||||
MetricsUpdated?.Invoke(this, server);
|
||||
logger.LogInformation("Reconnecting to {ServerName}...", server.Name);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
conn.Reconnected += connectionId =>
|
||||
{
|
||||
server.IsConnected = true;
|
||||
server.LastError = null;
|
||||
MetricsUpdated?.Invoke(this, server);
|
||||
logger.LogInformation("Reconnected to {ServerName}", server.Name);
|
||||
return Task.CompletedTask;
|
||||
};
|
||||
|
||||
return conn;
|
||||
});
|
||||
|
||||
if (connection.State == HubConnectionState.Disconnected)
|
||||
{
|
||||
await connection.StartAsync(cancellationToken);
|
||||
server.IsConnected = true;
|
||||
server.LastError = null;
|
||||
logger.LogInformation("Connected to {ServerName} at {Url}", server.Name, serverConfig.Url);
|
||||
|
||||
ServerMetrics initialMetrics = await connection.InvokeAsync<ServerMetrics>("GetMetrics", cancellationToken);
|
||||
UpdateServerMetrics(server, initialMetrics);
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
server.IsConnected = false;
|
||||
server.LastError = ex.Message;
|
||||
MetricsUpdated?.Invoke(this, server);
|
||||
logger.LogError(ex, "Error connecting to {ServerName} at {Url}. Retrying in 5 seconds...", server.Name, serverConfig.Url);
|
||||
await Task.Delay(TimeSpan.FromSeconds(5), cancellationToken);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateServerMetrics(ServerConnection server, ServerMetrics metrics)
|
||||
{
|
||||
server.CurrentMetrics = metrics;
|
||||
server.MetricsHistory.Add(metrics);
|
||||
|
||||
if (server.MetricsHistory.Count > configuration.MaxMetricsHistoryCount)
|
||||
{
|
||||
server.MetricsHistory.RemoveAt(0);
|
||||
}
|
||||
|
||||
MetricsUpdated?.Invoke(this, server);
|
||||
}
|
||||
|
||||
public override async Task StopAsync(CancellationToken cancellationToken)
|
||||
{
|
||||
foreach (HubConnection connection in connections.Values)
|
||||
{
|
||||
await connection.StopAsync(cancellationToken);
|
||||
await connection.DisposeAsync();
|
||||
}
|
||||
|
||||
await base.StopAsync(cancellationToken);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"Logging": {
|
||||
"LogLevel": {
|
||||
"Default": "Information",
|
||||
"Microsoft.AspNetCore": "Warning"
|
||||
}
|
||||
},
|
||||
"AllowedHosts": "*",
|
||||
|
||||
// Dashboard configuration
|
||||
"Dashboard": {
|
||||
// List of RemoteExec servers to monitor
|
||||
"Servers": [
|
||||
// Example:
|
||||
// {
|
||||
// "Name": "Production Server",
|
||||
// "Url": "https://server.example.com/remote",
|
||||
// "ApiKey": "your-api-key-here"
|
||||
// }
|
||||
],
|
||||
|
||||
// Maximum number of historical metrics data points to keep per server
|
||||
// Higher values = longer history, more memory usage
|
||||
// Default: 100
|
||||
"MaxMetricsHistoryCount": 100
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,7 @@
|
||||
<Solution>
|
||||
<Project Path="RemoteExec.Client.DependencyInjection/RemoteExec.Client.DependencyInjection.csproj" Id="e9895828-9e57-475e-98ea-2afff271b7e9" />
|
||||
<Project Path="RemoteExec.Client/RemoteExec.Client.csproj" Id="8ac1533b-f69a-4b1b-bb3a-fb381cd4bb7b" />
|
||||
<Project Path="RemoteExec.Dashboard/RemoteExec.Dashboard.csproj" />
|
||||
<Project Path="RemoteExec.Generators/RemoteExec.Generators.csproj" Id="71a394a7-22f4-4c59-b91f-744c48161696" />
|
||||
<Project Path="RemoteExec.Server/RemoteExec.Server.csproj" Id="f88cb8bb-8221-4e5e-880a-0c58e1255602" />
|
||||
<Project Path="RemoteExec.Shared/RemoteExec.Shared.csproj" Id="99317a9c-c0ad-42a2-a830-56aa9c6b9dd4" />
|
||||
|
||||
Reference in New Issue
Block a user