From 567ddea46abe371961619ecfe704a278870e5675 Mon Sep 17 00:00:00 2001 From: Kleidukos Date: Tue, 1 Sep 2026 17:38:57 +0200 Subject: [PATCH] chore(infra): update compose configurations, mqtt health bridge, and export scripts --- Docker/export_to_server.ps1 | 1 + .../Controllers/AdminSettingsController.cs | 2 + FinlyticBackend/Program.cs | 2 +- .../Services/SystemHealthBackgroundService.cs | 1 + FinlyticBackend/Util/BackendMqttBridge.cs | 26 ++++++ FinlyticCore/Dtos/MqttRequestDtos.cs | 7 ++ FinlyticCore/Dtos/Trading/EngineTradeDtos.cs | 1 + FinlyticCore/Util/MqttTopics.cs | 13 +++ compose.yaml | 85 ++++++------------- 9 files changed, 76 insertions(+), 62 deletions(-) diff --git a/Docker/export_to_server.ps1 b/Docker/export_to_server.ps1 index 92a169a..31dfd52 100644 --- a/Docker/export_to_server.ps1 +++ b/Docker/export_to_server.ps1 @@ -23,6 +23,7 @@ $images = @( "finlyticengine", "finlyticsimulation", "finlyticbot", + "finlyticnotify", "finlyticbackend" ) diff --git a/FinlyticBackend/Controllers/AdminSettingsController.cs b/FinlyticBackend/Controllers/AdminSettingsController.cs index cdab9d8..4c79219 100644 --- a/FinlyticBackend/Controllers/AdminSettingsController.cs +++ b/FinlyticBackend/Controllers/AdminSettingsController.cs @@ -83,6 +83,7 @@ public class AdminSettingsController : ControllerBase ["FinlyticAnalyzer"] = "engine", ["FinlyticTrades"] = "engine", ["FinlyticBot"] = "bot", + ["FinlyticNotify"] = "notify", // 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. @@ -209,6 +210,7 @@ public class AdminSettingsController : ControllerBase ("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"), + ("FinlyticNotify", $"{MqttTopics.Channels.HealthPing}/FinlyticNotify", "ntfy Push Notifications", "PostgreSQL / Mosquitto"), }; var results = new List diff --git a/FinlyticBackend/Program.cs b/FinlyticBackend/Program.cs index a5ac082..83d6995 100644 --- a/FinlyticBackend/Program.cs +++ b/FinlyticBackend/Program.cs @@ -149,7 +149,7 @@ app.UseExceptionHandler(); app.UseForwardedHeaders(new ForwardedHeadersOptions { ForwardedHeaders = ForwardedHeaders.XForwardedFor | ForwardedHeaders.XForwardedProto | ForwardedHeaders.XForwardedHost, - KnownNetworks = { }, + KnownIPNetworks = { }, KnownProxies = { } }); diff --git a/FinlyticBackend/Services/SystemHealthBackgroundService.cs b/FinlyticBackend/Services/SystemHealthBackgroundService.cs index 974ee0b..9472ea0 100644 --- a/FinlyticBackend/Services/SystemHealthBackgroundService.cs +++ b/FinlyticBackend/Services/SystemHealthBackgroundService.cs @@ -70,6 +70,7 @@ public class SystemHealthBackgroundService : BackgroundService ("FinlyticFundamentals", "health_Ping/FinlyticFundamentals", "Financial Statements & Estimates", "PostgreSQL fundamentals"), ("FinlyticEngine", "health_Ping/FinlyticEngine", "Strategy Screener & Signals", "PostgreSQL engine"), ("FinlyticBot", "health_Ping/FinlyticBot", "Automated Trading Execution", "PostgreSQL bot"), + ("FinlyticNotify", "health_Ping/FinlyticNotify", "ntfy Push Notifications", "PostgreSQL / Mosquitto"), }; var results = new List diff --git a/FinlyticBackend/Util/BackendMqttBridge.cs b/FinlyticBackend/Util/BackendMqttBridge.cs index fa2d91a..7de5f24 100644 --- a/FinlyticBackend/Util/BackendMqttBridge.cs +++ b/FinlyticBackend/Util/BackendMqttBridge.cs @@ -9,6 +9,7 @@ using FinlyticBackend.Database; using FinlyticBackend.Hubs; using FinlyticBackend.Services; using FinlyticBackend.Settings; +using FinlyticCore.Dtos; using FinlyticCore.Dtos.Bot; using FinlyticCore.Dtos.Logging; using FinlyticCore.Dtos.News; @@ -104,6 +105,11 @@ public class BackendMqttBridge : ManagedMqttClient, IHostedService MqttTopics.RequestFilter(MqttTopics.Channels.BackendGetAggregatedFavorites), HandleGetAggregatedFavoritesRpcAsync); + // Username lookup RPC Endpoint for FinlyticNotify + await SubscribeRpcAsync( + MqttTopics.RequestFilter(MqttTopics.Channels.BackendGetUsername), + HandleGetUsernameRpcAsync); + // FinlyticBackend previously never broadcast its OWN structured logs at all (it only relayed other // services' logs received on MqttTopics.LogsWildcard, subscribed above) - it never used // IFinlyticLogger, so FinlyticLogBroadcaster.Broadcast was never invoked for anything happening @@ -286,4 +292,24 @@ public class BackendMqttBridge : ManagedMqttClient, IHostedService "[BackendMqttBridge] Responded to backend_GetAggregatedFavorites with {Count} unique ISINs. [CorrelationId: {CorrelationId}]", isins.Count, correlationId); return isins; } + + private async Task HandleGetUsernameRpcAsync(UserIdRequest? req, string correlationId) + { + if (req == null || req.UserId == Guid.Empty) return null; + + using var scope = _scopeFactory.CreateScope(); + var dbContext = scope.ServiceProvider.GetRequiredService(); + + var user = await dbContext.Users + .AsNoTracking() + .FirstOrDefaultAsync(u => u.Id == req.UserId); + + if (user == null) return null; + + string username = !string.IsNullOrWhiteSpace(user.FullName) + ? user.FullName.Trim().ToLowerInvariant().Replace(" ", "_") + : user.Email.Split('@')[0].Trim().ToLowerInvariant(); + + return username; + } } \ No newline at end of file diff --git a/FinlyticCore/Dtos/MqttRequestDtos.cs b/FinlyticCore/Dtos/MqttRequestDtos.cs index fbd4532..7174f2f 100644 --- a/FinlyticCore/Dtos/MqttRequestDtos.cs +++ b/FinlyticCore/Dtos/MqttRequestDtos.cs @@ -245,6 +245,13 @@ public record CreateManualTradeRequest( [property: JsonPropertyName("fee")] decimal Fee = 0m ); +/// +/// Generic request payload carrying a user GUID to resolve user identity across microservices. +/// +public record UserIdRequest( + [property: JsonPropertyName("userId")] Guid UserId +); + /// /// Machine-readable classification of a server-side RPC fault, carried by so a /// caller can react to the specific failure mode instead of only learning "something went wrong" (or, before diff --git a/FinlyticCore/Dtos/Trading/EngineTradeDtos.cs b/FinlyticCore/Dtos/Trading/EngineTradeDtos.cs index df15250..9040e32 100644 --- a/FinlyticCore/Dtos/Trading/EngineTradeDtos.cs +++ b/FinlyticCore/Dtos/Trading/EngineTradeDtos.cs @@ -108,6 +108,7 @@ public record TradeFillDto( public record ActiveTradeDto( [property: JsonPropertyName("tradeId")] Guid TradeId, [property: JsonPropertyName("proposalId")] Guid ProposalId, + [property: JsonPropertyName("userId")] Guid UserId, [property: JsonPropertyName("underlyingIsin")] string UnderlyingIsin, [property: JsonPropertyName("symbol")] string Symbol, [property: JsonPropertyName("derivativeIsin")] string? DerivativeIsin, diff --git a/FinlyticCore/Util/MqttTopics.cs b/FinlyticCore/Util/MqttTopics.cs index 270dd70..f022fe9 100644 --- a/FinlyticCore/Util/MqttTopics.cs +++ b/FinlyticCore/Util/MqttTopics.cs @@ -279,6 +279,14 @@ public static class MqttTopics /// Served by FinlyticBot: applies dynamic setting updates for the service. public const string BotSettingsUpdate = "bot_settings_Update"; + // ---- FinlyticNotify ---- + + /// Served by FinlyticNotify: returns all dynamic settings for the service. + public const string NotifySettingsGetAll = "notify_settings_GetAll"; + + /// Served by FinlyticNotify: applies dynamic setting updates for the service. + public const string NotifySettingsUpdate = "notify_settings_Update"; + // ---- FinlyticBackend ---- /// @@ -286,6 +294,11 @@ public static class MqttTopics /// even though FinlyticBackend is outside this refactor's scope, so no future service hardcodes it again. /// public const string BackendGetAggregatedFavorites = "backend_GetAggregatedFavorites"; + + /// + /// Served by FinlyticBackend: resolves and returns the clean username for a given UserId GUID. + /// + public const string BackendGetUsername = "backend_GetUsername"; } // --------------------------------------------------------------------------------------------------- diff --git a/compose.yaml b/compose.yaml index 3975104..0b28f1a 100644 --- a/compose.yaml +++ b/compose.yaml @@ -217,25 +217,37 @@ services: # See finlyticassets above: broker has no auth configured yet, do not enable. #- MQTT__Username=${MQTT_USERNAME:-admin} #- MQTT__Password=${MQTT_PASSWORD} - - MQTT__ClientId=finlytic_bot - - Alpaca__KeyId=${ALPACA_KEY_ID:-PK_PAPER_PLACEHOLDER_KEY} - - Alpaca__SecretKey=${ALPACA_SECRET_KEY:-SK_PAPER_PLACEHOLDER_SECRET} - Alpaca__IsPaper=true + finlyticnotify: + image: finlyticnotify + build: + context: . + dockerfile: FinlyticNotify/Dockerfile + restart: unless-stopped + networks: + - postgres-network + environment: + - ConnectionStrings__DefaultConnection=Host=${DB_HOST:-OmniDB};Port=${DB_PORT:-5432};Database=finlytic_notify;Username=admin;Password=${DB_PASSWORD} + - MQTT__Host=${MQTT_HOST:-host.docker.internal} + - MQTT__Port=${MQTT_PORT:-4545} + - MQTT__ClientId=finlytic_notify + - Ntfy__BaseUrl=${NTFY_BASE_URL:-http://host.docker.internal:8080} + - Ntfy__TopicPrefix=finlytic + - Ntfy__BroadcastChannel=broadcast + - Ntfy__NewsChannel=news + - Ntfy__DefaultUsername=admin + - Ntfy__MinProposalScore=70.0 + - Ntfy__NotifyOnProposals=true + - Ntfy__NotifyOnTradeUpdates=true + - Ntfy__NotifyOnBotTrades=true + - Ntfy__NotifyOnNews=true + finlyticbackend: image: finlyticbackend build: context: . dockerfile: FinlyticBackend/Dockerfile - # on-failure (bounded), NOT unless-stopped: this is the one service that - # deliberately throws at startup if JWT_SECRET_KEY / ADMIN_DEFAULT_PASSWORD - # are missing or too weak (see Program.cs fail-fast guards). An - # unless-stopped policy would crash-loop that misconfiguration forever, - # burning CPU/log volume while masking the real problem. A bounded - # on-failure still recovers from transient startup races (e.g. DB not - # yet reachable) but eventually settles into a visibly "Exited" container - # (`docker compose ps`) once the retries are exhausted, surfacing a - # persistent config error instead of hiding it. restart: on-failure:5 ports: - "5000:8080" @@ -254,55 +266,6 @@ services: volumes: - ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/index:/app/assets/index - ${FINLYTIC_DATA_ROOT:-C:/Users/larsh/Documents/docker/finlytic}/assets/logos:/app/assets/logos - # Honest healthcheck: actually opens a TCP connection to the real Kestrel - # port and parses the real HTTP status line from the real GET /health - # endpoint (Program.cs, AllowAnonymous, no auth required). The final image - # (mcr.microsoft.com/dotnet/aspnet:10.0) has neither curl nor wget - # installed (verified) — installing one just for this would add an extra - # apt layer, so instead we use bash's built-in /dev/tcp (bash itself IS - # present in the base image, verified), invoked directly via exec form so - # it does not go through /bin/sh (which is dash on this image and does - # NOT support /dev/tcp). - healthcheck: - test: - - CMD - - bash - - -c - - >- - exec 3<>/dev/tcp/127.0.0.1/8080 && - printf 'GET /health HTTP/1.1\r\nHost: localhost\r\nConnection: close\r\n\r\n' >&3 && - head -n1 <&3 | grep -q '200' - interval: 30s - timeout: 5s - retries: 3 - start_period: 20s - - # ────────────────────────────────────────────────────────────────────────── - # No HEALTHCHECK on the 8 worker services above (finlyticassets, finlyticnews, - # finlyticfundamentals, finlyticsentiment, finlytictechnicals, finlyticengine, - # finlyticsimulation, finlyticbot) — and this is deliberate, not an omission: - # - # - They are Microsoft.NET.Sdk.Worker projects and MUST NOT host an HTTP - # server (Rules.md §5), so there is no `GET /health`-style endpoint to - # probe, by design. - # - Docker already restarts/reports a dead PID 1 via the `restart` policy - # above without any HEALTHCHECK — a HEALTHCHECK only adds value if it - # can distinguish "process alive but broken" from "process alive and - # working", which requires touching something specific to the app. - # - The only things reachable from inside these containers without an - # app-level probe endpoint are the external DB/MQTT dependencies - # themselves (e.g. via bash's /dev/tcp, as used for finlyticbackend - # above). But a bare TCP-reachability check to OmniDB/MQTT tests the - # network path, not the worker — it would report "healthy" while the - # worker is deadlocked, and "unhealthy" during a legitimate external - # outage the worker's own retry logic is already handling. That is - # placebo/misleading in both directions, not an honest signal. - # - # Conclusion: no meaningful, non-cosmetic healthcheck is possible here - # without adding an HTTP endpoint (forbidden by Rules.md §5). Leaving - # HEALTHCHECK unset is the honest choice. - # ────────────────────────────────────────────────────────────────────────── - networks: postgres-network: external: true