using OTSSignsOrchestrator.Server.Clients; using OTSSignsOrchestrator.Server.Data; using OTSSignsOrchestrator.Server.Data.Entities; namespace OTSSignsOrchestrator.Server.Health.Checks; /// /// Verifies the Xibo CMS theme is set to otssigns by calling GET /api/settings. /// Auto-remediates by calling PUT /api/settings if the theme is incorrect. /// public sealed class ThemeHealthCheck : IHealthCheck { private readonly XiboClientFactory _clientFactory; private readonly IServiceProvider _services; private readonly ILogger _logger; private const string ExpectedTheme = "otssigns"; public string CheckName => "Theme"; public bool AutoRemediate => true; public ThemeHealthCheck( XiboClientFactory clientFactory, IServiceProvider services, ILogger logger) { _clientFactory = clientFactory; _services = services; _logger = logger; } public async Task RunAsync(Instance instance, CancellationToken ct) { var (client, _) = await ResolveAsync(instance); if (client is null) return new HealthCheckResult(HealthStatus.Critical, "No OAuth app — cannot check theme"); try { var settingsResp = await client.GetSettingsAsync(); if (!settingsResp.IsSuccessStatusCode) return new HealthCheckResult(HealthStatus.Critical, $"GET /settings returned {settingsResp.StatusCode}"); var settings = settingsResp.Content; if (settings is null) return new HealthCheckResult(HealthStatus.Critical, "Settings response was null"); // Xibo returns settings as a list of { setting, value } objects or a dictionary var themeName = ExtractSetting(settings, "THEME_NAME"); if (string.Equals(themeName, ExpectedTheme, StringComparison.OrdinalIgnoreCase)) return new HealthCheckResult(HealthStatus.Healthy, $"Theme is {ExpectedTheme}"); return new HealthCheckResult(HealthStatus.Critical, $"Theme is '{themeName}', expected '{ExpectedTheme}'"); } catch (Exception ex) { return new HealthCheckResult(HealthStatus.Critical, $"Theme check failed: {ex.Message}"); } } public async Task RemediateAsync(Instance instance, CancellationToken ct) { var (client, _) = await ResolveAsync(instance); if (client is null) return false; try { var resp = await client.UpdateSettingsAsync( new UpdateSettingsRequest(new Dictionary { ["THEME_NAME"] = ExpectedTheme, })); if (resp.IsSuccessStatusCode) { await using var scope = _services.CreateAsyncScope(); var db = scope.ServiceProvider.GetRequiredService(); db.AuditLogs.Add(new AuditLog { Id = Guid.NewGuid(), InstanceId = instance.Id, Actor = "HealthCheckEngine:Theme", Action = "FixTheme", Target = instance.Customer.Abbreviation, Outcome = "Success", Detail = $"Reset THEME_NAME to {ExpectedTheme}", OccurredAt = DateTime.UtcNow, }); await db.SaveChangesAsync(ct); return true; } _logger.LogError("Failed to fix theme: {Err}", resp.Error?.Content); return false; } catch (Exception ex) { _logger.LogError(ex, "Theme remediation failed"); return false; } } private static string? ExtractSetting(object settingsObj, string key) { // Settings may come back as a dictionary or a list of objects if (settingsObj is System.Text.Json.JsonElement je) { if (je.ValueKind == System.Text.Json.JsonValueKind.Object && je.TryGetProperty(key, out var val)) return val.GetString(); if (je.ValueKind == System.Text.Json.JsonValueKind.Array) { foreach (var item in je.EnumerateArray()) { if (item.TryGetProperty("setting", out var settingProp) && string.Equals(settingProp.GetString(), key, StringComparison.OrdinalIgnoreCase) && item.TryGetProperty("value", out var valueProp)) { return valueProp.GetString(); } } } } return null; } 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); } }