using OTSSignsOrchestrator.Server.Clients; using OTSSignsOrchestrator.Server.Data; using OTSSignsOrchestrator.Server.Data.Entities; namespace OTSSignsOrchestrator.Server.Health.Checks; /// /// Verifies all 4 expected Xibo groups exist for the instance: /// {abbrev}-viewer, {abbrev}-editor, {abbrev}-admin, ots-it-{abbrev}. /// Uses to avoid pagination truncation. /// public sealed class GroupStructureHealthCheck : IHealthCheck { private readonly XiboClientFactory _clientFactory; private readonly IServiceProvider _services; private readonly ILogger _logger; public string CheckName => "GroupStructure"; public bool AutoRemediate => true; public GroupStructureHealthCheck( XiboClientFactory clientFactory, IServiceProvider services, ILogger logger) { _clientFactory = clientFactory; _services = services; _logger = logger; } public async Task RunAsync(Instance instance, CancellationToken ct) { var (client, abbrev) = await ResolveAsync(instance); if (client is null) return new HealthCheckResult(HealthStatus.Critical, "No OAuth app — cannot verify groups"); var expected = ExpectedGroups(abbrev); var groups = await client.GetAllPagesAsync( (start, length) => client.GetGroupsAsync(start, length)); var existing = groups .Select(g => g.TryGetValue("group", out var n) ? n?.ToString() : null) .Where(n => n is not null) .ToHashSet(StringComparer.OrdinalIgnoreCase); var missing = expected.Where(e => !existing.Contains(e)).ToList(); if (missing.Count == 0) return new HealthCheckResult(HealthStatus.Healthy, "All 4 expected groups present"); return new HealthCheckResult( HealthStatus.Critical, $"Missing groups: {string.Join(", ", missing)}", $"Expected: {string.Join(", ", expected)}"); } public async Task RemediateAsync(Instance instance, CancellationToken ct) { var (client, abbrev) = await ResolveAsync(instance); if (client is null) return false; await using var scope = _services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); var expected = ExpectedGroups(abbrev); var groups = await client.GetAllPagesAsync( (start, length) => client.GetGroupsAsync(start, length)); var existing = groups .Select(g => g.TryGetValue("group", out var n) ? n?.ToString() : null) .Where(n => n is not null) .ToHashSet(StringComparer.OrdinalIgnoreCase); var allFixed = true; foreach (var name in expected.Where(e => !existing.Contains(e))) { var resp = await client.CreateGroupAsync(new CreateGroupRequest(name, $"Auto-created by health check for {abbrev}")); if (resp.IsSuccessStatusCode) { db.AuditLogs.Add(new AuditLog { Id = Guid.NewGuid(), InstanceId = instance.Id, Actor = "HealthCheckEngine:GroupStructure", Action = "CreateGroup", Target = name, Outcome = "Success", Detail = $"Recreated missing group {name}", OccurredAt = DateTime.UtcNow, }); } else { _logger.LogError("Failed to create group {Group}: {Err}", name, resp.Error?.Content); allFixed = false; } } await db.SaveChangesAsync(ct); return allFixed; } private static string[] ExpectedGroups(string abbrev) => [ $"{abbrev}-viewer", $"{abbrev}-editor", $"{abbrev}-admin", $"ots-it-{abbrev}", ]; private async Task<(IXiboApiClient? Client, string Abbrev)> ResolveAsync(Instance instance) { var abbrev = instance.Customer.Abbreviation; var oauthApp = instance.OauthAppRegistries.FirstOrDefault(); if (oauthApp is null) return (null, abbrev); var settings = _services.GetRequiredService(); var secret = await settings.GetAsync(Core.Services.SettingsService.InstanceOAuthSecretId(abbrev)); if (string.IsNullOrEmpty(secret)) return (null, abbrev); var client = await _clientFactory.CreateAsync(instance.XiboUrl, oauthApp.ClientId, secret); return (client, abbrev); } }