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