feat(backend): add admin evaluation history, engine, simulation, bot API controllers and global exception middleware
This commit is contained in:
@@ -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
|
||||
));
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user