Files
OTSSignsOrchestrator/OTSSignsOrchestrator.Desktop/Services/ServerSignalRService.cs
Matt Batchelder c6d46098dd feat: Implement provisioning pipelines for subscription management
- 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.
2026-03-18 10:27:26 -04:00

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