From 25265c02f8ccccd39d5433e6f6ab4fb13f4a73bd Mon Sep 17 00:00:00 2001 From: Loretta Date: Tue, 14 Jul 2026 07:04:10 +0200 Subject: [PATCH] Add platform env-badge to app title via IAppTitleService Introduced IAppTitleService for platform-agnostic environment badge in the app title ("[PROD]" or "[DEV] FruitBank"), with implementations for Web (document.title), MAUI (window title), and server (no-op). Registered the service per platform in DI. MainLayout now updates the title reactively on environment changes via FruitBankConstClient.IsProdChanged. Improved event handling and prevented redundant environment hydration calls. No business logic affected; changes are UI/infrastructure only. --- FruitBank.Common/FruitBankConstClient.cs | 12 ++++++++++- .../Layout/MainLayout.razor.cs | 13 ++++++++++++ .../Services/IAppTitleService.cs | 11 ++++++++++ .../SignalRs/FruitBankSignalRClient.cs | 12 +++++++---- FruitBankHybrid.Web.Client/Program.cs | 1 + .../Services/WebAppTitleService.cs | 11 ++++++++++ FruitBankHybrid.Web/Program.cs | 1 + .../Services/ServerAppTitleService.cs | 9 ++++++++ FruitBankHybrid/MauiProgram.cs | 1 + .../Services/MauiAppTitleService.cs | 21 +++++++++++++++++++ 10 files changed, 87 insertions(+), 5 deletions(-) create mode 100644 FruitBankHybrid.Shared/Services/IAppTitleService.cs create mode 100644 FruitBankHybrid.Web.Client/Services/WebAppTitleService.cs create mode 100644 FruitBankHybrid.Web/Services/ServerAppTitleService.cs create mode 100644 FruitBankHybrid/Services/MauiAppTitleService.cs diff --git a/FruitBank.Common/FruitBankConstClient.cs b/FruitBank.Common/FruitBankConstClient.cs index dc6ae370..e22c2f15 100644 --- a/FruitBank.Common/FruitBankConstClient.cs +++ b/FruitBank.Common/FruitBankConstClient.cs @@ -31,7 +31,17 @@ public static class FruitBankConstClient => _isProd ?? throw new InvalidOperationException( "FruitBankConstClient.IsProd nincs hidratálva — a FruitBankSignalRClient a kapcsolat felépülésekor tölti fel a szerver IsProdEnvironment() válaszából (await client.EnsureEnvironmentAsync())."); - public static void SetIsProd(bool isProd) => _isProd = isProd; + /// Az IsProd TÉNYLEGES változásakor tüzel (első hidratálás + announce-detektált env-váltás) — + /// a megjelenítők (title-badge, UI-sáv) feliratkozási pontja. Az értékadás forrása változatlanul EGY hely. + public static event Action? IsProdChanged; + + public static void SetIsProd(bool isProd) + { + var changed = _isProd != isProd; + _isProd = isProd; + + if (changed) IsProdChanged?.Invoke(isProd); + } /// True, ha az IsProd már hidratált — kapu-logikához (első-connect await) és a push-összevetéshez. public static bool IsProdHydrated => _isProd != null; diff --git a/FruitBankHybrid.Shared/Layout/MainLayout.razor.cs b/FruitBankHybrid.Shared/Layout/MainLayout.razor.cs index 55e9bb7a..15b70897 100644 --- a/FruitBankHybrid.Shared/Layout/MainLayout.razor.cs +++ b/FruitBankHybrid.Shared/Layout/MainLayout.razor.cs @@ -1,6 +1,8 @@ using AyCode.Core.Consts; using AyCode.Core.Extensions; +using AyCode.Core.Helpers; using AyCode.Core.Loggers; +using FruitBank.Common; using AyCode.Services.SignalRs; using AyCode.Utils.Extensions; using DevExpress.Blazor; @@ -9,6 +11,7 @@ using FruitBank.Common.Models; using FruitBank.Common.Models.SignalRs; using FruitBank.Common.SignalRs; using FruitBankHybrid.Shared.Pages; +using FruitBankHybrid.Shared.Services; using FruitBankHybrid.Shared.Services.Loggers; using FruitBankHybrid.Shared.Services.SignalRs; using Mango.Nop.Core.Loggers; @@ -23,6 +26,7 @@ public partial class MainLayout : LayoutComponentBase, IDisposable [Inject] public required LoggedInModel LoggedInModel { get; set; } [Inject] IToastNotificationService ToastService { get; set; } [Inject] public required FruitBankSignalRClient FruitBankSignalRClient { get; set; } + [Inject] public required IAppTitleService AppTitleService { get; set; } private EventCallback RefreshMainLayoutEventCallback => EventCallback.Factory.Create(this, RefreshMainLayout); private NavMenu _navMenu = null!; @@ -47,8 +51,16 @@ public partial class MainLayout : LayoutComponentBase, IDisposable LoggedInModel.GetRolesFunc = FruitBankSignalRClient.GetCustomerRolesByCustomerId; FruitBankSignalRClient.OnMessageReceived += SignalRClientOnMessageReceived; + + // Env-badge a címsorba (a nop-szerver Console.Title mintája): azonnal, ha az IsProd már hidratált, + // és feliratkozva a változásra (első hidratálás / announce-detektált env-váltás → riasztó title-frissítés). + // A hidratálást nem itt indítjuk — az első SignalR-hívás connect-kapuja végzi (pontosan 1 hívás). + FruitBankConstClient.IsProdChanged += OnIsProdChanged; + if (FruitBankConstClient.IsProdHydrated) OnIsProdChanged(FruitBankConstClient.IsProd); } + private void OnIsProdChanged(bool isProd) => AppTitleService.SetEnvironmentBadgeAsync(isProd).Forget(); + protected override async Task OnAfterRenderAsync(bool firstRender) { if (!firstRender) return; @@ -132,5 +144,6 @@ public partial class MainLayout : LayoutComponentBase, IDisposable public void Dispose() { FruitBankSignalRClient.OnMessageReceived -= SignalRClientOnMessageReceived; + FruitBankConstClient.IsProdChanged -= OnIsProdChanged; } } \ No newline at end of file diff --git a/FruitBankHybrid.Shared/Services/IAppTitleService.cs b/FruitBankHybrid.Shared/Services/IAppTitleService.cs new file mode 100644 index 00000000..352ebcd2 --- /dev/null +++ b/FruitBankHybrid.Shared/Services/IAppTitleService.cs @@ -0,0 +1,11 @@ +namespace FruitBankHybrid.Shared.Services; + +/// +/// Platform címsor-szolgáltatás az env-badge-hez — a nop-szerver Console.Title mintájának kliens-párja +/// ("[PROD]/[DEV] FruitBank"). Host-onkénti implementáció (az IFormFactor/ISecureCredentialService mintára): +/// browser document.title / MAUI Window.Title / szerver-prerender no-op. +/// +public interface IAppTitleService +{ + Task SetEnvironmentBadgeAsync(bool isProd); +} diff --git a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs index 2a7a4c5b..20f4a427 100644 --- a/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs +++ b/FruitBankHybrid.Shared/Services/SignalRs/FruitBankSignalRClient.cs @@ -83,14 +83,18 @@ namespace FruitBankHybrid.Shared.Services.SignalRs private Task? _environmentHydration; - // ELSŐ felépülés: HARD GATE — a StartConnection bevárja a hidratálást, addig SEMMILYEN más művelet - // nem megy ki ezen a kapcsolaton. Hiba → a kiváltó kérés is elhasal (fail closed: env-tudás nélkül - // nem beszélünk a szerverrel). Reconnect: NINCS újra-lekérdezés ("csak az elején 1x") — az env-váltást - // (szerver-restart MÁSIK DB-vel) a szerver push-on-connect announce-a jelzi (ServerEnvironment tag). + // ELSŐ felépülés: HARD GATE — a StartConnection bevárja a hidratálást, addig a kiváltó kérés nem megy ki + // (fail closed: env-tudás nélkül nem beszélünk a szerverrel). PONTOSAN EGY wire-hívás: ha a hidratálás már + // fut (pl. a teszt-guard EnsureEnvironmentAsync-je váltotta ki magát a connectet), nem indítunk másodikat — + // és nem is várjuk meg (önmagát várná: promise-ciklus lenne), az elindítója úgyis await-eli. + // Reconnect: NINCS újra-lekérdezés ("csak az elején 1x") — az env-váltást (szerver-restart MÁSIK DB-vel) + // a szerver push-on-connect announce-a jelzi (ServerEnvironment tag). protected override async Task OnConnectionEstablished() { if (FruitBankConstClient.IsProdHydrated) return; + if (_environmentHydration is { IsCompleted: false }) return; + _environmentHydration = HydrateEnvironmentAsync(); await _environmentHydration; } diff --git a/FruitBankHybrid.Web.Client/Program.cs b/FruitBankHybrid.Web.Client/Program.cs index 6b830a65..3a192601 100644 --- a/FruitBankHybrid.Web.Client/Program.cs +++ b/FruitBankHybrid.Web.Client/Program.cs @@ -28,6 +28,7 @@ builder.Services.AddDevExpressBlazor(configure => configure.SizeMode = DevExpres // Add device-specific services used by the FruitBankHybrid.Shared project builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); #if DEBUG builder.Services.AddSingleton(); diff --git a/FruitBankHybrid.Web.Client/Services/WebAppTitleService.cs b/FruitBankHybrid.Web.Client/Services/WebAppTitleService.cs new file mode 100644 index 00000000..0a1ac3ec --- /dev/null +++ b/FruitBankHybrid.Web.Client/Services/WebAppTitleService.cs @@ -0,0 +1,11 @@ +using FruitBankHybrid.Shared.Services; +using Microsoft.JSInterop; + +namespace FruitBankHybrid.Web.Client.Services; + +/// Böngésző-tab title env-badge: "[PROD]/[DEV] FruitBank" (a nop-szerver Console.Title mintája). +public class WebAppTitleService(IJSRuntime js) : IAppTitleService +{ + public async Task SetEnvironmentBadgeAsync(bool isProd) + => await js.InvokeVoidAsync("eval", $"document.title = '{(isProd ? "[PROD]" : "[DEV]")} FruitBank'"); +} diff --git a/FruitBankHybrid.Web/Program.cs b/FruitBankHybrid.Web/Program.cs index ad1a231c..b0d26ca2 100644 --- a/FruitBankHybrid.Web/Program.cs +++ b/FruitBankHybrid.Web/Program.cs @@ -27,6 +27,7 @@ builder.Services.AddMvc(); builder.Services.AddSingleton(); builder.Services.AddSingleton(); +builder.Services.AddSingleton(); builder.Services.AddSingleton(); diff --git a/FruitBankHybrid.Web/Services/ServerAppTitleService.cs b/FruitBankHybrid.Web/Services/ServerAppTitleService.cs new file mode 100644 index 00000000..d82af3d0 --- /dev/null +++ b/FruitBankHybrid.Web/Services/ServerAppTitleService.cs @@ -0,0 +1,9 @@ +using FruitBankHybrid.Shared.Services; + +namespace FruitBankHybrid.Web.Services; + +/// No-op: a szerver-host (prerender) nem birtokol címsort — az env-badge a WASM/MAUI kliensé. +public class ServerAppTitleService : IAppTitleService +{ + public Task SetEnvironmentBadgeAsync(bool isProd) => Task.CompletedTask; +} diff --git a/FruitBankHybrid/MauiProgram.cs b/FruitBankHybrid/MauiProgram.cs index 9002c470..9d15e19b 100644 --- a/FruitBankHybrid/MauiProgram.cs +++ b/FruitBankHybrid/MauiProgram.cs @@ -75,6 +75,7 @@ namespace FruitBankHybrid // Add device-specific services used by the FruitBankHybrid.Shared project builder.Services.AddSingleton(); builder.Services.AddSingleton(); + builder.Services.AddSingleton(); builder.Services.AddSingleton(sp => new LoggedInModel(sp.GetRequiredService())); diff --git a/FruitBankHybrid/Services/MauiAppTitleService.cs b/FruitBankHybrid/Services/MauiAppTitleService.cs new file mode 100644 index 00000000..9114086c --- /dev/null +++ b/FruitBankHybrid/Services/MauiAppTitleService.cs @@ -0,0 +1,21 @@ +using FruitBankHybrid.Shared.Services; + +namespace FruitBankHybrid.Services; + +/// +/// MAUI ablak-cím env-badge: "[PROD]/[DEV] FruitBank" — Windows desktopon látszik; +/// Android/iOS-en nincs futásidőben írható ablakcím (ott hatástalan, majd egy UI-badge fedheti). +/// +public class MauiAppTitleService : IAppTitleService +{ + public Task SetEnvironmentBadgeAsync(bool isProd) + { + MainThread.BeginInvokeOnMainThread(() => + { + var window = Application.Current?.Windows.FirstOrDefault(); + if (window != null) window.Title = $"{(isProd ? "[PROD]" : "[DEV]")} FruitBank"; + }); + + return Task.CompletedTask; + } +}