feat(backend): generic settings RPC bridge, LogStreamHub SignalR, and log ringbuffer

This commit is contained in:
2026-08-15 21:30:56 +02:00
parent 1f9d66405a
commit f18f75c1ab
5 changed files with 207 additions and 65 deletions
@@ -6,6 +6,7 @@ using System.Text.Json.Serialization;
using System.Threading.Tasks;
using FinlyticBackend.Util;
using FinlyticCore.Dtos;
using FinlyticCore.Dtos.Settings;
using FinlyticCore.Util;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Cors;
@@ -58,38 +59,18 @@ public class AdminSettingsController : ControllerBase
private readonly WebMqttClient _mqttClient;
private readonly ILogger<AdminSettingsController> _logger;
// In-memory static store for default UI configuration templates
private static readonly ConcurrentDictionary<string, List<ServiceConfigItem>> _inMemorySettings = new(StringComparer.OrdinalIgnoreCase);
static AdminSettingsController()
private static readonly Dictionary<string, string> ServiceRpcPrefixes = new(StringComparer.OrdinalIgnoreCase)
{
// Default-Templates für Microservice-Konfigurationen initialisieren
_inMemorySettings["FinlyticAnalyzer"] = new List<ServiceConfigItem>
{
new("MinSignalScore", "75.0", "double", "Mindest-Score für KI-Trade-Proposals (0-100)", DateTime.UtcNow),
new("VixPanicThreshold", "30.0", "double", "VIX-Wert ab dem Panik-Modus aktiviert wird", DateTime.UtcNow),
new("ProposalTtlMinutes", "180", "int", "Gültigkeitsdauer von Trade-Proposals in Minuten", DateTime.UtcNow)
};
["FinlyticFundamentals"] = "fundamentals",
["FinlyticNews"] = "news",
["FinlyticTechnicalAnalysis"] = "ta",
["FinlyticSentiment"] = "sentiment",
["FinlyticAnalyzer"] = "analyzer",
["FinlyticTrades"] = "trades",
["FinlyticAssets"] = "assets"
};
_inMemorySettings["FinlyticNews"] = new List<ServiceConfigItem>
{
new("ScrapeIntervalMinutes", "15", "int", "Intervall für das Scraping neuer Nachrichten", DateTime.UtcNow),
new("FinBertBatchSize", "8", "int", "Batch-Größe für die Sentiment-Analyse", DateTime.UtcNow)
};
_inMemorySettings["FinlyticTechnicalAnalysis"] = new List<ServiceConfigItem>
{
new("EmaShortPeriod", "20", "int", "Kurze Periode für EMA-Berechnungen", DateTime.UtcNow),
new("EmaLongPeriod", "50", "int", "Lange Periode für EMA-Berechnungen", DateTime.UtcNow),
new("RsiPeriod", "14", "int", "Standard-Periode für RSI-Berechnung", DateTime.UtcNow)
};
_inMemorySettings["FinlyticTrades"] = new List<ServiceConfigItem>
{
new("ExportFeedbackIntervalHours", "6", "int", "Intervall für den Parquet/JSON Feedback-Export", DateTime.UtcNow),
new("DefaultLeverageLimit", "10", "decimal", "Standardmäßiger Maximalhebel für Derivate", DateTime.UtcNow)
};
}
private static readonly ConcurrentDictionary<string, List<ServiceConfigItem>> _inMemorySettings = new(StringComparer.OrdinalIgnoreCase);
public AdminSettingsController(
WebMqttClient mqttClient,
@@ -100,11 +81,57 @@ public class AdminSettingsController : ControllerBase
}
/// <summary>
/// Retrieves all service configurations grouped by service name.
/// Retrieves recent buffered in-memory logs for a specific service.
/// </summary>
[HttpGet("logs/{serviceName}")]
public IActionResult GetServiceLogs(string serviceName)
{
if (BackendMqttBridge.ServiceLogsRingBuffer.TryGetValue(serviceName, out var queue))
{
return Ok(queue.ToList());
}
return Ok(new List<FinlyticCore.Dtos.Logging.LogMessageDto>());
}
/// <summary>
/// Retrieves all service configurations grouped by service name via live MQTT RPC queries.
/// </summary>
[HttpGet]
public IActionResult GetAllSettings()
public async Task<IActionResult> GetAllSettings()
{
if (_mqttClient.IsConnected)
{
var fetchTasks = ServiceRpcPrefixes.Select(async kvp =>
{
var serviceName = kvp.Key;
var prefix = kvp.Value;
try
{
var liveSettings = await _mqttClient.SendRpcRequestAsync<List<DynamicSettingDto>, string>(
$"{prefix}_settings_GetAll",
"",
TimeSpan.FromSeconds(2));
if (liveSettings != null && liveSettings.Count > 0)
{
_inMemorySettings[serviceName] = liveSettings.Select(d => new ServiceConfigItem(
Key: d.Key,
Value: d.Value?.ToString() ?? "",
DataType: d.Type,
Description: d.Description,
UpdatedAt: d.UpdatedAt ?? DateTime.UtcNow
)).ToList();
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[AdminSettings] Could not fetch live settings from {ServiceName} via MQTT.", serviceName);
}
});
await Task.WhenAll(fetchTasks);
}
var grouped = _inMemorySettings.ToDictionary(
g => g.Key,
g => g.Value.Select(item => new ServiceConfigItemResponseDto(
@@ -198,11 +225,37 @@ public class AdminSettingsController : ControllerBase
}
/// <summary>
/// Retrieves settings for a specific service.
/// Retrieves settings for a specific service via live MQTT RPC.
/// </summary>
[HttpGet("{serviceName}")]
public IActionResult GetServiceSettings(string serviceName)
public async Task<IActionResult> GetServiceSettings(string serviceName)
{
if (ServiceRpcPrefixes.TryGetValue(serviceName, out var prefix) && _mqttClient.IsConnected)
{
try
{
var liveSettings = await _mqttClient.SendRpcRequestAsync<List<DynamicSettingDto>, string>(
$"{prefix}_settings_GetAll",
"",
TimeSpan.FromSeconds(2));
if (liveSettings != null && liveSettings.Count > 0)
{
_inMemorySettings[serviceName] = liveSettings.Select(d => new ServiceConfigItem(
Key: d.Key,
Value: d.Value?.ToString() ?? "",
DataType: d.Type,
Description: d.Description,
UpdatedAt: d.UpdatedAt ?? DateTime.UtcNow
)).ToList();
}
}
catch (Exception ex)
{
_logger.LogWarning(ex, "[AdminSettings] Could not fetch live settings for {ServiceName} via MQTT.", serviceName);
}
}
if (_inMemorySettings.TryGetValue(serviceName, out var list))
{
var dtos = list.Select(item => new ServiceConfigItemResponseDto(
@@ -220,11 +273,10 @@ public class AdminSettingsController : ControllerBase
}
/// <summary>
/// Broadcasts configuration settings to the targeted microservice via MQTT.
/// Does NOT write to Backend database. The microservice persists updated settings directly into its own database.
/// Broadcasts configuration settings to the targeted microservice via MQTT RPC and updates in-memory cache.
/// </summary>
[HttpPut("{serviceName}")]
public async Task<IActionResult> UpdateServiceSettings(string serviceName, [FromBody] Dictionary<string, string> updatedValues)
public async Task<IActionResult> UpdateServiceSettings(string serviceName, [FromBody] Dictionary<string, object?> updatedValues)
{
if (updatedValues == null || !updatedValues.Any())
{
@@ -233,40 +285,32 @@ public class AdminSettingsController : ControllerBase
_logger.LogInformation("[AdminSettings] Transmitting {Count} config settings to microservice '{ServiceName}' via MQTT", updatedValues.Count, serviceName);
// In-Memory Template-Store aktualisieren
if (_inMemorySettings.TryGetValue(serviceName, out var existingList))
{
foreach (var (key, value) in updatedValues)
{
var idx = existingList.FindIndex(item => item.Key.Equals(key, StringComparison.OrdinalIgnoreCase));
if (idx >= 0)
{
var old = existingList[idx];
existingList[idx] = old with { Value = value, UpdatedAt = DateTime.UtcNow };
}
else
{
existingList.Add(new ServiceConfigItem(key, value, "string", $"Setting for {serviceName}", DateTime.UtcNow));
}
}
}
else
{
var newList = updatedValues.Select(kv => new ServiceConfigItem(kv.Key, kv.Value, "string", $"Setting for {serviceName}", DateTime.UtcNow)).ToList();
_inMemorySettings[serviceName] = newList;
}
// MQTT Config-Update Event senden
bool mqttPublished = false;
try
{
if (_mqttClient.IsConnected)
if (_mqttClient.IsConnected && ServiceRpcPrefixes.TryGetValue(serviceName, out var prefix))
{
var updated = await _mqttClient.SendRpcRequestAsync<List<DynamicSettingDto>, Dictionary<string, object?>>(
$"{prefix}_settings_Update",
updatedValues,
TimeSpan.FromSeconds(3));
if (updated != null && updated.Count > 0)
{
_inMemorySettings[serviceName] = updated.Select(d => new ServiceConfigItem(
Key: d.Key,
Value: d.Value?.ToString() ?? "",
DataType: d.Type,
Description: d.Description,
UpdatedAt: d.UpdatedAt ?? DateTime.UtcNow
)).ToList();
}
string topic = $"services/config/updated/{serviceName}";
var payload = new ServiceConfigUpdatePayload(
ServiceName: serviceName,
Timestamp: DateTime.UtcNow,
Settings: updatedValues
Settings: updatedValues.ToDictionary(kv => kv.Key, kv => kv.Value?.ToString() ?? "")
);
await _mqttClient.PublishAsync(topic, payload);