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; + } +}