feat(backend): add admin evaluation history, engine, simulation, bot API controllers and global exception middleware

This commit is contained in:
2026-08-24 21:37:32 +02:00
parent 6974b2075b
commit 676496b77d
37 changed files with 88203 additions and 83075 deletions
@@ -4,12 +4,15 @@ using System.Collections.Generic;
using System.Linq;
using System.Text.Json.Serialization;
using System.Threading.Tasks;
using FinlyticBackend.Settings;
using FinlyticBackend.Util;
using FinlyticCore.Dtos;
using FinlyticCore.Dtos.Settings;
using FinlyticCore.Services;
using FinlyticCore.Util;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Logging;
@@ -37,6 +40,15 @@ public record ServiceConfigItemResponseDto(
[property: JsonPropertyName("updatedAt")] DateTime UpdatedAt
);
/// <summary>
/// Result DTO for a service settings update request, reporting whether the RPC broadcast to the
/// targeted microservice actually succeeded (AOT-compliant; replaces an anonymous response object).
/// </summary>
public record ServiceSettingsUpdateResultDto(
[property: JsonPropertyName("message")] string Message,
[property: JsonPropertyName("mqttDispatched")] bool MqttDispatched
);
/// <summary>
/// DTO representing the operational health status of a microservice (AOT-compliant).
/// </summary>
@@ -63,24 +75,55 @@ public class AdminSettingsController : ControllerBase
{
["FinlyticFundamentals"] = "fundamentals",
["FinlyticNews"] = "news",
["FinlyticTechnicals"] = "ta",
["FinlyticTechnicalAnalysis"] = "ta",
["FinlyticSentiment"] = "sentiment",
["FinlyticAnalyzer"] = "analyzer",
["FinlyticTrades"] = "trades",
["FinlyticAssets"] = "assets",
["FinlyticBot"] = "bot"
["FinlyticEngine"] = "engine",
["FinlyticAnalyzer"] = "engine",
["FinlyticTrades"] = "engine",
["FinlyticBot"] = "bot",
// FinlyticSimulation was previously missing from this map entirely, so its settings
// (SimulationSettingKeys: slippage/fee defaults, matrix-recompute schedule, etc.) never showed up
// in the admin UI's settings screen even though the sim_settings_GetAll/Update RPC channels exist.
["FinlyticSimulation"] = "sim"
};
/// <summary>
/// FinlyticBackend is the RPC *caller* for every entry in <see cref="ServiceRpcPrefixes"/> above, not a
/// callee - it has no MQTT-served "backend_settings_GetAll" channel to ask, because it would just be
/// asking itself over the network for no reason. Its own settings (<see cref="BackendSettingKeys"/>) are
/// instead read directly, in-process, via <see cref="_settingsService"/> - see the special-casing in
/// <see cref="GetAllSettings"/>/<see cref="GetServiceSettings"/>/<see cref="UpdateServiceSettings"/>.
/// </summary>
private const string BackendServiceName = "FinlyticBackend";
private static readonly ConcurrentDictionary<string, List<ServiceConfigItem>> _inMemorySettings = new(StringComparer.OrdinalIgnoreCase);
private readonly ISettingsService _settingsService;
public AdminSettingsController(
WebMqttClient mqttClient,
ISettingsService settingsService,
ILogger<AdminSettingsController> logger)
{
_mqttClient = mqttClient;
_settingsService = settingsService;
_logger = logger;
}
private async Task<List<ServiceConfigItemResponseDto>> GetBackendOwnSettingsAsync()
{
var settings = await _settingsService.GetAllRegisteredSettingsAsync(new[] { typeof(BackendSettingKeys) });
return settings.Select(d => new ServiceConfigItemResponseDto(
Key: d.Key,
Value: d.Value?.ToString() ?? "",
DataType: d.Type,
Description: d.Description,
UpdatedAt: d.UpdatedAt ?? DateTime.UtcNow
)).ToList();
}
/// <summary>
/// Retrieves recent buffered in-memory logs for a specific service.
/// </summary>
@@ -144,6 +187,10 @@ public class AdminSettingsController : ControllerBase
)).ToList()
);
// FinlyticBackend's own settings never go through the MQTT RPC loop above (see BackendServiceName's
// doc comment) - read directly, in-process.
grouped[BackendServiceName] = await GetBackendOwnSettingsAsync();
return Ok(grouped);
}
@@ -155,13 +202,13 @@ public class AdminSettingsController : ControllerBase
{
var servicesToCheck = new (string Name, string Channel, string Type, string Db)[]
{
("FinlyticAssets", "health_Ping/FinlyticAssets", "Asset Catalog & Scraper", "PostgreSQL assets"),
("FinlyticNews", "health_Ping/FinlyticNews", "News RSS Scraper & AI", "PostgreSQL news"),
("FinlyticTechnicalAnalysis", "health_Ping/FinlyticTechnicalAnalysis", "Technical Indicators (EMA/RSI)", "PostgreSQL ta"),
("FinlyticSentiment", "health_Ping/FinlyticSentiment", "NLP Sentiment Engine", "PostgreSQL sentiment"),
("FinlyticAnalyzer", "health_Ping/FinlyticAnalyzer", "Multi-Layer Signal Engine", "PostgreSQL analyzer"),
("FinlyticTrades", "health_Ping/FinlyticTrades", "Trade Lifecycle Manager", "PostgreSQL trades"),
("FinlyticFundamentals", "health_Ping/FinlyticFundamentals", "Financial Statements & Estimates", "PostgreSQL fundamentals"),
("FinlyticAssets", $"{MqttTopics.Channels.HealthPing}/FinlyticAssets", "Asset Catalog & Scraper", "PostgreSQL assets"),
("FinlyticNews", $"{MqttTopics.Channels.HealthPing}/FinlyticNews", "News RSS Scraper & AI", "PostgreSQL news"),
("FinlyticTechnicals", $"{MqttTopics.Channels.HealthPing}/FinlyticTechnicals", "Technical Indicators & SMC Patterns", "PostgreSQL ta"),
("FinlyticSentiment", $"{MqttTopics.Channels.HealthPing}/FinlyticSentiment", "NLP Sentiment Engine", "PostgreSQL sentiment"),
("FinlyticFundamentals", $"{MqttTopics.Channels.HealthPing}/FinlyticFundamentals", "Financial Statements & Estimates", "PostgreSQL fundamentals"),
("FinlyticEngine", $"{MqttTopics.Channels.HealthPing}/FinlyticEngine", "Strategy Screener & Signals", "PostgreSQL engine"),
("FinlyticBot", $"{MqttTopics.Channels.HealthPing}/FinlyticBot", "Automated Trading Execution", "PostgreSQL bot"),
};
var results = new List<ServiceHealthStatusDto>
@@ -231,6 +278,11 @@ public class AdminSettingsController : ControllerBase
[HttpGet("{serviceName}")]
public async Task<IActionResult> GetServiceSettings(string serviceName)
{
if (string.Equals(serviceName, BackendServiceName, StringComparison.OrdinalIgnoreCase))
{
return Ok(await GetBackendOwnSettingsAsync());
}
if (ServiceRpcPrefixes.TryGetValue(serviceName, out var prefix) && _mqttClient.IsConnected)
{
try
@@ -281,7 +333,17 @@ public class AdminSettingsController : ControllerBase
{
if (updatedValues == null || !updatedValues.Any())
{
return BadRequest(new { message = "No settings provided for update." });
return Problem(title: "No settings provided for update.", statusCode: StatusCodes.Status400BadRequest);
}
if (string.Equals(serviceName, BackendServiceName, StringComparison.OrdinalIgnoreCase))
{
// No MQTT round trip needed - FinlyticBackend updates its own dynamic settings directly, in-process.
await _settingsService.UpdateSettingsAsync(updatedValues);
return Ok(new ServiceSettingsUpdateResultDto(
Message: $"Settings successfully updated for {BackendServiceName}.",
MqttDispatched: false
));
}
_logger.LogInformation("[AdminSettings] Transmitting {Count} config settings to microservice '{ServiceName}' via MQTT", updatedValues.Count, serviceName);
@@ -307,16 +369,11 @@ public class AdminSettingsController : ControllerBase
)).ToList();
}
string topic = $"services/config/updated/{serviceName}";
var payload = new ServiceConfigUpdatePayload(
ServiceName: serviceName,
Timestamp: DateTime.UtcNow,
Settings: updatedValues.ToDictionary(kv => kv.Key, kv => kv.Value?.ToString() ?? "")
);
await _mqttClient.PublishAsync(topic, payload);
mqttPublished = true;
_logger.LogInformation("[AdminSettings] Broadcasted config update event to MQTT topic '{Topic}' for microservice persistence.", topic);
// Settings propagation happens entirely over the {prefix}_settings_Update RPC call above.
// There used to be an additional fire-and-forget MQTT publish to a per-service config-updated
// topic here, but no service in the fleet ever subscribed to it (dead legacy code predating the
// RPC mechanism) - removed rather than kept as a no-op broadcast.
mqttPublished = updated != null && updated.Count > 0;
}
}
catch (Exception ex)
@@ -324,10 +381,9 @@ public class AdminSettingsController : ControllerBase
_logger.LogError(ex, "[AdminSettings] Failed to publish MQTT config update event for service '{ServiceName}'", serviceName);
}
return Ok(new
{
message = $"Settings successfully transmitted to microservice {serviceName}.",
mqttDispatched = mqttPublished
});
return Ok(new ServiceSettingsUpdateResultDto(
Message: $"Settings successfully transmitted to microservice {serviceName}.",
MqttDispatched: mqttPublished
));
}
}