From 2eda51e6d3a9f1e900b46a8bede2e5119d9a7267 Mon Sep 17 00:00:00 2001 From: Shawn Jackson Date: Sat, 26 Sep 2026 10:14:17 -0700 Subject: [PATCH] RG-T135 Fixing Setup Wizard and Setup Report Issue --- .../User/AdminAssist/AdminAssist.ar.resx | 2 +- .../User/AdminAssist/AdminAssist.de.resx | 2 +- .../User/AdminAssist/AdminAssist.el.resx | 2 +- .../User/AdminAssist/AdminAssist.en.resx | 2 +- .../User/AdminAssist/AdminAssist.es.resx | 2 +- .../User/AdminAssist/AdminAssist.fr.resx | 2 +- .../User/AdminAssist/AdminAssist.it.resx | 2 +- .../User/AdminAssist/AdminAssist.pl.resx | 2 +- .../Areas/User/AdminAssist/AdminAssist.resx | 2 +- .../User/AdminAssist/AdminAssist.sv.resx | 2 +- .../User/AdminAssist/AdminAssist.uk.resx | 2 +- .../AdminAssistFeatureAvailability.cs | 20 +++- .../AdminAssist/FeatureToggleTests.cs | 16 ++++ .../AdminAssist/SetupPromptVisibilityTests.cs | 91 +++++++++++++++++++ .../Web/ApiLocalizationRegistrationTests.cs | 74 +++++++++++++++ Web/Resgrid.Web.Services/Startup.cs | 4 + .../User/Controllers/AdminAssistController.cs | 5 +- .../Areas/User/Views/Shared/_TopNavbar.cshtml | 30 +++--- .../AdminAssistSetupPromptViewComponent.cs | 2 + 19 files changed, 232 insertions(+), 32 deletions(-) create mode 100644 Tests/Resgrid.Tests/AdminAssist/SetupPromptVisibilityTests.cs create mode 100644 Tests/Resgrid.Tests/Web/ApiLocalizationRegistrationTests.cs diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.ar.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.ar.resx index a4addbc9..f2aca058 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.ar.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.ar.resx @@ -6421,7 +6421,7 @@ تستفيد معظم الأقسام من هذه. - Resgrid Admin Assist + Admin Assist مفعّل diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.de.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.de.resx index 569c52cb..c16d2571 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.de.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.de.resx @@ -6421,7 +6421,7 @@ Die meisten Abteilungen profitieren davon. - Resgrid Admin Assist + Admin Assist Aktiviert diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.el.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.el.resx index f5f7e88b..7486af12 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.el.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.el.resx @@ -6421,7 +6421,7 @@ Τα περισσότερα τμήματα επωφελούνται από αυτές. - Resgrid Admin Assist + Admin Assist Ενεργοποιημένο diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.en.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.en.resx index 9935880f..48f0e7b2 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.en.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.en.resx @@ -6421,7 +6421,7 @@ Most departments benefit from these. - Resgrid Admin Assist + Admin Assist Enabled diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.es.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.es.resx index ccab5cda..9c7bb81d 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.es.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.es.resx @@ -6421,7 +6421,7 @@ La mayoría de los departamentos se benefician de estos. - Resgrid Admin Assist + Admin Assist Habilitado diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.fr.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.fr.resx index b647aed9..1eb709b3 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.fr.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.fr.resx @@ -6421,7 +6421,7 @@ La plupart des départements en tirent parti. - Resgrid Admin Assist + Admin Assist Activé diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.it.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.it.resx index a3debd46..e52522a1 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.it.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.it.resx @@ -6421,7 +6421,7 @@ La maggior parte dei dipartimenti ne trae vantaggio. - Resgrid Admin Assist + Admin Assist Attivato diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.pl.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.pl.resx index c3b86fcc..b27959a2 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.pl.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.pl.resx @@ -6421,7 +6421,7 @@ Większość oddziałów odnosi z nich korzyści. - Resgrid Admin Assist + Admin Assist Włączone diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.resx index 9935880f..48f0e7b2 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.resx @@ -6421,7 +6421,7 @@ Most departments benefit from these. - Resgrid Admin Assist + Admin Assist Enabled diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.sv.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.sv.resx index 6eba4bfa..98dc54f3 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.sv.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.sv.resx @@ -6421,7 +6421,7 @@ De flesta avdelningar har nytta av dessa. - Resgrid Admin Assist + Admin Assist Aktiverad diff --git a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.uk.resx b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.uk.resx index fbdf3f94..921b9882 100644 --- a/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.uk.resx +++ b/Core/Resgrid.Localization/Areas/User/AdminAssist/AdminAssist.uk.resx @@ -6421,7 +6421,7 @@ Більшість підрозділів отримують користь від цих модулів. - Resgrid Admin Assist + Admin Assist Увімкнено diff --git a/Core/Resgrid.Services/AdminAssist/AdminAssistFeatureAvailability.cs b/Core/Resgrid.Services/AdminAssist/AdminAssistFeatureAvailability.cs index 6144a4d9..c8d60632 100644 --- a/Core/Resgrid.Services/AdminAssist/AdminAssistFeatureAvailability.cs +++ b/Core/Resgrid.Services/AdminAssist/AdminAssistFeatureAvailability.cs @@ -9,19 +9,29 @@ namespace Resgrid.Services.AdminAssist /// Fresh rollout checks. Missing flags and evaluation failures disable the optional feature. public static class AdminAssistFeatureAvailability { - public static async Task IsEnabledAsync(IFeatureToggleService flags, int departmentId, bool setup, CancellationToken ct = default) + public static Task IsEnabledAsync(IFeatureToggleService flags, int departmentId, bool setup, CancellationToken ct = default) => + IsFlagEnabledAsync(flags, setup ? FeatureFlagKeys.AdminSetup : FeatureFlagKeys.AdminAssist, departmentId, ct); + + /// + /// The Admin Assist workspace (its menu entry and pages) launches with its AI, after the Setup Wizard and Setup Report. + /// Admin.Assist alone still drives field help, impact previews and digests; the workspace also needs Ai.AdminAssist. + /// + public static async Task IsWorkspaceEnabledAsync(IFeatureToggleService flags, int departmentId, CancellationToken ct = default) => + await IsEnabledAsync(flags, departmentId, false, ct) && await IsFlagEnabledAsync(flags, FeatureFlagKeys.AiAdminAssist, departmentId, ct); + + public static async Task CanConfigureOperatingProfileAsync(IFeatureToggleService flags, int departmentId, CancellationToken ct = default) => + await IsEnabledAsync(flags, departmentId, true, ct) || await IsEnabledAsync(flags, departmentId, false, ct); + + private static async Task IsFlagEnabledAsync(IFeatureToggleService flags, string key, int departmentId, CancellationToken ct) { ct.ThrowIfCancellationRequested(); if (departmentId <= 0) return false; try { - return (await flags.EvaluateFreshAsync(setup ? FeatureFlagKeys.AdminSetup : FeatureFlagKeys.AdminAssist, departmentId).WaitAsync(ct))?.IsEnabled == true; + return (await flags.EvaluateFreshAsync(key, departmentId).WaitAsync(ct))?.IsEnabled == true; } catch (OperationCanceledException) when (ct.IsCancellationRequested) { throw; } catch (Exception) { return false; } } - - public static async Task CanConfigureOperatingProfileAsync(IFeatureToggleService flags, int departmentId, CancellationToken ct = default) => - await IsEnabledAsync(flags, departmentId, true, ct) || await IsEnabledAsync(flags, departmentId, false, ct); } } diff --git a/Tests/Resgrid.Tests/AdminAssist/FeatureToggleTests.cs b/Tests/Resgrid.Tests/AdminAssist/FeatureToggleTests.cs index 6d2ab42c..1564d274 100644 --- a/Tests/Resgrid.Tests/AdminAssist/FeatureToggleTests.cs +++ b/Tests/Resgrid.Tests/AdminAssist/FeatureToggleTests.cs @@ -42,6 +42,22 @@ public async Task Setup_and_assist_are_independent_and_AI_does_not_enable_either Assert.That(await AdminAssistFeatureAvailability.CanConfigureOperatingProfileAsync(_flags.Object,7), Is.EqualTo(setup || assist)); _flags.Verify(f => f.EvaluateFreshAsync(FeatureFlagKeys.AiAdminAssist,7), Times.Never); } + [TestCase(false,false)] [TestCase(true,false)] [TestCase(false,true)] [TestCase(true,true)] + public async Task Workspace_launches_only_with_both_assist_and_AI(bool assist, bool ai) + { + Set(FeatureFlagKeys.AdminAssist,assist); Set(FeatureFlagKeys.AiAdminAssist,ai); + Assert.That(await AdminAssistFeatureAvailability.IsWorkspaceEnabledAsync(_flags.Object,7), Is.EqualTo(assist && ai)); + // Field help, previews and digests keep following Admin.Assist alone. + Assert.That(await _access.CanAccessAsync(Actor,false), Is.EqualTo(assist)); + } + [Test] + public async Task Workspace_fails_closed_when_the_AI_flag_is_missing_or_unavailable() + { + Set(FeatureFlagKeys.AdminAssist,true); + Assert.That(await AdminAssistFeatureAvailability.IsWorkspaceEnabledAsync(_flags.Object,7), Is.False); + _flags.Setup(f => f.EvaluateFreshAsync(FeatureFlagKeys.AiAdminAssist,7)).ThrowsAsync(new InvalidOperationException("Store unavailable")); + Assert.That(await AdminAssistFeatureAvailability.IsWorkspaceEnabledAsync(_flags.Object,7), Is.False); + } [TestCase(true)] [TestCase(false)] public async Task Missing_flag_and_store_outage_fail_closed(bool setup) { diff --git a/Tests/Resgrid.Tests/AdminAssist/SetupPromptVisibilityTests.cs b/Tests/Resgrid.Tests/AdminAssist/SetupPromptVisibilityTests.cs new file mode 100644 index 00000000..261bbcb0 --- /dev/null +++ b/Tests/Resgrid.Tests/AdminAssist/SetupPromptVisibilityTests.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Security.Claims; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.AspNetCore.Http; +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Rendering; +using Microsoft.AspNetCore.Mvc.ViewComponents; +using Microsoft.AspNetCore.Mvc.ViewEngines; +using Moq; +using NUnit.Framework; +using Resgrid.AdminAssist; +using Resgrid.Model.AdminAssist; +using Resgrid.Providers.Claims; +using Resgrid.Web.Helpers; +using Resgrid.Web.ViewComponents; + +namespace Resgrid.Tests.AdminAssist +{ + /// The dashboard's Setup Wizard banner is for department administrators only. + [TestFixture, NonParallelizable] + public class SetupPromptVisibilityTests + { + private static readonly AdminAssistActor Actor = new(7, "user-1"); + private static readonly ConfigurationCatalog Catalog = new(); + private IHttpContextAccessor _previousAccessor; + private Mock _access; + private Mock _repository; + + [SetUp] + public void SetUp() + { + _previousAccessor = ClaimsAuthorizationHelper._httpContextAccessor; + _access = new Mock(MockBehavior.Strict); + _repository = new Mock(MockBehavior.Strict); + } + + [TearDown] + public void TearDown() => ClaimsAuthorizationHelper._httpContextAccessor = _previousAccessor; + + [Test] + public async Task Member_never_sees_the_banner_and_costs_no_flag_or_workspace_read() + { + var result = await InvokeAsync(admin: false); + + Assert.That(result, Is.InstanceOf()); + Assert.That(((ContentViewComponentResult)result).Content, Is.Empty); + _access.VerifyNoOtherCalls(); _repository.VerifyNoOtherCalls(); + } + + [Test] + public async Task Admin_sees_the_banner_while_setup_is_open() + { + _access.Setup(a => a.CanAccessAsync(It.Is(x => x.DepartmentId == Actor.DepartmentId && x.UserId == Actor.UserId), true, It.IsAny())).ReturnsAsync(true); + _repository.Setup(r => r.GetWorkspaceAsync(Actor.DepartmentId, Actor.UserId, Catalog.Version, It.IsAny())) + .ReturnsAsync(new SetupWorkspace(Actor.DepartmentId, 0, SetupMode.Fresh, new Dictionary(), Array.Empty(), Array.Empty(), Catalog.Version, null)); + + var result = await InvokeAsync(admin: true); + + Assert.That(result, Is.InstanceOf()); + } + + [Test] + public async Task Admin_claim_alone_is_not_enough_when_fresh_access_is_refused() + { + // The claim may be stale; the fresh membership and rollout check still decides. + _access.Setup(a => a.CanAccessAsync(It.IsAny(), true, It.IsAny())).ReturnsAsync(false); + + var result = await InvokeAsync(admin: true); + + Assert.That(((ContentViewComponentResult)result).Content, Is.Empty); + _repository.VerifyNoOtherCalls(); + } + + private async Task InvokeAsync(bool admin) + { + var claims = new List { new(ClaimTypes.PrimarySid, Actor.UserId), new(ClaimTypes.PrimaryGroupSid, Actor.DepartmentId.ToString()) }; + if (admin) claims.Add(new Claim(ResgridClaimTypes.Resources.Department, ResgridClaimTypes.Actions.Update)); + var http = new DefaultHttpContext { User = new ClaimsPrincipal(new ClaimsIdentity(claims, "Test")) }; + ClaimsAuthorizationHelper._httpContextAccessor = new HttpContextAccessor { HttpContext = http }; + + var component = new AdminAssistSetupPromptViewComponent(_access.Object, _repository.Object, Catalog) + { + ViewComponentContext = new ViewComponentContext { ViewContext = new ViewContext { HttpContext = http } }, + ViewEngine = Mock.Of() + }; + return await component.InvokeAsync(); + } + } +} diff --git a/Tests/Resgrid.Tests/Web/ApiLocalizationRegistrationTests.cs b/Tests/Resgrid.Tests/Web/ApiLocalizationRegistrationTests.cs new file mode 100644 index 00000000..6c48aa99 --- /dev/null +++ b/Tests/Resgrid.Tests/Web/ApiLocalizationRegistrationTests.cs @@ -0,0 +1,74 @@ +using System; +using System.Globalization; +using System.IO; +using System.Linq; +using System.Text.RegularExpressions; +using FluentAssertions; +using Microsoft.AspNetCore.Mvc; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Extensions.Localization; +using NUnit.Framework; +using AdminAssistLabels = Resgrid.Localization.Areas.User.AdminAssist.AdminAssist; + +namespace Resgrid.Tests.Web +{ + /// + /// The API host never registered localization, so every v4 controller taking an IStringLocalizer (Admin Assist among + /// them) failed to construct and returned 500. The Setup Wizard and Setup Report showed only "The report could not be + /// loaded" because their Catalog and Overview reads never reached the controller. + /// + [TestFixture] + public class ApiLocalizationRegistrationTests + { + [Test] + public void Api_startup_registers_localization_for_controllers_that_take_string_localizers() + { + var localized = typeof(Resgrid.Web.Services.Controllers.v4.AdminAssistController).Assembly.GetTypes() + .Where(t => typeof(ControllerBase).IsAssignableFrom(t) && !t.IsAbstract) + .Where(t => t.GetConstructors().Any(c => c.GetParameters().Any(p => p.ParameterType.IsGenericType && + p.ParameterType.GetGenericTypeDefinition() == typeof(IStringLocalizer<>)))) + .Select(t => t.Name) + .ToList(); + localized.Should().Contain("AdminAssistController"); + + var startup = File.ReadAllText(FindRepositoryFile("Web/Resgrid.Web.Services/Startup.cs")); + Regex.IsMatch(startup, @"^\s*services\.AddLocalization\(", RegexOptions.Multiline) + .Should().BeTrue($"the API constructs {string.Join(", ", localized)} with an IStringLocalizer"); + } + + [TestCase("")] + [TestCase("en")] + [TestCase("de")] + public void Admin_assist_catalog_strings_resolve_from_the_registered_localizer(string culture) + { + var previous = CultureInfo.CurrentUICulture; + try + { + CultureInfo.CurrentUICulture = CultureInfo.GetCultureInfo(culture); + // The host supplies logging; the localizer factory depends on it. + using var provider = new ServiceCollection().AddLogging().AddLocalization().BuildServiceProvider(); + var labels = provider.GetRequiredService>(); + + // The same projection the Catalog endpoint returns to the page. + var strings = labels.GetAllStrings(true).GroupBy(s => s.Name).ToDictionary(g => g.Key, g => g.First().Value); + + strings.Should().ContainKey("Ui.Error"); + strings["Ui.Title"].Should().Be("Admin Assist"); + } + finally { CultureInfo.CurrentUICulture = previous; } + } + + private static string FindRepositoryFile(string relativePath) + { + var directory = new DirectoryInfo(TestContext.CurrentContext.TestDirectory); + + while (directory != null && !File.Exists(Path.Combine(directory.FullName, "Resgrid.sln"))) + directory = directory.Parent; + + if (directory == null) + throw new InvalidOperationException("Unable to locate the repository root."); + + return Path.Combine(directory.FullName, relativePath.Replace('/', Path.DirectorySeparatorChar)); + } + } +} diff --git a/Web/Resgrid.Web.Services/Startup.cs b/Web/Resgrid.Web.Services/Startup.cs index 756e4440..0d3b27ca 100644 --- a/Web/Resgrid.Web.Services/Startup.cs +++ b/Web/Resgrid.Web.Services/Startup.cs @@ -166,6 +166,10 @@ public void ConfigureServices(IServiceCollection services) services.AddCors(); + // v4 controllers (Admin Assist, Checklists, Inventory, Work Orders) take IStringLocalizer. Without this + // registration none of them can be constructed and every request to them fails with a 500. + services.AddLocalization(); + services.AddControllers(options => { // ADP department operation lock: refuses department-scoped mutations with 423 Locked diff --git a/Web/Resgrid.Web/Areas/User/Controllers/AdminAssistController.cs b/Web/Resgrid.Web/Areas/User/Controllers/AdminAssistController.cs index bf80556e..011efda5 100644 --- a/Web/Resgrid.Web/Areas/User/Controllers/AdminAssistController.cs +++ b/Web/Resgrid.Web/Areas/User/Controllers/AdminAssistController.cs @@ -12,7 +12,7 @@ namespace Resgrid.Web.Areas.User.Controllers [Authorize] [ResponseCache(NoStore = true, Location = ResponseCacheLocation.None)] public sealed class AdminAssistController(IAdminAssistAccessService access, IAdminAssistService service, - IDepartmentDataProtectionService protection) : SecureBaseController + IDepartmentDataProtectionService protection, IFeatureToggleService flags) : SecureBaseController { [HttpGet] public Task Index(CancellationToken cancellationToken) => PageAsync("overview", false, cancellationToken); @@ -48,6 +48,7 @@ public async Task PrintReport(bool setup, CancellationToken cance Response.Headers["Referrer-Policy"] = "no-referrer"; try { + if (!setup && !await Resgrid.Services.AdminAssist.AdminAssistFeatureAvailability.IsWorkspaceEnabledAsync(flags, DepartmentId, cancellationToken)) return NotFound(); // A new authorized read: no previously rendered worklist content or browser snapshot is reused. var overview = await service.GetOverviewAsync(new AdminAssistActor(DepartmentId, UserId, CultureInfo.CurrentUICulture.Name), setup, cancellationToken); return View("PrintReport", overview); @@ -75,6 +76,8 @@ private async Task PageAsync(string page, bool setup, Cancellatio { var actor = new AdminAssistActor(DepartmentId, UserId, CultureInfo.CurrentUICulture.Name); if (!await access.CanAccessAsync(actor, setup, ct)) return NotFound(); + // The workspace launches after the Setup Wizard and Setup Report; it stays hidden until its AI is on. + if (!setup && !await Resgrid.Services.AdminAssist.AdminAssistFeatureAvailability.IsWorkspaceEnabledAsync(flags, DepartmentId, ct)) return NotFound(); ViewBag.AdminAssistPage = page; ViewBag.AdminAssistSetup = setup; ViewBag.AdminAssistProtected = await protection.IsProtectionEnforcedAsync(DepartmentId).WaitAsync(ct); diff --git a/Web/Resgrid.Web/Areas/User/Views/Shared/_TopNavbar.cshtml b/Web/Resgrid.Web/Areas/User/Views/Shared/_TopNavbar.cshtml index ba036073..c778aa62 100644 --- a/Web/Resgrid.Web/Areas/User/Views/Shared/_TopNavbar.cshtml +++ b/Web/Resgrid.Web/Areas/User/Views/Shared/_TopNavbar.cshtml @@ -4,8 +4,9 @@ @{ var adminSetupVisible = ClaimsAuthorizationHelper.IsUserDepartmentAdmin() && await Resgrid.Services.AdminAssist.AdminAssistFeatureAvailability.IsEnabledAsync(featureToggleService, ClaimsAuthorizationHelper.GetDepartmentId(), true, Context.RequestAborted); + // The workspace launches after Setup Wizard and Setup Report: it also needs Ai.AdminAssist, not just Admin.Assist. var adminAssistVisible = ClaimsAuthorizationHelper.IsUserDepartmentAdmin() && - await Resgrid.Services.AdminAssist.AdminAssistFeatureAvailability.IsEnabledAsync(featureToggleService, ClaimsAuthorizationHelper.GetDepartmentId(), false, Context.RequestAborted); + await Resgrid.Services.AdminAssist.AdminAssistFeatureAvailability.IsWorkspaceEnabledAsync(featureToggleService, ClaimsAuthorizationHelper.GetDepartmentId(), Context.RequestAborted); // Chat.System gates the moderation console link; it is only useful to department or group admins. var chatModerationVisible = ClaimsAuthorizationHelper.IsUserDepartmentOrGroupAdmin(0) && await featureToggleService.IsEnabledAsync(FeatureFlagKeys.ChatSystem, ClaimsAuthorizationHelper.GetDepartmentId()); @@ -21,11 +22,11 @@