- Add ReactivatePipeline to handle subscription reactivation, including scaling Docker services, health verification, status updates, audit logging, and broadcasting status changes. - Introduce RotateCredentialsPipeline for OAuth2 credential rotation, managing the deletion of old apps, creation of new ones, credential storage, access verification, and audit logging. - Create StepRunner to manage job step execution, including lifecycle management and progress broadcasting via SignalR. - Implement SuspendPipeline for subscription suspension, scaling down services, updating statuses, logging audits, and broadcasting changes. - Add UpdateScreenLimitPipeline to update Xibo CMS screen limits and record snapshots. - Introduce XiboFeatureManifests for hardcoded feature ACLs per role. - Add docker-compose.dev.yml for local development with PostgreSQL setup.
113 lines
4.1 KiB
C#
113 lines
4.1 KiB
C#
using Avalonia.Threading;
|
|
using CommunityToolkit.Mvvm.Messaging;
|
|
using Microsoft.AspNetCore.SignalR.Client;
|
|
using Microsoft.Extensions.Configuration;
|
|
using Microsoft.Extensions.Logging;
|
|
|
|
namespace OTSSignsOrchestrator.Desktop.Services;
|
|
|
|
/// <summary>
|
|
/// Singleton service managing the persistent SignalR connection to the server's FleetHub.
|
|
/// All handlers dispatch to the UI thread and republish via <see cref="WeakReferenceMessenger"/>.
|
|
/// </summary>
|
|
public sealed class ServerSignalRService : IAsyncDisposable
|
|
{
|
|
private readonly HubConnection _connection;
|
|
private readonly ILogger<ServerSignalRService> _logger;
|
|
|
|
public ServerSignalRService(
|
|
TokenStoreService tokenStore,
|
|
IConfiguration config,
|
|
ILogger<ServerSignalRService> logger)
|
|
{
|
|
_logger = logger;
|
|
var baseUrl = config["ServerBaseUrl"] ?? "https://localhost:5001";
|
|
|
|
_connection = new HubConnectionBuilder()
|
|
.WithUrl($"{baseUrl}/hubs/fleet", options =>
|
|
{
|
|
options.AccessTokenProvider = () => Task.FromResult(tokenStore.GetJwt());
|
|
})
|
|
.WithAutomaticReconnect(new[] { TimeSpan.Zero, TimeSpan.FromSeconds(2), TimeSpan.FromSeconds(5), TimeSpan.FromSeconds(30) })
|
|
.Build();
|
|
|
|
RegisterHandlers();
|
|
|
|
_connection.Reconnecting += ex =>
|
|
{
|
|
_logger.LogWarning(ex, "SignalR reconnecting...");
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
_connection.Reconnected += connectionId =>
|
|
{
|
|
_logger.LogInformation("SignalR reconnected (connId={ConnectionId})", connectionId);
|
|
return Task.CompletedTask;
|
|
};
|
|
|
|
_connection.Closed += ex =>
|
|
{
|
|
_logger.LogWarning(ex, "SignalR connection closed");
|
|
return Task.CompletedTask;
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Starts the SignalR connection. Call from <c>App.OnFrameworkInitializationCompleted</c>.
|
|
/// Failures are logged but do not throw — automatic reconnect will retry.
|
|
/// </summary>
|
|
public async Task StartAsync()
|
|
{
|
|
try
|
|
{
|
|
await _connection.StartAsync();
|
|
_logger.LogInformation("SignalR connected to FleetHub");
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
_logger.LogWarning(ex, "SignalR initial connection failed — will retry via automatic reconnect");
|
|
}
|
|
}
|
|
|
|
public async Task StopAsync()
|
|
{
|
|
try { await _connection.StopAsync(); }
|
|
catch (Exception ex) { _logger.LogWarning(ex, "Error stopping SignalR connection"); }
|
|
}
|
|
|
|
public HubConnectionState State => _connection.State;
|
|
|
|
private void RegisterHandlers()
|
|
{
|
|
_connection.On<string, string, string>("SendJobCreated", (jobId, abbrev, jobType) =>
|
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|
WeakReferenceMessenger.Default.Send(
|
|
new JobCreatedMessage(new(jobId, abbrev, jobType)))));
|
|
|
|
_connection.On<string, string, int, string>("SendJobProgressUpdate", (jobId, stepName, pct, logLine) =>
|
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|
WeakReferenceMessenger.Default.Send(
|
|
new JobProgressUpdateMessage(new(jobId, stepName, pct, logLine)))));
|
|
|
|
_connection.On<string, bool, string>("SendJobCompleted", (jobId, success, summary) =>
|
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|
WeakReferenceMessenger.Default.Send(
|
|
new JobCompletedMessage(new(jobId, success, summary)))));
|
|
|
|
_connection.On<string, string>("SendInstanceStatusChanged", (customerId, status) =>
|
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|
WeakReferenceMessenger.Default.Send(
|
|
new InstanceStatusChangedMessage(new(customerId, status)))));
|
|
|
|
_connection.On<string, string>("SendAlertRaised", (severity, message) =>
|
|
Dispatcher.UIThread.InvokeAsync(() =>
|
|
WeakReferenceMessenger.Default.Send(
|
|
new AlertRaisedMessage(new(severity, message)))));
|
|
}
|
|
|
|
public async ValueTask DisposeAsync()
|
|
{
|
|
await _connection.DisposeAsync();
|
|
}
|
|
}
|