feat(spec-286): neutralize environment-first admin copy
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m21s
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m21s
Align chooser, landing, dashboard, shell, and bounded policy helper copy to environment-first terminology for spec 286. Validation: - export PATH="/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/Users/ahmeddarrazi/.antigravity/antigravity/bin:/Users/ahmeddarrazi/.antigravity/antigravity/bin:/Users/ahmeddarrazi/.nvm/versions/node/v24.11.0/bin:/Users/ahmeddarrazi/.config/herd-lite/bin:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/debugCommand:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/copilotCli:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/homebrew/bin:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/debugCommand:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/copilotCli:/Users/ahmeddarrazi/.antigravity/antigravity/bin:/Users/ahmeddarrazi/.local/bin:/Users/ahmeddarrazi/.nvm/versions/node/v24.11.0/bin:/Users/ahmeddarrazi/.config/herd-lite/bin:/Users/ahmeddarrazi/.vscode/extensions/ms-python.debugpy-2026.6.0-darwin-arm64/bundled/scripts/noConfigScripts" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Localization/EnvironmentContextTerminologyTest.php tests/Feature/Filament/EnvironmentContextSurfaceCopyTest.php tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php - export PATH="/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/Users/ahmeddarrazi/.antigravity/antigravity/bin:/Users/ahmeddarrazi/.antigravity/antigravity/bin:/Users/ahmeddarrazi/.nvm/versions/node/v24.11.0/bin:/Users/ahmeddarrazi/.config/herd-lite/bin:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/debugCommand:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/copilotCli:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/homebrew/bin:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/debugCommand:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/copilotCli:/Users/ahmeddarrazi/.antigravity/antigravity/bin:/Users/ahmeddarrazi/.local/bin:/Users/ahmeddarrazi/.nvm/versions/node/v24.11.0/bin:/Users/ahmeddarrazi/.config/herd-lite/bin:/Users/ahmeddarrazi/.vscode/extensions/ms-python.debugpy-2026.6.0-darwin-arm64/bundled/scripts/noConfigScripts" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec286EnvironmentCopyNeutralizationSmokeTest.php - export PATH="/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/bin:/usr/bin:/usr/local/bin:/Users/ahmeddarrazi/.antigravity/antigravity/bin:/Users/ahmeddarrazi/.antigravity/antigravity/bin:/Users/ahmeddarrazi/.nvm/versions/node/v24.11.0/bin:/Users/ahmeddarrazi/.config/herd-lite/bin:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/debugCommand:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/copilotCli:/usr/local/bin:/System/Cryptexes/App/usr/bin:/usr/bin:/bin:/usr/sbin:/sbin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/local/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/bin:/var/run/com.apple.security.cryptexd/codex.system/bootstrap/usr/appleinternal/bin:/opt/homebrew/bin:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/debugCommand:/Users/ahmeddarrazi/Library/Application Support/Code/User/globalStorage/github.copilot-chat/copilotCli:/Users/ahmeddarrazi/.antigravity/antigravity/bin:/Users/ahmeddarrazi/.local/bin:/Users/ahmeddarrazi/.nvm/versions/node/v24.11.0/bin:/Users/ahmeddarrazi/.config/herd-lite/bin:/Users/ahmeddarrazi/.vscode/extensions/ms-python.debugpy-2026.6.0-darwin-arm64/bundled/scripts/noConfigScripts" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent Note: the discovered Capture snapshot modal runtime issue remains out-of-scope runtime debt for spec 286 and is recorded in the spec tasks close-out notes.
This commit is contained in:
parent
c7b38606a9
commit
ae0e0a0674
@ -27,10 +27,13 @@ class ChooseTenant extends Page
|
|||||||
|
|
||||||
protected static ?string $slug = 'choose-tenant';
|
protected static ?string $slug = 'choose-tenant';
|
||||||
|
|
||||||
protected static ?string $title = 'Choose tenant';
|
|
||||||
|
|
||||||
protected string $view = 'filament.pages.choose-tenant';
|
protected string $view = 'filament.pages.choose-tenant';
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return __('localization.shell.choose_environment');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable the simple-layout topbar to prevent lazy-loaded
|
* Disable the simple-layout topbar to prevent lazy-loaded
|
||||||
* DatabaseNotifications from triggering Livewire update 404s.
|
* DatabaseNotifications from triggering Livewire update 404s.
|
||||||
|
|||||||
@ -209,7 +209,7 @@ protected function getHeaderActions(): array
|
|||||||
|
|
||||||
if ($activeTenant instanceof ManagedEnvironment) {
|
if ($activeTenant instanceof ManagedEnvironment) {
|
||||||
$actions[] = Action::make('operate_hub_show_all_tenants')
|
$actions[] = Action::make('operate_hub_show_all_tenants')
|
||||||
->label('Show all tenants')
|
->label(__('localization.shell.show_all_environments'))
|
||||||
->color('gray')
|
->color('gray')
|
||||||
->action(function (): void {
|
->action(function (): void {
|
||||||
Filament::setTenant(null, true);
|
Filament::setTenant(null, true);
|
||||||
@ -256,13 +256,13 @@ public function landingHierarchySummary(): array
|
|||||||
return [
|
return [
|
||||||
'scope_label' => $operateHubShell->scopeLabel(request()),
|
'scope_label' => $operateHubShell->scopeLabel(request()),
|
||||||
'scope_body' => $activeTenant instanceof ManagedEnvironment
|
'scope_body' => $activeTenant instanceof ManagedEnvironment
|
||||||
? 'The landing is currently narrowed to one tenant inside the active workspace.'
|
? 'The landing is currently narrowed to one environment inside the active workspace.'
|
||||||
: 'The landing is currently showing workspace-wide monitoring across all entitled tenants.',
|
: 'The landing is currently showing workspace-wide monitoring across all entitled environments.',
|
||||||
'return_label' => $returnLabel,
|
'return_label' => $returnLabel,
|
||||||
'return_body' => $returnBody,
|
'return_body' => $returnBody,
|
||||||
'scope_reset_label' => $activeTenant instanceof ManagedEnvironment ? 'Show all tenants' : null,
|
'scope_reset_label' => $activeTenant instanceof ManagedEnvironment ? __('localization.shell.show_all_environments') : null,
|
||||||
'scope_reset_body' => $activeTenant instanceof ManagedEnvironment
|
'scope_reset_body' => $activeTenant instanceof ManagedEnvironment
|
||||||
? 'Reset the landing back to workspace-wide monitoring when tenant-specific context is no longer needed.'
|
? 'Reset the landing back to workspace-wide monitoring when environment-specific context is no longer needed.'
|
||||||
: null,
|
: null,
|
||||||
'inspect_body' => 'Open a run from the table to enter the canonical monitoring detail viewer.',
|
'inspect_body' => 'Open a run from the table to enter the canonical monitoring detail viewer.',
|
||||||
];
|
];
|
||||||
|
|||||||
@ -734,25 +734,25 @@ public function canonicalContextBanner(): ?array
|
|||||||
'tone' => 'slate',
|
'tone' => 'slate',
|
||||||
'title' => 'Workspace-level operation',
|
'title' => 'Workspace-level operation',
|
||||||
'body' => $activeTenant instanceof ManagedEnvironment
|
'body' => $activeTenant instanceof ManagedEnvironment
|
||||||
? 'This canonical workspace view is not tied to the current tenant context ('.$activeTenant->name.').'
|
? 'This canonical workspace view is not tied to the current environment context ('.$activeTenant->name.').'
|
||||||
: 'This canonical workspace view is not tied to any tenant.',
|
: 'This canonical workspace view is not tied to any environment.',
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
|
|
||||||
$messages = ['Operation tenant: '.$runTenant->name.'.'];
|
$messages = ['Operation environment: '.$runTenant->name.'.'];
|
||||||
$tone = 'sky';
|
$tone = 'sky';
|
||||||
$title = null;
|
$title = null;
|
||||||
|
|
||||||
if ($activeTenant instanceof ManagedEnvironment && ! $activeTenant->is($runTenant)) {
|
if ($activeTenant instanceof ManagedEnvironment && ! $activeTenant->is($runTenant)) {
|
||||||
$title = 'Current tenant context differs from this operation';
|
$title = 'Current environment context differs from this operation';
|
||||||
array_unshift($messages, 'Current tenant context: '.$activeTenant->name.'.');
|
array_unshift($messages, 'Current environment context: '.$activeTenant->name.'.');
|
||||||
$messages[] = 'This canonical workspace view remains valid without switching tenant context.';
|
$messages[] = 'This canonical workspace view remains valid without switching environment context.';
|
||||||
}
|
}
|
||||||
|
|
||||||
$referencedTenant = ReferencedTenantLifecyclePresentation::forOperationRun($runTenant);
|
$referencedTenant = ReferencedTenantLifecyclePresentation::forOperationRun($runTenant);
|
||||||
|
|
||||||
if ($selectorAvailabilityMessage = $referencedTenant->selectorAvailabilityMessage()) {
|
if ($selectorAvailabilityMessage = $referencedTenant->selectorAvailabilityMessage()) {
|
||||||
$title ??= 'Operation tenant is not available in the current tenant selector';
|
$title ??= 'Operation environment is not available in the current environment selector';
|
||||||
$tone = 'amber';
|
$tone = 'amber';
|
||||||
$messages[] = $selectorAvailabilityMessage;
|
$messages[] = $selectorAvailabilityMessage;
|
||||||
|
|
||||||
@ -761,7 +761,7 @@ public function canonicalContextBanner(): ?array
|
|||||||
}
|
}
|
||||||
} elseif (! $activeTenant instanceof ManagedEnvironment) {
|
} elseif (! $activeTenant instanceof ManagedEnvironment) {
|
||||||
$title ??= 'Canonical workspace view';
|
$title ??= 'Canonical workspace view';
|
||||||
$messages[] = 'No tenant context is currently selected.';
|
$messages[] = 'No environment context is currently selected.';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($title === null) {
|
if ($title === null) {
|
||||||
|
|||||||
@ -56,7 +56,7 @@ class TenantDashboard extends Dashboard
|
|||||||
|
|
||||||
public static function getNavigationLabel(): string
|
public static function getNavigationLabel(): string
|
||||||
{
|
{
|
||||||
return __('localization.dashboard.tenant_title');
|
return __('localization.dashboard.environment_title');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getTitle(): string | Htmlable
|
public function getTitle(): string | Htmlable
|
||||||
@ -64,7 +64,7 @@ public function getTitle(): string | Htmlable
|
|||||||
$tenant = Filament::getTenant();
|
$tenant = Filament::getTenant();
|
||||||
|
|
||||||
if (! $tenant instanceof ManagedEnvironment) {
|
if (! $tenant instanceof ManagedEnvironment) {
|
||||||
return __('localization.dashboard.tenant_title');
|
return __('localization.dashboard.environment_title');
|
||||||
}
|
}
|
||||||
|
|
||||||
$summary = $this->dashboardSummary();
|
$summary = $this->dashboardSummary();
|
||||||
|
|||||||
@ -25,12 +25,15 @@ class ManagedTenantsLanding extends Page
|
|||||||
|
|
||||||
protected static string $layout = 'filament-panels::components.layout.simple';
|
protected static string $layout = 'filament-panels::components.layout.simple';
|
||||||
|
|
||||||
protected static ?string $title = 'Managed tenants';
|
|
||||||
|
|
||||||
protected string $view = 'filament.pages.workspaces.managed-tenants-landing';
|
protected string $view = 'filament.pages.workspaces.managed-tenants-landing';
|
||||||
|
|
||||||
public Workspace $workspace;
|
public Workspace $workspace;
|
||||||
|
|
||||||
|
public function getTitle(): string
|
||||||
|
{
|
||||||
|
return __('localization.shell.managed_environments_title');
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The Filament simple layout renders the topbar by default, which includes
|
* The Filament simple layout renders the topbar by default, which includes
|
||||||
* lazy-loaded database notifications. On this workspace-scoped landing page,
|
* lazy-loaded database notifications. On this workspace-scoped landing page,
|
||||||
|
|||||||
@ -28,7 +28,7 @@ protected function getViewData(): array
|
|||||||
return [
|
return [
|
||||||
'context' => [
|
'context' => [
|
||||||
'workspace' => __('localization.dashboard.overview.context_workspace'),
|
'workspace' => __('localization.dashboard.overview.context_workspace'),
|
||||||
'tenant' => __('localization.dashboard.overview.context_no_tenant'),
|
'tenant' => __('localization.dashboard.overview.context_no_environment'),
|
||||||
'provider' => null,
|
'provider' => null,
|
||||||
'providerKey' => null,
|
'providerKey' => null,
|
||||||
'latestActivity' => null,
|
'latestActivity' => null,
|
||||||
|
|||||||
@ -123,7 +123,7 @@ public function onboardingEntryDescriptor(int $resumableDraftCount): TenantActio
|
|||||||
default => new TenantActionDescriptor(
|
default => new TenantActionDescriptor(
|
||||||
key: 'add_tenant',
|
key: 'add_tenant',
|
||||||
family: TenantActionFamily::OnboardingWorkflow,
|
family: TenantActionFamily::OnboardingWorkflow,
|
||||||
label: 'Add tenant',
|
label: 'Add environment',
|
||||||
icon: 'heroicon-m-plus',
|
icon: 'heroicon-m-plus',
|
||||||
group: 'primary',
|
group: 'primary',
|
||||||
),
|
),
|
||||||
|
|||||||
@ -105,7 +105,7 @@ public static function forTenant(?ManagedEnvironment $tenant): self
|
|||||||
if (! $assignment instanceof BaselineTenantAssignment) {
|
if (! $assignment instanceof BaselineTenantAssignment) {
|
||||||
return self::empty(
|
return self::empty(
|
||||||
'no_assignment',
|
'no_assignment',
|
||||||
'This tenant has no baseline assignment. A workspace manager can assign a baseline profile to this tenant.',
|
'This environment has no baseline assignment. A workspace manager can assign a baseline profile to this environment.',
|
||||||
findingAttentionCounts: $findingAttentionCounts,
|
findingAttentionCounts: $findingAttentionCounts,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -242,7 +242,7 @@ private function headline(
|
|||||||
},
|
},
|
||||||
BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => 'Baseline compare is in progress.',
|
BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => 'Baseline compare is in progress.',
|
||||||
default => match ($stats->state) {
|
default => match ($stats->state) {
|
||||||
'no_assignment' => 'This tenant does not have an assigned baseline yet.',
|
'no_assignment' => 'This environment does not have an assigned baseline yet.',
|
||||||
'no_snapshot' => 'The current baseline snapshot is not available for compare.',
|
'no_snapshot' => 'The current baseline snapshot is not available for compare.',
|
||||||
'idle' => 'A current baseline compare result is not available yet.',
|
'idle' => 'A current baseline compare result is not available yet.',
|
||||||
default => 'A usable baseline compare result is not currently available.',
|
default => 'A usable baseline compare result is not currently available.',
|
||||||
|
|||||||
@ -104,7 +104,7 @@ public static function forTenantRegistry(string $backLinkUrl, ?int $tenantId = n
|
|||||||
sourceSurface: 'tenant_registry',
|
sourceSurface: 'tenant_registry',
|
||||||
canonicalRouteName: ListTenants::getRouteName(Filament::getPanel('admin')),
|
canonicalRouteName: ListTenants::getRouteName(Filament::getPanel('admin')),
|
||||||
tenantId: $tenantId,
|
tenantId: $tenantId,
|
||||||
backLinkLabel: 'Back to tenant registry',
|
backLinkLabel: __('localization.shell.back_to_environment_registry'),
|
||||||
backLinkUrl: $backLinkUrl,
|
backLinkUrl: $backLinkUrl,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -32,10 +32,10 @@ public function scopeLabel(?Request $request = null): string
|
|||||||
$activeTenant = $this->activeEntitledTenant($request);
|
$activeTenant = $this->activeEntitledTenant($request);
|
||||||
|
|
||||||
if ($activeTenant instanceof ManagedEnvironment) {
|
if ($activeTenant instanceof ManagedEnvironment) {
|
||||||
return 'ManagedEnvironment scope: '.$activeTenant->name;
|
return __('localization.shell.environment_scope').': '.$activeTenant->name;
|
||||||
}
|
}
|
||||||
|
|
||||||
return 'All tenants';
|
return __('localization.shell.all_environments');
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -62,7 +62,7 @@ public static function forLifecycle(TenantLifecycle $lifecycle): self
|
|||||||
badgeIcon: 'heroicon-m-arrow-path',
|
badgeIcon: 'heroicon-m-arrow-path',
|
||||||
badgeIconColor: null,
|
badgeIconColor: null,
|
||||||
shortDescription: 'Onboarding is in progress.',
|
shortDescription: 'Onboarding is in progress.',
|
||||||
longDescription: 'This tenant is still onboarding. It remains visible on management and review surfaces, but it is not selectable as active context until onboarding completes.',
|
longDescription: 'This environment is still onboarding. It remains visible on management and review surfaces, but it is not selectable as active context until onboarding completes.',
|
||||||
isInvalidFallback: false,
|
isInvalidFallback: false,
|
||||||
lifecycle: $lifecycle,
|
lifecycle: $lifecycle,
|
||||||
),
|
),
|
||||||
@ -72,8 +72,8 @@ public static function forLifecycle(TenantLifecycle $lifecycle): self
|
|||||||
badgeColor: 'success',
|
badgeColor: 'success',
|
||||||
badgeIcon: 'heroicon-m-check-circle',
|
badgeIcon: 'heroicon-m-check-circle',
|
||||||
badgeIconColor: null,
|
badgeIconColor: null,
|
||||||
shortDescription: 'Active tenant available for normal operations.',
|
shortDescription: 'Active environment available for normal operations.',
|
||||||
longDescription: 'This tenant is active and available across normal management, tenant selection, and operational follow-up flows.',
|
longDescription: 'This environment is active and available across normal management, environment selection, and operational follow-up flows.',
|
||||||
isInvalidFallback: false,
|
isInvalidFallback: false,
|
||||||
lifecycle: $lifecycle,
|
lifecycle: $lifecycle,
|
||||||
),
|
),
|
||||||
@ -83,8 +83,8 @@ public static function forLifecycle(TenantLifecycle $lifecycle): self
|
|||||||
badgeColor: 'gray',
|
badgeColor: 'gray',
|
||||||
badgeIcon: 'heroicon-m-archive-box',
|
badgeIcon: 'heroicon-m-archive-box',
|
||||||
badgeIconColor: null,
|
badgeIconColor: null,
|
||||||
shortDescription: 'Archived tenant retained for inspection only.',
|
shortDescription: 'Archived environment retained for inspection only.',
|
||||||
longDescription: 'This tenant remains available for inspection and audit history, but it is not selectable as active context until you restore it.',
|
longDescription: 'This environment remains available for inspection and audit history, but it is not selectable as active context until you restore it.',
|
||||||
isInvalidFallback: false,
|
isInvalidFallback: false,
|
||||||
lifecycle: $lifecycle,
|
lifecycle: $lifecycle,
|
||||||
),
|
),
|
||||||
|
|||||||
@ -135,7 +135,7 @@ public function build(Workspace $workspace, User $user): array
|
|||||||
$attentionEmptyState = [
|
$attentionEmptyState = [
|
||||||
'title' => $calmness['title'],
|
'title' => $calmness['title'],
|
||||||
'body' => $calmness['body'],
|
'body' => $calmness['body'],
|
||||||
'action_label' => $calmness['next_action']['label'] ?? 'Choose tenant',
|
'action_label' => $calmness['next_action']['label'] ?? __('localization.shell.choose_environment'),
|
||||||
'action_url' => $calmness['next_action']['url'] ?? ChooseTenant::getUrl(panel: 'admin'),
|
'action_url' => $calmness['next_action']['url'] ?? ChooseTenant::getUrl(panel: 'admin'),
|
||||||
];
|
];
|
||||||
|
|
||||||
@ -993,7 +993,7 @@ private function summaryMetrics(
|
|||||||
value: $accessibleTenantCount,
|
value: $accessibleTenantCount,
|
||||||
category: 'scope',
|
category: 'scope',
|
||||||
description: $accessibleTenantCount > 0
|
description: $accessibleTenantCount > 0
|
||||||
? 'ManagedEnvironment drill-down stays explicit from this workspace home.'
|
? 'Environment drill-down stays explicit from this workspace home.'
|
||||||
: 'No managed environments are available in this workspace yet.',
|
: 'No managed environments are available in this workspace yet.',
|
||||||
color: $accessibleTenantCount > 0 ? 'primary' : 'warning',
|
color: $accessibleTenantCount > 0 ? 'primary' : 'warning',
|
||||||
destination: $accessibleTenantCount > 0
|
destination: $accessibleTenantCount > 0
|
||||||
@ -1008,7 +1008,7 @@ private function summaryMetrics(
|
|||||||
description: 'Affected visible tenants with overdue findings, governance expiry, lapsed governance, or compare posture that needs review.',
|
description: 'Affected visible tenants with overdue findings, governance expiry, lapsed governance, or compare posture that needs review.',
|
||||||
color: $governanceAttentionTenantCount > 0 ? 'danger' : 'gray',
|
color: $governanceAttentionTenantCount > 0 ? 'danger' : 'gray',
|
||||||
destination: $governanceAttentionTenantCount > 0
|
destination: $governanceAttentionTenantCount > 0
|
||||||
? $this->chooseTenantTarget('Choose tenant')
|
? $this->chooseTenantTarget(__('localization.shell.choose_environment'))
|
||||||
: null,
|
: null,
|
||||||
),
|
),
|
||||||
$this->makeSummaryMetric(
|
$this->makeSummaryMetric(
|
||||||
@ -1272,15 +1272,15 @@ private function attentionMetricDestination(array $tenantContexts, User $user, s
|
|||||||
TenantBackupHealthAssessment::POSTURE_DEGRADED,
|
TenantBackupHealthAssessment::POSTURE_DEGRADED,
|
||||||
],
|
],
|
||||||
'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
|
'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
|
||||||
], 'Choose tenant'),
|
], __('localization.shell.choose_environment')),
|
||||||
'has_recovery_attention' => $this->filteredTenantRegistryTarget([
|
'has_recovery_attention' => $this->filteredTenantRegistryTarget([
|
||||||
'recovery_evidence' => [
|
'recovery_evidence' => [
|
||||||
TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED,
|
TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED,
|
||||||
TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_UNVALIDATED,
|
TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_UNVALIDATED,
|
||||||
],
|
],
|
||||||
'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
|
'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
|
||||||
], 'Choose tenant'),
|
], __('localization.shell.choose_environment')),
|
||||||
default => $this->chooseTenantTarget('Choose tenant'),
|
default => $this->chooseTenantTarget(__('localization.shell.choose_environment')),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1451,8 +1451,8 @@ private function quickActions(
|
|||||||
$actions = [
|
$actions = [
|
||||||
[
|
[
|
||||||
'key' => 'choose_tenant',
|
'key' => 'choose_tenant',
|
||||||
'label' => 'Choose tenant',
|
'label' => __('localization.shell.choose_environment'),
|
||||||
'description' => 'Deliberately enter tenant context from this workspace.',
|
'description' => 'Deliberately enter environment context from this workspace.',
|
||||||
'url' => ChooseTenant::getUrl(panel: 'admin'),
|
'url' => ChooseTenant::getUrl(panel: 'admin'),
|
||||||
'icon' => 'heroicon-o-building-office-2',
|
'icon' => 'heroicon-o-building-office-2',
|
||||||
'color' => 'primary',
|
'color' => 'primary',
|
||||||
@ -1529,12 +1529,12 @@ private function tenantRouteKey(ManagedEnvironment $tenant): string
|
|||||||
/**
|
/**
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
private function chooseTenantTarget(string $label = 'Choose tenant'): array
|
private function chooseTenantTarget(?string $label = null): array
|
||||||
{
|
{
|
||||||
return $this->destination(
|
return $this->destination(
|
||||||
kind: 'choose_tenant',
|
kind: 'choose_tenant',
|
||||||
url: ChooseTenant::getUrl(panel: 'admin'),
|
url: ChooseTenant::getUrl(panel: 'admin'),
|
||||||
label: $label,
|
label: $label ?? __('localization.shell.choose_environment'),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1542,12 +1542,12 @@ private function chooseTenantTarget(string $label = 'Choose tenant'): array
|
|||||||
* @param array<string, mixed> $filters
|
* @param array<string, mixed> $filters
|
||||||
* @return array<string, mixed>
|
* @return array<string, mixed>
|
||||||
*/
|
*/
|
||||||
private function filteredTenantRegistryTarget(array $filters, string $label = 'Choose tenant'): array
|
private function filteredTenantRegistryTarget(array $filters, ?string $label = null): array
|
||||||
{
|
{
|
||||||
return $this->destination(
|
return $this->destination(
|
||||||
kind: 'choose_tenant',
|
kind: 'choose_tenant',
|
||||||
url: $this->appendQuery(TenantResource::getUrl(panel: 'admin'), $filters),
|
url: $this->appendQuery(TenantResource::getUrl(panel: 'admin'), $filters),
|
||||||
label: $label,
|
label: $label ?? __('localization.shell.choose_environment'),
|
||||||
filters: $filters,
|
filters: $filters,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -69,7 +69,7 @@
|
|||||||
'empty_no_assignment' => 'Keine Baseline zugewiesen',
|
'empty_no_assignment' => 'Keine Baseline zugewiesen',
|
||||||
'empty_no_snapshot' => 'Kein Snapshot verfügbar',
|
'empty_no_snapshot' => 'Kein Snapshot verfügbar',
|
||||||
'findings_description' => 'Die Tenant-Konfiguration weicht vom Baseline-Profil ab.',
|
'findings_description' => 'Die Tenant-Konfiguration weicht vom Baseline-Profil ab.',
|
||||||
'rbac_summary_title' => 'Intune-RBAC-Rollendefinitionen',
|
'rbac_summary_title' => 'RBAC-Rollendefinitionen',
|
||||||
'rbac_summary_description' => 'Rollenzuweisungen sind in diesem Baseline-Compare-Release nicht enthalten.',
|
'rbac_summary_description' => 'Rollenzuweisungen sind in diesem Baseline-Compare-Release nicht enthalten.',
|
||||||
'rbac_summary_compared' => 'Verglichen',
|
'rbac_summary_compared' => 'Verglichen',
|
||||||
'rbac_summary_unchanged' => 'Unverändert',
|
'rbac_summary_unchanged' => 'Unverändert',
|
||||||
|
|||||||
@ -28,6 +28,23 @@
|
|||||||
'choose_workspace' => 'Workspace auswählen',
|
'choose_workspace' => 'Workspace auswählen',
|
||||||
'switch_workspace' => 'Workspace wechseln',
|
'switch_workspace' => 'Workspace wechseln',
|
||||||
'workspace_home' => 'Workspace-Start',
|
'workspace_home' => 'Workspace-Start',
|
||||||
|
'choose_environment' => 'Umgebung auswählen',
|
||||||
|
'environment_count' => ':count Umgebung|:count Umgebungen',
|
||||||
|
'choose_environment_description' => 'Wählen Sie die Umgebung für Ihren normalen aktiven Betriebskontext aus.',
|
||||||
|
'workspace_wide_available_without_environment' => 'Keine Umgebung ausgewählt. Workspace-weite Seiten wie Operationen und Managed Environments bleiben verfügbar.',
|
||||||
|
'add_environment' => 'Umgebung hinzufügen',
|
||||||
|
'all_environments' => 'Alle Umgebungen',
|
||||||
|
'show_all_environments' => 'Alle Umgebungen anzeigen',
|
||||||
|
'managed_environments_title' => 'Managed Environments',
|
||||||
|
'no_managed_environments_yet' => 'Noch keine Managed Environments',
|
||||||
|
'managed_environments_empty_state_description' => 'Fügen Sie Ihre erste Managed Environment hinzu, um Inventar, Backups, Drift-Erkennung und Richtlinien zu verwalten.',
|
||||||
|
'environment_scope' => 'Umgebungskontext',
|
||||||
|
'select_environment' => 'Umgebung auswählen',
|
||||||
|
'selected_environment' => 'Ausgewählte Umgebung',
|
||||||
|
'no_environment_selected' => 'Keine Umgebung ausgewählt',
|
||||||
|
'switch_environment' => 'Umgebung wechseln',
|
||||||
|
'clear_environment_scope' => 'Umgebungskontext löschen',
|
||||||
|
'back_to_environment_registry' => 'Zur Umgebungsregistrierung zurück',
|
||||||
'tenant_scope' => 'Tenant-Kontext',
|
'tenant_scope' => 'Tenant-Kontext',
|
||||||
'select_tenant' => 'Tenant auswählen',
|
'select_tenant' => 'Tenant auswählen',
|
||||||
'selected_tenant' => 'Ausgewählter Tenant',
|
'selected_tenant' => 'Ausgewählter Tenant',
|
||||||
@ -37,9 +54,13 @@
|
|||||||
'context_unavailable' => 'Kontext nicht verfügbar',
|
'context_unavailable' => 'Kontext nicht verfügbar',
|
||||||
'context_unavailable_workspace' => 'Der angeforderte Kontext konnte nicht wiederhergestellt werden. Die Shell zeigt stattdessen einen gültigen Workspace-Kontext.',
|
'context_unavailable_workspace' => 'Der angeforderte Kontext konnte nicht wiederhergestellt werden. Die Shell zeigt stattdessen einen gültigen Workspace-Kontext.',
|
||||||
'context_unavailable_no_workspace' => 'Wählen Sie einen Workspace aus, um mit einem gültigen Admin-Kontext fortzufahren.',
|
'context_unavailable_no_workspace' => 'Wählen Sie einen Workspace aus, um mit einem gültigen Admin-Kontext fortzufahren.',
|
||||||
|
'no_active_environments' => 'Keine aktiven Umgebungen verfügbar',
|
||||||
|
'no_active_environments_description' => 'In diesem Workspace sind keine auswählbaren aktiven Umgebungen für den normalen Betriebskontext verfügbar. Workspace-weite Seiten funktionieren weiterhin ohne ausgewählte Umgebung, und Sie können Onboarding- oder archivierte Einträge über Managed Environments prüfen.',
|
||||||
|
'view_managed_environments' => 'Managed Environments anzeigen',
|
||||||
'no_active_tenants' => 'In diesem Workspace sind keine aktiven Tenants für den Standardbetrieb verfügbar.',
|
'no_active_tenants' => 'In diesem Workspace sind keine aktiven Tenants für den Standardbetrieb verfügbar.',
|
||||||
'view_managed_tenants' => 'Managed Tenants anzeigen',
|
'view_managed_tenants' => 'Managed Tenants anzeigen',
|
||||||
'workspace_wide_available' => 'Kein Tenant ausgewählt. Workspace-weite Seiten bleiben verfügbar; ein Tenant setzt nur den normalen aktiven Betriebskontext.',
|
'workspace_wide_available' => 'Kein Tenant ausgewählt. Workspace-weite Seiten bleiben verfügbar; ein Tenant setzt nur den normalen aktiven Betriebskontext.',
|
||||||
|
'search_environments' => 'Umgebungen suchen...',
|
||||||
'search_tenants' => 'Tenants suchen...',
|
'search_tenants' => 'Tenants suchen...',
|
||||||
'choose_workspace_first' => 'Wählen Sie zuerst einen Workspace aus.',
|
'choose_workspace_first' => 'Wählen Sie zuerst einen Workspace aus.',
|
||||||
],
|
],
|
||||||
@ -76,6 +97,7 @@
|
|||||||
],
|
],
|
||||||
'dashboard' => [
|
'dashboard' => [
|
||||||
'tenant_title' => 'Tenant-Dashboard',
|
'tenant_title' => 'Tenant-Dashboard',
|
||||||
|
'environment_title' => 'Umgebungs-Dashboard',
|
||||||
'system_title' => 'System-Dashboard',
|
'system_title' => 'System-Dashboard',
|
||||||
'more_actions' => 'Mehr',
|
'more_actions' => 'Mehr',
|
||||||
'request_support' => 'Support anfragen',
|
'request_support' => 'Support anfragen',
|
||||||
@ -126,11 +148,12 @@
|
|||||||
'recovery_mode_enabled' => 'Wiederherstellungsmodus aktiviert',
|
'recovery_mode_enabled' => 'Wiederherstellungsmodus aktiviert',
|
||||||
'recovery_mode_ended' => 'Wiederherstellungsmodus beendet',
|
'recovery_mode_ended' => 'Wiederherstellungsmodus beendet',
|
||||||
'overview' => [
|
'overview' => [
|
||||||
'page_subheading' => 'Tenant-Governance-Übersicht',
|
'page_subheading' => 'Umgebungs-Governance-Übersicht',
|
||||||
'context_workspace' => 'Aktueller Workspace',
|
'context_workspace' => 'Aktueller Workspace',
|
||||||
'context_no_tenant' => 'Kein Tenant ausgewählt',
|
'context_no_tenant' => 'Kein Tenant ausgewählt',
|
||||||
|
'context_no_environment' => 'Keine Umgebung ausgewählt',
|
||||||
'context_workspace_chip' => 'Workspace: :workspace',
|
'context_workspace_chip' => 'Workspace: :workspace',
|
||||||
'context_provider_chip' => ':provider-Tenant',
|
'context_provider_chip' => ':provider-Umgebung',
|
||||||
'context_latest_activity_chip' => 'Letzte Aktivität: :time',
|
'context_latest_activity_chip' => 'Letzte Aktivität: :time',
|
||||||
'status_unavailable' => 'Nicht verfügbar',
|
'status_unavailable' => 'Nicht verfügbar',
|
||||||
'status_blocked' => 'Blockiert',
|
'status_blocked' => 'Blockiert',
|
||||||
@ -141,6 +164,8 @@
|
|||||||
'status_needs_action' => 'Aufmerksamkeit erforderlich',
|
'status_needs_action' => 'Aufmerksamkeit erforderlich',
|
||||||
'tenant_context_unavailable_headline' => 'Tenant-Kontext ist nicht verfügbar.',
|
'tenant_context_unavailable_headline' => 'Tenant-Kontext ist nicht verfügbar.',
|
||||||
'tenant_context_unavailable_summary' => 'Wählen Sie einen Tenant aus, um die entscheidungsorientierte Dashboard-Übersicht anzuzeigen.',
|
'tenant_context_unavailable_summary' => 'Wählen Sie einen Tenant aus, um die entscheidungsorientierte Dashboard-Übersicht anzuzeigen.',
|
||||||
|
'environment_context_unavailable_headline' => 'Umgebungskontext ist nicht verfügbar.',
|
||||||
|
'environment_context_unavailable_summary' => 'Wählen Sie eine Umgebung aus, um die entscheidungsorientierte Dashboard-Übersicht anzuzeigen.',
|
||||||
'posture_blocked_headline' => 'Provider-Berechtigungen blockieren Tenant-Workflows.',
|
'posture_blocked_headline' => 'Provider-Berechtigungen blockieren Tenant-Workflows.',
|
||||||
'posture_blocked_summary' => 'Erforderliche Anwendungsberechtigungen fehlen. Provider-gestützte Abläufe können deshalb nicht als bereit bewertet werden.',
|
'posture_blocked_summary' => 'Erforderliche Anwendungsberechtigungen fehlen. Provider-gestützte Abläufe können deshalb nicht als bereit bewertet werden.',
|
||||||
'posture_calm_headline' => 'Kein unmittelbarer Tenant-Blocker ist sichtbar.',
|
'posture_calm_headline' => 'Kein unmittelbarer Tenant-Blocker ist sichtbar.',
|
||||||
@ -539,11 +564,11 @@
|
|||||||
'sync_action_primary' => 'Richtlinien synchronisieren',
|
'sync_action_primary' => 'Richtlinien synchronisieren',
|
||||||
'sync_action_secondary' => 'Synchronisieren',
|
'sync_action_secondary' => 'Synchronisieren',
|
||||||
'sync_modal_heading' => 'Richtlinien-Inventar synchronisieren',
|
'sync_modal_heading' => 'Richtlinien-Inventar synchronisieren',
|
||||||
'sync_modal_description' => 'Diese Aktion reiht eine Hintergrundssynchronisierung für unterstützte Richtlinientypen im aktuellen Tenant ein.',
|
'sync_modal_description' => 'Diese Aktion reiht eine Hintergrundssynchronisierung für unterstützte Richtlinientypen in der aktuellen Umgebung ein.',
|
||||||
'sync_permission_tooltip' => 'Sie haben keine Berechtigung, Richtlinien zu synchronisieren.',
|
'sync_permission_tooltip' => 'Sie haben keine Berechtigung, Richtlinien zu synchronisieren.',
|
||||||
'capture_snapshot_action' => 'Snapshot erfassen',
|
'capture_snapshot_action' => 'Snapshot erfassen',
|
||||||
'capture_snapshot_modal_heading' => 'Snapshot jetzt erfassen',
|
'capture_snapshot_modal_heading' => 'Snapshot jetzt erfassen',
|
||||||
'capture_snapshot_modal_subheading' => 'Diese Aktion reiht einen Hintergrundjob ein, der die aktuelle Konfiguration aus Microsoft Graph abruft und eine neue Richtlinienversion speichert.',
|
'capture_snapshot_modal_subheading' => 'Diese Aktion reiht einen Hintergrundjob ein, der die aktuelle Konfiguration für die aktuelle Umgebung erfasst und eine neue Richtlinienversion speichert.',
|
||||||
'capture_snapshot_include_assignments' => 'Zuweisungen einschließen',
|
'capture_snapshot_include_assignments' => 'Zuweisungen einschließen',
|
||||||
'capture_snapshot_include_assignments_helper' => 'Erfasst Include-/Exclude-Ziele und Filter für Zuweisungen.',
|
'capture_snapshot_include_assignments_helper' => 'Erfasst Include-/Exclude-Ziele und Filter für Zuweisungen.',
|
||||||
'capture_snapshot_include_scope_tags' => 'Scope-Tags einschließen',
|
'capture_snapshot_include_scope_tags' => 'Scope-Tags einschließen',
|
||||||
@ -593,7 +618,7 @@
|
|||||||
'ignore_permission_tooltip' => 'Sie haben keine Berechtigung, Richtlinien zu ignorieren.',
|
'ignore_permission_tooltip' => 'Sie haben keine Berechtigung, Richtlinien zu ignorieren.',
|
||||||
'policy_ignored' => 'Richtlinie ignoriert',
|
'policy_ignored' => 'Richtlinie ignoriert',
|
||||||
'empty_state_heading' => 'Noch keine Richtlinien im Inventory',
|
'empty_state_heading' => 'Noch keine Richtlinien im Inventory',
|
||||||
'empty_state_description' => 'Starte eine Synchronisierung, um das Richtlinien-Inventar dieses Tenants mit Versionen, Wiederherstellbarkeit und Governance-Evidence aufzubauen.',
|
'empty_state_description' => 'Starte eine Synchronisierung, um das Richtlinien-Inventar dieser Umgebung mit Versionen, Wiederherstellbarkeit und Governance-Evidence aufzubauen.',
|
||||||
'delete_queued_body' => 'Löschung für :count Richtlinien eingeplant.',
|
'delete_queued_body' => 'Löschung für :count Richtlinien eingeplant.',
|
||||||
],
|
],
|
||||||
'versions' => [
|
'versions' => [
|
||||||
@ -640,7 +665,7 @@
|
|||||||
'metadata_only_tooltip' => 'Für reine Metadaten-Snapshots deaktiviert (Graph hat keine Richtlinieneinstellungen geliefert).',
|
'metadata_only_tooltip' => 'Für reine Metadaten-Snapshots deaktiviert (Graph hat keine Richtlinieneinstellungen geliefert).',
|
||||||
'restore_disabled_metadata_title' => 'Wiederherstellung für reinen Metadaten-Snapshot deaktiviert',
|
'restore_disabled_metadata_title' => 'Wiederherstellung für reinen Metadaten-Snapshot deaktiviert',
|
||||||
'restore_disabled_metadata_body' => 'Dieser Snapshot enthält nur Metadaten; Graph hat keine Richtlinieneinstellungen für eine Wiederherstellung geliefert.',
|
'restore_disabled_metadata_body' => 'Dieser Snapshot enthält nur Metadaten; Graph hat keine Richtlinieneinstellungen für eine Wiederherstellung geliefert.',
|
||||||
'different_tenant_title' => 'Richtlinienversion gehört zu einem anderen Tenant',
|
'different_tenant_title' => 'Richtlinienversion gehört zu einer anderen Umgebung',
|
||||||
'missing_policy_title' => 'Richtlinie für diese Version konnte nicht gefunden werden',
|
'missing_policy_title' => 'Richtlinie für diese Version konnte nicht gefunden werden',
|
||||||
'backup_set_name' => 'Richtlinienversions-Wiederherstellung - :policy - v:version',
|
'backup_set_name' => 'Richtlinienversions-Wiederherstellung - :policy - v:version',
|
||||||
'archive' => 'Archivieren',
|
'archive' => 'Archivieren',
|
||||||
@ -666,9 +691,9 @@
|
|||||||
'fallback_display_name' => 'Version :version',
|
'fallback_display_name' => 'Version :version',
|
||||||
],
|
],
|
||||||
'relation' => [
|
'relation' => [
|
||||||
'restore_to_microsoft_intune' => 'In Microsoft Intune wiederherstellen',
|
'restore_to_microsoft_intune' => 'In die Umgebung wiederherstellen',
|
||||||
'restore_heading' => 'Version :version in Microsoft Intune wiederherstellen?',
|
'restore_heading' => 'Version :version in die Umgebung wiederherstellen?',
|
||||||
'restore_subheading' => 'Erstellt einen Wiederherstellungslauf mit diesem Richtlinienversions-Snapshot.',
|
'restore_subheading' => 'Erstellt einen Wiederherstellungslauf für die aktuelle Umgebung mit diesem Richtlinienversions-Snapshot.',
|
||||||
'missing_context_title' => 'Tenant- oder Benutzerkontext fehlt.',
|
'missing_context_title' => 'Tenant- oder Benutzerkontext fehlt.',
|
||||||
'restore_run_failed_title' => 'Wiederherstellungslauf konnte nicht gestartet werden',
|
'restore_run_failed_title' => 'Wiederherstellungslauf konnte nicht gestartet werden',
|
||||||
'restore_run_started_title' => 'Wiederherstellungslauf gestartet',
|
'restore_run_started_title' => 'Wiederherstellungslauf gestartet',
|
||||||
|
|||||||
@ -95,7 +95,7 @@
|
|||||||
|
|
||||||
// Findings section
|
// Findings section
|
||||||
'findings_description' => 'The tenant configuration drifted from the baseline profile.',
|
'findings_description' => 'The tenant configuration drifted from the baseline profile.',
|
||||||
'rbac_summary_title' => 'Intune RBAC Role Definitions',
|
'rbac_summary_title' => 'RBAC role definitions',
|
||||||
'rbac_summary_description' => 'Role Assignments are not included in this baseline compare release.',
|
'rbac_summary_description' => 'Role Assignments are not included in this baseline compare release.',
|
||||||
'rbac_summary_compared' => 'Compared',
|
'rbac_summary_compared' => 'Compared',
|
||||||
'rbac_summary_unchanged' => 'Unchanged',
|
'rbac_summary_unchanged' => 'Unchanged',
|
||||||
|
|||||||
@ -28,6 +28,23 @@
|
|||||||
'choose_workspace' => 'Choose workspace',
|
'choose_workspace' => 'Choose workspace',
|
||||||
'switch_workspace' => 'Switch workspace',
|
'switch_workspace' => 'Switch workspace',
|
||||||
'workspace_home' => 'Workspace Home',
|
'workspace_home' => 'Workspace Home',
|
||||||
|
'choose_environment' => 'Choose environment',
|
||||||
|
'environment_count' => ':count environment|:count environments',
|
||||||
|
'choose_environment_description' => 'Select the environment for your normal active operating context.',
|
||||||
|
'workspace_wide_available_without_environment' => 'No environment selected is still a valid workspace state on workspace-wide pages such as operations and managed environments.',
|
||||||
|
'add_environment' => 'Add environment',
|
||||||
|
'all_environments' => 'All environments',
|
||||||
|
'show_all_environments' => 'Show all environments',
|
||||||
|
'managed_environments_title' => 'Managed environments',
|
||||||
|
'no_managed_environments_yet' => 'No managed environments yet',
|
||||||
|
'managed_environments_empty_state_description' => 'Add your first managed environment to start managing inventory, backups, drift detection, and policies.',
|
||||||
|
'environment_scope' => 'Environment scope',
|
||||||
|
'select_environment' => 'Select environment',
|
||||||
|
'selected_environment' => 'Selected environment',
|
||||||
|
'no_environment_selected' => 'No environment selected',
|
||||||
|
'switch_environment' => 'Switch environment',
|
||||||
|
'clear_environment_scope' => 'Clear environment scope',
|
||||||
|
'back_to_environment_registry' => 'Back to environment registry',
|
||||||
'tenant_scope' => 'Tenant scope',
|
'tenant_scope' => 'Tenant scope',
|
||||||
'select_tenant' => 'Select tenant',
|
'select_tenant' => 'Select tenant',
|
||||||
'selected_tenant' => 'Selected tenant',
|
'selected_tenant' => 'Selected tenant',
|
||||||
@ -37,9 +54,13 @@
|
|||||||
'context_unavailable' => 'Context unavailable',
|
'context_unavailable' => 'Context unavailable',
|
||||||
'context_unavailable_workspace' => 'The requested scope could not be restored. The shell is showing a valid workspace state instead.',
|
'context_unavailable_workspace' => 'The requested scope could not be restored. The shell is showing a valid workspace state instead.',
|
||||||
'context_unavailable_no_workspace' => 'Choose a workspace to continue with a valid admin context.',
|
'context_unavailable_no_workspace' => 'Choose a workspace to continue with a valid admin context.',
|
||||||
|
'no_active_environments' => 'No active environments available',
|
||||||
|
'no_active_environments_description' => 'There are no selectable active environments for the normal operating context in this workspace. Workspace-level pages still work with no environment selected, and you can inspect onboarding or archived records through managed environments.',
|
||||||
|
'view_managed_environments' => 'View managed environments',
|
||||||
'no_active_tenants' => 'No active tenants are available for the standard operating context in this workspace.',
|
'no_active_tenants' => 'No active tenants are available for the standard operating context in this workspace.',
|
||||||
'view_managed_tenants' => 'View managed tenants',
|
'view_managed_tenants' => 'View managed tenants',
|
||||||
'workspace_wide_available' => 'No tenant selected. Workspace-wide pages remain available, and choosing a tenant only sets the normal active operating context.',
|
'workspace_wide_available' => 'No tenant selected. Workspace-wide pages remain available, and choosing a tenant only sets the normal active operating context.',
|
||||||
|
'search_environments' => 'Search environments...',
|
||||||
'search_tenants' => 'Search tenants...',
|
'search_tenants' => 'Search tenants...',
|
||||||
'choose_workspace_first' => 'Choose a workspace first.',
|
'choose_workspace_first' => 'Choose a workspace first.',
|
||||||
],
|
],
|
||||||
@ -76,6 +97,7 @@
|
|||||||
],
|
],
|
||||||
'dashboard' => [
|
'dashboard' => [
|
||||||
'tenant_title' => 'Tenant dashboard',
|
'tenant_title' => 'Tenant dashboard',
|
||||||
|
'environment_title' => 'Environment dashboard',
|
||||||
'system_title' => 'System dashboard',
|
'system_title' => 'System dashboard',
|
||||||
'more_actions' => 'More',
|
'more_actions' => 'More',
|
||||||
'request_support' => 'Request support',
|
'request_support' => 'Request support',
|
||||||
@ -126,11 +148,12 @@
|
|||||||
'recovery_mode_enabled' => 'Recovery mode enabled',
|
'recovery_mode_enabled' => 'Recovery mode enabled',
|
||||||
'recovery_mode_ended' => 'Recovery mode ended',
|
'recovery_mode_ended' => 'Recovery mode ended',
|
||||||
'overview' => [
|
'overview' => [
|
||||||
'page_subheading' => 'Tenant governance overview',
|
'page_subheading' => 'Environment governance overview',
|
||||||
'context_workspace' => 'Current workspace',
|
'context_workspace' => 'Current workspace',
|
||||||
'context_no_tenant' => 'No tenant selected',
|
'context_no_tenant' => 'No tenant selected',
|
||||||
|
'context_no_environment' => 'No environment selected',
|
||||||
'context_workspace_chip' => 'Workspace: :workspace',
|
'context_workspace_chip' => 'Workspace: :workspace',
|
||||||
'context_provider_chip' => ':provider tenant',
|
'context_provider_chip' => ':provider environment',
|
||||||
'context_latest_activity_chip' => 'Latest activity: :time',
|
'context_latest_activity_chip' => 'Latest activity: :time',
|
||||||
'status_unavailable' => 'Unavailable',
|
'status_unavailable' => 'Unavailable',
|
||||||
'status_blocked' => 'Blocked',
|
'status_blocked' => 'Blocked',
|
||||||
@ -141,6 +164,8 @@
|
|||||||
'status_needs_action' => 'Needs attention',
|
'status_needs_action' => 'Needs attention',
|
||||||
'tenant_context_unavailable_headline' => 'Tenant context is not available.',
|
'tenant_context_unavailable_headline' => 'Tenant context is not available.',
|
||||||
'tenant_context_unavailable_summary' => 'Select a tenant to view the decision-first dashboard overview.',
|
'tenant_context_unavailable_summary' => 'Select a tenant to view the decision-first dashboard overview.',
|
||||||
|
'environment_context_unavailable_headline' => 'Environment context is not available.',
|
||||||
|
'environment_context_unavailable_summary' => 'Select an environment to view the decision-first dashboard overview.',
|
||||||
'posture_blocked_headline' => 'Provider permissions are blocking tenant workflows.',
|
'posture_blocked_headline' => 'Provider permissions are blocking tenant workflows.',
|
||||||
'posture_blocked_summary' => 'Required application permissions are missing, so provider-backed operations cannot be treated as healthy readiness.',
|
'posture_blocked_summary' => 'Required application permissions are missing, so provider-backed operations cannot be treated as healthy readiness.',
|
||||||
'posture_calm_headline' => 'No immediate tenant blocker is visible.',
|
'posture_calm_headline' => 'No immediate tenant blocker is visible.',
|
||||||
@ -539,11 +564,11 @@
|
|||||||
'sync_action_primary' => 'Sync policies',
|
'sync_action_primary' => 'Sync policies',
|
||||||
'sync_action_secondary' => 'Sync',
|
'sync_action_secondary' => 'Sync',
|
||||||
'sync_modal_heading' => 'Sync policy inventory',
|
'sync_modal_heading' => 'Sync policy inventory',
|
||||||
'sync_modal_description' => 'This queues a background sync operation for supported policy types in the current tenant.',
|
'sync_modal_description' => 'This queues a background sync operation for supported policy types in the current environment.',
|
||||||
'sync_permission_tooltip' => 'You do not have permission to sync policies.',
|
'sync_permission_tooltip' => 'You do not have permission to sync policies.',
|
||||||
'capture_snapshot_action' => 'Capture snapshot',
|
'capture_snapshot_action' => 'Capture snapshot',
|
||||||
'capture_snapshot_modal_heading' => 'Capture snapshot now',
|
'capture_snapshot_modal_heading' => 'Capture snapshot now',
|
||||||
'capture_snapshot_modal_subheading' => 'This queues a background job that fetches the latest configuration from Microsoft Graph and stores a new policy version.',
|
'capture_snapshot_modal_subheading' => 'This queues a background job that captures the latest configuration for the current environment and stores a new policy version.',
|
||||||
'capture_snapshot_include_assignments' => 'Include assignments',
|
'capture_snapshot_include_assignments' => 'Include assignments',
|
||||||
'capture_snapshot_include_assignments_helper' => 'Captures assignment include/exclude targeting and filters.',
|
'capture_snapshot_include_assignments_helper' => 'Captures assignment include/exclude targeting and filters.',
|
||||||
'capture_snapshot_include_scope_tags' => 'Include scope tags',
|
'capture_snapshot_include_scope_tags' => 'Include scope tags',
|
||||||
@ -593,7 +618,7 @@
|
|||||||
'ignore_permission_tooltip' => 'You do not have permission to ignore policies.',
|
'ignore_permission_tooltip' => 'You do not have permission to ignore policies.',
|
||||||
'policy_ignored' => 'Policy ignored',
|
'policy_ignored' => 'Policy ignored',
|
||||||
'empty_state_heading' => 'No policies in inventory yet',
|
'empty_state_heading' => 'No policies in inventory yet',
|
||||||
'empty_state_description' => 'Run a sync to build this tenant\'s policy inventory, including versions, restore readiness, and governance evidence.',
|
'empty_state_description' => 'Run a sync to build this environment\'s policy inventory, including versions, restore readiness, and governance evidence.',
|
||||||
'delete_queued_body' => 'Queued deletion for :count policies.',
|
'delete_queued_body' => 'Queued deletion for :count policies.',
|
||||||
],
|
],
|
||||||
'versions' => [
|
'versions' => [
|
||||||
@ -640,7 +665,7 @@
|
|||||||
'metadata_only_tooltip' => 'Disabled for metadata-only snapshots (Graph did not provide policy settings).',
|
'metadata_only_tooltip' => 'Disabled for metadata-only snapshots (Graph did not provide policy settings).',
|
||||||
'restore_disabled_metadata_title' => 'Restore disabled for metadata-only snapshot',
|
'restore_disabled_metadata_title' => 'Restore disabled for metadata-only snapshot',
|
||||||
'restore_disabled_metadata_body' => 'This snapshot only contains metadata; Graph did not provide policy settings to restore.',
|
'restore_disabled_metadata_body' => 'This snapshot only contains metadata; Graph did not provide policy settings to restore.',
|
||||||
'different_tenant_title' => 'Policy version belongs to a different tenant',
|
'different_tenant_title' => 'Policy version belongs to a different environment',
|
||||||
'missing_policy_title' => 'Policy could not be found for this version',
|
'missing_policy_title' => 'Policy could not be found for this version',
|
||||||
'backup_set_name' => 'Policy version restore - :policy - v:version',
|
'backup_set_name' => 'Policy version restore - :policy - v:version',
|
||||||
'archive' => 'Archive',
|
'archive' => 'Archive',
|
||||||
@ -666,9 +691,9 @@
|
|||||||
'fallback_display_name' => 'Version :version',
|
'fallback_display_name' => 'Version :version',
|
||||||
],
|
],
|
||||||
'relation' => [
|
'relation' => [
|
||||||
'restore_to_microsoft_intune' => 'Restore to Microsoft Intune',
|
'restore_to_microsoft_intune' => 'Restore to environment',
|
||||||
'restore_heading' => 'Restore version :version to Microsoft Intune?',
|
'restore_heading' => 'Restore version :version to environment?',
|
||||||
'restore_subheading' => 'Creates a restore run using this policy version snapshot.',
|
'restore_subheading' => 'Creates a restore run for the current environment using this policy version snapshot.',
|
||||||
'missing_context_title' => 'Missing tenant or user context.',
|
'missing_context_title' => 'Missing tenant or user context.',
|
||||||
'restore_run_failed_title' => 'Restore run failed to start',
|
'restore_run_failed_title' => 'Restore run failed to start',
|
||||||
'restore_run_started_title' => 'Restore run started',
|
'restore_run_started_title' => 'Restore run started',
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
<x-filament::section>
|
<x-filament::section>
|
||||||
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
||||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||||
Launch the compare matrix with the currently known baseline profile and any carried subject focus from this tenant landing.
|
Launch the compare matrix with the currently known baseline profile and any carried subject focus from this environment landing.
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<x-filament::button tag="a" :href="$openCompareMatrixUrl" color="gray" icon="heroicon-o-squares-2x2">
|
<x-filament::button tag="a" :href="$openCompareMatrixUrl" color="gray" icon="heroicon-o-squares-2x2">
|
||||||
|
|||||||
@ -2,6 +2,7 @@
|
|||||||
@php
|
@php
|
||||||
$tenants = $this->getTenants();
|
$tenants = $this->getTenants();
|
||||||
$workspace = app(\App\Support\Workspaces\WorkspaceContext::class)->currentWorkspace();
|
$workspace = app(\App\Support\Workspaces\WorkspaceContext::class)->currentWorkspace();
|
||||||
|
$environmentCount = $tenants->count();
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@if ($tenants->isEmpty())
|
@if ($tenants->isEmpty())
|
||||||
@ -22,9 +23,9 @@ class="h-7 w-7 text-primary-500 dark:text-primary-400"
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">No active tenants available</h3>
|
<h3 class="text-base font-semibold text-gray-900 dark:text-white">{{ __('localization.shell.no_active_environments') }}</h3>
|
||||||
<p class="mx-auto mt-2 max-w-xs text-sm text-gray-500 dark:text-gray-400">
|
<p class="mx-auto mt-2 max-w-xs text-sm text-gray-500 dark:text-gray-400">
|
||||||
There are no selectable active tenants for the normal operating context in this workspace. Workspace-level pages still work with no tenant selected, and you can inspect onboarding or archived records through managed tenants.
|
{{ __('localization.shell.no_active_environments_description') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-6 flex flex-col items-center gap-3">
|
<div class="mt-6 flex flex-col items-center gap-3">
|
||||||
@ -34,7 +35,7 @@ class="h-7 w-7 text-primary-500 dark:text-primary-400"
|
|||||||
icon="heroicon-m-arrow-right"
|
icon="heroicon-m-arrow-right"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
View managed tenants
|
{{ __('localization.shell.view_managed_environments') }}
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
|
|
||||||
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
||||||
@ -58,13 +59,13 @@ class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors
|
|||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
· {{ $tenants->count() }} {{ \Illuminate\Support\Str::plural('tenant', $tenants->count()) }}
|
· {{ trans_choice('localization.shell.environment_count', $environmentCount, ['count' => $environmentCount]) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">Select the tenant for your normal active operating context.</p>
|
<p class="mb-2 text-sm text-gray-500 dark:text-gray-400">{{ __('localization.shell.choose_environment_description') }}</p>
|
||||||
<p class="mb-4 text-xs text-gray-500 dark:text-gray-400">No tenant selected is still a valid workspace state on workspace-wide pages such as operations and managed tenants.</p>
|
<p class="mb-4 text-xs text-gray-500 dark:text-gray-400">{{ __('localization.shell.workspace_wide_available_without_environment') }}</p>
|
||||||
|
|
||||||
{{-- ManagedEnvironment cards --}}
|
{{-- ManagedEnvironment cards --}}
|
||||||
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-{{ min($tenants->count(), 3) }}">
|
<div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-{{ min($tenants->count(), 3) }}">
|
||||||
@ -138,7 +139,7 @@ class="h-4 w-4 text-gray-400 dark:text-gray-500"
|
|||||||
<a href="{{ route('admin.onboarding') }}"
|
<a href="{{ route('admin.onboarding') }}"
|
||||||
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
<x-filament::icon icon="heroicon-m-plus" class="h-4 w-4" />
|
<x-filament::icon icon="heroicon-m-plus" class="h-4 w-4" />
|
||||||
Add tenant
|
{{ __('localization.shell.add_environment') }}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
||||||
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
|
|||||||
@ -1,6 +1,7 @@
|
|||||||
<x-filament-panels::page>
|
<x-filament-panels::page>
|
||||||
@php
|
@php
|
||||||
$tenants = $this->getTenants();
|
$tenants = $this->getTenants();
|
||||||
|
$environmentCount = $tenants->count();
|
||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@if ($tenants->isEmpty())
|
@if ($tenants->isEmpty())
|
||||||
@ -21,9 +22,9 @@ class="h-7 w-7 text-primary-500 dark:text-primary-400"
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="text-base font-semibold text-gray-900 dark:text-white">No managed tenants yet</h3>
|
<h3 class="text-base font-semibold text-gray-900 dark:text-white">{{ __('localization.shell.no_managed_environments_yet') }}</h3>
|
||||||
<p class="mx-auto mt-2 max-w-xs text-sm text-gray-500 dark:text-gray-400">
|
<p class="mx-auto mt-2 max-w-xs text-sm text-gray-500 dark:text-gray-400">
|
||||||
Connect your first Microsoft Entra tenant to start managing inventory, backups, drift detection, and policies.
|
{{ __('localization.shell.managed_environments_empty_state_description') }}
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="mt-6 flex flex-col items-center gap-3">
|
<div class="mt-6 flex flex-col items-center gap-3">
|
||||||
@ -33,7 +34,7 @@ class="h-7 w-7 text-primary-500 dark:text-primary-400"
|
|||||||
icon="heroicon-m-plus"
|
icon="heroicon-m-plus"
|
||||||
size="lg"
|
size="lg"
|
||||||
>
|
>
|
||||||
Add tenant
|
{{ __('localization.shell.add_environment') }}
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
|
|
||||||
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
||||||
@ -55,7 +56,7 @@ class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors
|
|||||||
{{ $this->workspace->name }}
|
{{ $this->workspace->name }}
|
||||||
</div>
|
</div>
|
||||||
<span class="text-sm text-gray-500 dark:text-gray-400">
|
<span class="text-sm text-gray-500 dark:text-gray-400">
|
||||||
· {{ $tenants->count() }} {{ \Illuminate\Support\Str::plural('tenant', $tenants->count()) }}
|
· {{ trans_choice('localization.shell.environment_count', $environmentCount, ['count' => $environmentCount]) }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -66,7 +67,7 @@ class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors
|
|||||||
icon="heroicon-m-arrow-right"
|
icon="heroicon-m-arrow-right"
|
||||||
icon-position="after"
|
icon-position="after"
|
||||||
>
|
>
|
||||||
Choose tenant
|
{{ __('localization.shell.choose_environment') }}
|
||||||
</x-filament::button>
|
</x-filament::button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -129,7 +130,7 @@ class="h-4 w-4 text-gray-400 dark:text-gray-500"
|
|||||||
<a href="{{ route('admin.onboarding') }}"
|
<a href="{{ route('admin.onboarding') }}"
|
||||||
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
<x-filament::icon icon="heroicon-m-plus" class="h-4 w-4" />
|
<x-filament::icon icon="heroicon-m-plus" class="h-4 w-4" />
|
||||||
Add tenant
|
{{ __('localization.shell.add_environment') }}
|
||||||
</a>
|
</a>
|
||||||
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
<a href="{{ route('filament.admin.pages.choose-workspace') }}"
|
||||||
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
class="inline-flex items-center gap-1.5 text-sm text-gray-500 transition-colors hover:text-gray-700 dark:text-gray-400 dark:hover:text-gray-200">
|
||||||
|
|||||||
@ -31,7 +31,7 @@
|
|||||||
@endphp
|
@endphp
|
||||||
|
|
||||||
@php
|
@php
|
||||||
$tenantLabel = $currentTenantName ?? __('localization.shell.no_tenant_selected');
|
$tenantLabel = $currentTenantName ?? __('localization.shell.no_environment_selected');
|
||||||
$workspaceLabel = $workspace?->name ?? __('localization.shell.choose_workspace');
|
$workspaceLabel = $workspace?->name ?? __('localization.shell.choose_workspace');
|
||||||
$hasActiveTenant = $currentTenantName !== null;
|
$hasActiveTenant = $currentTenantName !== null;
|
||||||
$managedTenantsUrl = $workspace
|
$managedTenantsUrl = $workspace
|
||||||
@ -64,7 +64,7 @@ class="inline-flex items-center gap-1.5 rounded-l-lg px-2.5 py-1.5 font-medium t
|
|||||||
<x-slot name="trigger">
|
<x-slot name="trigger">
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="{{ $workspace ? __('localization.shell.tenant_scope') : __('localization.shell.select_tenant') }}"
|
aria-label="{{ $workspace ? __('localization.shell.environment_scope') : __('localization.shell.select_environment') }}"
|
||||||
class="inline-flex items-center gap-1.5 rounded-r-lg px-2 py-1.5 transition hover:bg-gray-50 dark:hover:bg-white/10"
|
class="inline-flex items-center gap-1.5 rounded-r-lg px-2 py-1.5 transition hover:bg-gray-50 dark:hover:bg-white/10"
|
||||||
>
|
>
|
||||||
<span class="{{ $workspace && $hasActiveTenant ? 'font-medium text-primary-600 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400' }}">
|
<span class="{{ $workspace && $hasActiveTenant ? 'font-medium text-primary-600 dark:text-primary-400' : 'text-gray-500 dark:text-gray-400' }}">
|
||||||
@ -125,7 +125,7 @@ class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 transi
|
|||||||
<div class="space-y-2">
|
<div class="space-y-2">
|
||||||
<div class="flex items-center justify-between">
|
<div class="flex items-center justify-between">
|
||||||
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
<div class="text-xs font-semibold uppercase tracking-wider text-gray-400 dark:text-gray-500">
|
||||||
{{ __('localization.shell.selected_tenant') }}
|
{{ __('localization.shell.selected_environment') }}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -138,7 +138,7 @@ class="flex items-center gap-2 rounded-lg px-3 py-2 text-sm text-gray-700 transi
|
|||||||
href="{{ ChooseTenant::getUrl(panel: 'admin') }}"
|
href="{{ ChooseTenant::getUrl(panel: 'admin') }}"
|
||||||
class="ml-auto text-xs font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
|
class="ml-auto text-xs font-medium text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300"
|
||||||
>
|
>
|
||||||
{{ __('localization.shell.switch_tenant') }}
|
{{ __('localization.shell.switch_environment') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -147,7 +147,7 @@ class="ml-auto text-xs font-medium text-primary-600 hover:text-primary-500 dark:
|
|||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<button type="submit" class="w-full rounded-lg px-3 py-1.5 text-left text-xs font-medium text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-200">
|
<button type="submit" class="w-full rounded-lg px-3 py-1.5 text-left text-xs font-medium text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-200">
|
||||||
{{ __('localization.shell.clear_tenant_scope') }}
|
{{ __('localization.shell.clear_environment_scope') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
@ -155,23 +155,23 @@ class="ml-auto text-xs font-medium text-primary-600 hover:text-primary-500 dark:
|
|||||||
@else
|
@else
|
||||||
@if ($tenants->isEmpty())
|
@if ($tenants->isEmpty())
|
||||||
<div class="space-y-2 rounded-lg border border-dashed border-gray-300 px-3 py-3 text-center text-xs text-gray-500 dark:border-gray-600 dark:text-gray-400">
|
<div class="space-y-2 rounded-lg border border-dashed border-gray-300 px-3 py-3 text-center text-xs text-gray-500 dark:border-gray-600 dark:text-gray-400">
|
||||||
<div>{{ __('localization.shell.no_active_tenants') }}</div>
|
<div>{{ __('localization.shell.no_active_environments') }}</div>
|
||||||
<a href="{{ $managedTenantsUrl }}" class="inline-flex items-center gap-1 text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
|
<a href="{{ $managedTenantsUrl }}" class="inline-flex items-center gap-1 text-primary-600 hover:text-primary-500 dark:text-primary-400 dark:hover:text-primary-300">
|
||||||
<x-filament::icon icon="heroicon-o-arrow-top-right-on-square" class="h-3.5 w-3.5" />
|
<x-filament::icon icon="heroicon-o-arrow-top-right-on-square" class="h-3.5 w-3.5" />
|
||||||
{{ __('localization.shell.view_managed_tenants') }}
|
{{ __('localization.shell.view_managed_environments') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
@else
|
@else
|
||||||
@if (! $hasActiveTenant)
|
@if (! $hasActiveTenant)
|
||||||
<div class="rounded-lg bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:bg-white/5 dark:text-gray-400">
|
<div class="rounded-lg bg-gray-50 px-3 py-2 text-xs text-gray-600 dark:bg-white/5 dark:text-gray-400">
|
||||||
{{ __('localization.shell.workspace_wide_available') }}
|
{{ __('localization.shell.workspace_wide_available_without_environment') }}
|
||||||
</div>
|
</div>
|
||||||
@endif
|
@endif
|
||||||
|
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
class="fi-input fi-text-input w-full"
|
class="fi-input fi-text-input w-full"
|
||||||
placeholder="{{ __('localization.shell.search_tenants') }}"
|
placeholder="{{ __('localization.shell.search_environments') }}"
|
||||||
x-model="query"
|
x-model="query"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@ -208,7 +208,7 @@ class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition {{
|
|||||||
@csrf
|
@csrf
|
||||||
|
|
||||||
<button type="submit" class="w-full rounded-lg px-3 py-1.5 text-left text-xs font-medium text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-200">
|
<button type="submit" class="w-full rounded-lg px-3 py-1.5 text-left text-xs font-medium text-gray-500 transition hover:bg-gray-50 hover:text-gray-700 dark:text-gray-400 dark:hover:bg-white/5 dark:hover:text-gray-200">
|
||||||
{{ __('localization.shell.clear_tenant_scope') }}
|
{{ __('localization.shell.clear_environment_scope') }}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
@endif
|
@endif
|
||||||
|
|||||||
@ -0,0 +1,95 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Resources\PolicyResource;
|
||||||
|
use App\Models\ManagedEnvironment;
|
||||||
|
use App\Models\Policy;
|
||||||
|
use App\Models\PolicyVersion;
|
||||||
|
use App\Models\ProviderConnection;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use App\Models\WorkspaceMembership;
|
||||||
|
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||||
|
|
||||||
|
uses(RefreshDatabase::class);
|
||||||
|
|
||||||
|
pest()->browser()->timeout(20_000);
|
||||||
|
|
||||||
|
it('smokes environment-first chooser and policy helper copy', function (): void {
|
||||||
|
$workspace = Workspace::factory()->create([
|
||||||
|
'name' => 'Spec 286 Workspace',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$environment = ManagedEnvironment::factory()->active()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'name' => 'Spec 286 Production',
|
||||||
|
'slug' => 'spec-286-production',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$secondaryEnvironment = ManagedEnvironment::factory()->active()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'name' => 'Spec 286 Secondary',
|
||||||
|
'slug' => 'spec-286-secondary',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
WorkspaceMembership::factory()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'user_id' => (int) $user->getKey(),
|
||||||
|
'role' => 'owner',
|
||||||
|
]);
|
||||||
|
|
||||||
|
foreach ([$environment, $secondaryEnvironment] as $memberEnvironment) {
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
(int) $memberEnvironment->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
ProviderConnection::factory()->platform()->consentGranted()->create([
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'is_default' => true,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$policy = Policy::factory()->create([
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'display_name' => 'Spec 286 Policy',
|
||||||
|
]);
|
||||||
|
|
||||||
|
PolicyVersion::factory()->create([
|
||||||
|
'managed_environment_id' => (int) $environment->getKey(),
|
||||||
|
'policy_id' => (int) $policy->getKey(),
|
||||||
|
'metadata' => [],
|
||||||
|
]);
|
||||||
|
|
||||||
|
$this->actingAs($user);
|
||||||
|
|
||||||
|
$landing = visit(route('admin.workspace.managed-tenants.index', ['workspace' => $workspace]))
|
||||||
|
->waitForText('Managed environments')
|
||||||
|
->assertSee('Choose environment')
|
||||||
|
->assertSee('Spec 286 Production')
|
||||||
|
->assertSee('Spec 286 Secondary')
|
||||||
|
->assertDontSee('Managed tenants')
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->assertNoConsoleLogs();
|
||||||
|
|
||||||
|
$landing
|
||||||
|
->click('[wire\:key="tenant-'.$environment->getKey().'"]')
|
||||||
|
->waitForText('Spec 286 Production')
|
||||||
|
->waitForText('Environment governance overview')
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->assertNoConsoleLogs();
|
||||||
|
|
||||||
|
visit(PolicyResource::getUrl('view', ['record' => $policy], tenant: $environment))
|
||||||
|
->waitForText('Capture snapshot')
|
||||||
|
->assertSee('Restore to environment')
|
||||||
|
->assertDontSee('Restore to Microsoft Intune')
|
||||||
|
->click('Capture snapshot')
|
||||||
|
->waitForText('Capture snapshot now')
|
||||||
|
->assertSee('current environment')
|
||||||
|
->assertSee('Source: Microsoft Intune')
|
||||||
|
->assertNoJavaScriptErrors()
|
||||||
|
->assertNoConsoleLogs();
|
||||||
|
});
|
||||||
@ -23,7 +23,8 @@
|
|||||||
$stats = BaselineCompareStats::forTenant($tenant);
|
$stats = BaselineCompareStats::forTenant($tenant);
|
||||||
|
|
||||||
expect($stats->state)->toBe('no_assignment')
|
expect($stats->state)->toBe('no_assignment')
|
||||||
->and($stats->profileName)->toBeNull();
|
->and($stats->profileName)->toBeNull()
|
||||||
|
->and($stats->message)->toBe('This environment has no baseline assignment. A workspace manager can assign a baseline profile to this environment.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('returns no_snapshot state when profile has no consumable snapshot', function (): void {
|
it('returns no_snapshot state when profile has no consumable snapshot', function (): void {
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
$this
|
$this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin')
|
->get(route('admin.workspace.home', ['workspace' => $workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Workspace overview')
|
->assertSee('Workspace overview')
|
||||||
->assertSee('No accessible tenants in this workspace');
|
->assertSee('No accessible tenants in this workspace');
|
||||||
@ -62,10 +62,10 @@
|
|||||||
$this
|
$this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin')
|
->get(route('admin.workspace.home', ['workspace' => $workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Workspace overview')
|
->assertSee('Workspace overview')
|
||||||
->assertSee('Choose tenant');
|
->assertSee(__('localization.shell.choose_environment'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the workspace overview when a workspace is selected and has exactly one tenant', function (): void {
|
it('renders the workspace overview when a workspace is selected and has exactly one tenant', function (): void {
|
||||||
@ -96,8 +96,8 @@
|
|||||||
$this
|
$this
|
||||||
->actingAs($user)
|
->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin')
|
->get(route('admin.workspace.home', ['workspace' => $workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Workspace overview')
|
->assertSee('Workspace overview')
|
||||||
->assertSee('Choose tenant');
|
->assertSee(__('localization.shell.choose_environment'));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -22,6 +22,6 @@
|
|||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->assertSee('ManagedEnvironment archived')
|
->assertSee('ManagedEnvironment archived')
|
||||||
->assertSee(UiTooltips::TENANT_ARCHIVED)
|
->assertSee(UiTooltips::TENANT_ARCHIVED)
|
||||||
->assertSee('This tenant remains available for inspection and audit history, but it is not selectable as active context until you restore it.')
|
->assertSee('This environment remains available for inspection and audit history, but it is not selectable as active context until you restore it.')
|
||||||
->assertDontSee('deactivated');
|
->assertDontSee('deactivated');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -105,11 +105,12 @@
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
Livewire::test(BaselineCompareLanding::class)
|
Livewire::test(BaselineCompareLanding::class)
|
||||||
->assertSee('Intune RBAC Role Definitions')
|
->assertSee('RBAC role definitions')
|
||||||
->assertSee('Compared')
|
->assertSee('Compared')
|
||||||
->assertSee('Modified')
|
->assertSee('Modified')
|
||||||
->assertSee('Missing')
|
->assertSee('Missing')
|
||||||
->assertSee('Unexpected')
|
->assertSee('Unexpected')
|
||||||
->assertSee('Role Assignments are not included')
|
->assertSee('Role Assignments are not included')
|
||||||
|
->assertDontSee('Intune RBAC Role Definitions')
|
||||||
->assertDontSee('RBAC restore');
|
->assertDontSee('RBAC restore');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -33,8 +33,8 @@
|
|||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin/choose-tenant')
|
->get('/admin/choose-tenant')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('No active tenants available')
|
->assertSee(__('localization.shell.no_active_environments'))
|
||||||
->assertSee('View managed tenants')
|
->assertSee(__('localization.shell.view_managed_environments'))
|
||||||
->assertDontSee('Register tenant')
|
->assertDontSee('Register tenant')
|
||||||
->assertDontSee('Add tenant');
|
->assertDontSee('Add tenant');
|
||||||
});
|
});
|
||||||
@ -53,7 +53,7 @@
|
|||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin/choose-tenant')
|
->get('/admin/choose-tenant')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('No active tenants available')
|
->assertSee(__('localization.shell.no_active_environments'))
|
||||||
->assertSee('Switch workspace')
|
->assertSee('Switch workspace')
|
||||||
->assertDontSee('Register tenant');
|
->assertDontSee('Register tenant');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -308,7 +308,7 @@ function getPlacementEmptyStateAction(Testable $component, string $name): ?Actio
|
|||||||
|
|
||||||
$emptyStateCreate = getPlacementEmptyStateAction($component, 'add_tenant');
|
$emptyStateCreate = getPlacementEmptyStateAction($component, 'add_tenant');
|
||||||
expect($emptyStateCreate)->not->toBeNull();
|
expect($emptyStateCreate)->not->toBeNull();
|
||||||
expect($emptyStateCreate?->getLabel())->toBe('Add tenant');
|
expect($emptyStateCreate?->getLabel())->toBe('Add environment');
|
||||||
|
|
||||||
$headerCreate = getHeaderAction($component, 'add_tenant');
|
$headerCreate = getHeaderAction($component, 'add_tenant');
|
||||||
expect($headerCreate)->not->toBeNull();
|
expect($headerCreate)->not->toBeNull();
|
||||||
@ -326,7 +326,7 @@ function getPlacementEmptyStateAction(Testable $component, string $name): ?Actio
|
|||||||
$headerCreate = getHeaderAction($component, 'add_tenant');
|
$headerCreate = getHeaderAction($component, 'add_tenant');
|
||||||
expect($headerCreate)->not->toBeNull();
|
expect($headerCreate)->not->toBeNull();
|
||||||
expect($headerCreate?->isVisible())->toBeTrue();
|
expect($headerCreate?->isVisible())->toBeTrue();
|
||||||
expect($headerCreate?->getLabel())->toBe('Add tenant');
|
expect($headerCreate?->getLabel())->toBe('Add environment');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('labels the empty-state tenant action as resume onboarding when one draft exists', function (): void {
|
it('labels the empty-state tenant action as resume onboarding when one draft exists', function (): void {
|
||||||
|
|||||||
@ -0,0 +1,35 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Filament\Pages\TenantDashboard;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
|
||||||
|
it('renders environment-first context actions on the dashboard', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
Filament::setTenant($environment, true);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
||||||
|
->get(TenantDashboard::getUrl(tenant: $environment))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('Switch environment')
|
||||||
|
->assertSee('Clear environment scope')
|
||||||
|
->assertDontSee('Switch tenant')
|
||||||
|
->assertDontSee('Clear tenant scope');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders all-environments shell wording on tenantless monitoring pages', function (): void {
|
||||||
|
[$user, $environment] = createUserWithTenant(role: 'owner');
|
||||||
|
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
||||||
|
->get(route('admin.operations.index', ['workspace' => $environment->workspace]))
|
||||||
|
->assertOk()
|
||||||
|
->assertSee('All environments')
|
||||||
|
->assertDontSee('All tenants');
|
||||||
|
});
|
||||||
@ -44,10 +44,16 @@ function getPolicyInventoryEmptyStateAction(Testable $component, string $name):
|
|||||||
->assertSee(__('localization.policy.resource.empty_state_heading'))
|
->assertSee(__('localization.policy.resource.empty_state_heading'))
|
||||||
->assertSee(__('localization.policy.resource.empty_state_description'));
|
->assertSee(__('localization.policy.resource.empty_state_description'));
|
||||||
|
|
||||||
|
expect(__('localization.policy.resource.empty_state_description'))
|
||||||
|
->toContain('dieser Umgebung')
|
||||||
|
->not->toContain('dieses Tenants');
|
||||||
|
|
||||||
$action = getPolicyInventoryEmptyStateAction($component, 'sync');
|
$action = getPolicyInventoryEmptyStateAction($component, 'sync');
|
||||||
|
|
||||||
expect($action)->not->toBeNull()
|
expect($action)->not->toBeNull()
|
||||||
->and($action?->getLabel())->toBe(__('localization.policy.resource.sync_action_primary'));
|
->and($action?->getLabel())->toBe(__('localization.policy.resource.sync_action_primary'))
|
||||||
|
->and((string) $action?->getModalDescription())->toContain('aktuellen Umgebung')
|
||||||
|
->and((string) $action?->getModalDescription())->not->toContain('aktuellen Tenant');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders source-unavailable policy labels from the active German locale', function (): void {
|
it('renders source-unavailable policy labels from the active German locale', function (): void {
|
||||||
@ -90,7 +96,7 @@ function getPolicyInventoryEmptyStateAction(Testable $component, string $name):
|
|||||||
->assertSee(__('localization.policy.versions.open_backup_sets'));
|
->assertSee(__('localization.policy.versions.open_backup_sets'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders the restore-to-Microsoft-Intune action from the active German locale', function (): void {
|
it('renders the restore-to-environment action from the active German locale', function (): void {
|
||||||
App::setLocale('de');
|
App::setLocale('de');
|
||||||
|
|
||||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||||
@ -112,7 +118,8 @@ function getPolicyInventoryEmptyStateAction(Testable $component, string $name):
|
|||||||
Livewire::test(VersionsRelationManager::class, [
|
Livewire::test(VersionsRelationManager::class, [
|
||||||
'ownerRecord' => $policy,
|
'ownerRecord' => $policy,
|
||||||
'pageClass' => ViewPolicy::class,
|
'pageClass' => ViewPolicy::class,
|
||||||
])->assertSee(__('localization.policy.relation.restore_to_microsoft_intune'));
|
])->assertSee('In die Umgebung wiederherstellen')
|
||||||
|
->assertDontSee('In Microsoft Intune wiederherstellen');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders policy version quality and related labels from the active German locale', function (): void {
|
it('renders policy version quality and related labels from the active German locale', function (): void {
|
||||||
@ -190,7 +197,8 @@ function getPolicyInventoryEmptyStateAction(Testable $component, string $name):
|
|||||||
return $action->getLabel() === __('localization.policy.resource.capture_snapshot_action')
|
return $action->getLabel() === __('localization.policy.resource.capture_snapshot_action')
|
||||||
&& $action->isConfirmationRequired()
|
&& $action->isConfirmationRequired()
|
||||||
&& (string) $action->getModalHeading() === __('localization.policy.resource.capture_snapshot_modal_heading')
|
&& (string) $action->getModalHeading() === __('localization.policy.resource.capture_snapshot_modal_heading')
|
||||||
&& str_contains((string) $action->getModalDescription(), __('localization.policy.resource.capture_snapshot_modal_subheading'))
|
&& str_contains((string) $action->getModalDescription(), 'aktuelle Umgebung')
|
||||||
&& str_contains((string) $action->getModalDescription(), __('localization.policy.common.source_microsoft_intune'));
|
&& ! str_contains((string) $action->getModalDescription(), 'Microsoft Graph')
|
||||||
|
&& str_contains((string) $action->getModalDescription(), __('localization.policy.common.source_microsoft_intune'));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
@ -64,7 +64,7 @@
|
|||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->assertSee('Lifecycle summary')
|
->assertSee('Lifecycle summary')
|
||||||
->assertSee('This tenant is active and available across normal management, tenant selection, and operational follow-up flows.')
|
->assertSee('This environment is active and available across normal management, environment selection, and operational follow-up flows.')
|
||||||
->assertSee('RBAC status')
|
->assertSee('RBAC status')
|
||||||
->assertDontSee('App status')
|
->assertDontSee('App status')
|
||||||
->assertSee('Provider connection');
|
->assertSee('Provider connection');
|
||||||
@ -81,5 +81,5 @@
|
|||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->assertSee('ManagedEnvironment archived')
|
->assertSee('ManagedEnvironment archived')
|
||||||
->assertSee('This tenant remains available for inspection and audit history, but it is not selectable as active context until you restore it.');
|
->assertSee('This environment remains available for inspection and audit history, but it is not selectable as active context until you restore it.');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->assertSee('Lifecycle summary')
|
->assertSee('Lifecycle summary')
|
||||||
->assertSee('This tenant is still onboarding. It remains visible on management and review surfaces, but it is not selectable as active context until onboarding completes.')
|
->assertSee('This environment is still onboarding. It remains visible on management and review surfaces, but it is not selectable as active context until onboarding completes.')
|
||||||
->assertDontSee('App status')
|
->assertDontSee('App status')
|
||||||
->assertDontSee('Consent required')
|
->assertDontSee('Consent required')
|
||||||
->assertSee('RBAC status')
|
->assertSee('RBAC status')
|
||||||
|
|||||||
@ -108,7 +108,7 @@
|
|||||||
|
|
||||||
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()])
|
||||||
->assertSee('Lifecycle summary')
|
->assertSee('Lifecycle summary')
|
||||||
->assertSee('This tenant is still onboarding. It remains visible on management and review surfaces, but it is not selectable as active context until onboarding completes.')
|
->assertSee('This environment is still onboarding. It remains visible on management and review surfaces, but it is not selectable as active context until onboarding completes.')
|
||||||
->assertSee('RBAC status')
|
->assertSee('RBAC status')
|
||||||
->assertSee('Failed')
|
->assertSee('Failed')
|
||||||
->assertDontSee('App status')
|
->assertDontSee('App status')
|
||||||
|
|||||||
@ -33,9 +33,9 @@
|
|||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $validTenant->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $validTenant->workspace_id,
|
||||||
])
|
])
|
||||||
->get(route('admin.operations.index', ['tenant' => $foreignTenant->external_id]))
|
->get(route('admin.operations.index', ['workspace' => $validTenant->workspace, 'tenant' => $foreignTenant->external_id]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Context unavailable')
|
->assertSee('Context unavailable')
|
||||||
->assertSee('No tenant selected')
|
->assertSee(__('localization.shell.no_environment_selected'))
|
||||||
->assertDontSee('ManagedEnvironment scope: '.$foreignTenant->name);
|
->assertDontSee(__('localization.shell.environment_scope').': '.$foreignTenant->name);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -28,7 +28,7 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get('/admin')
|
->get(route('admin.workspace.home', ['workspace' => $tenant->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Workspace overview')
|
->assertSee('Workspace overview')
|
||||||
->assertSee('Accessible tenants')
|
->assertSee('Accessible tenants')
|
||||||
@ -38,7 +38,7 @@
|
|||||||
->assertSee('Active operations')
|
->assertSee('Active operations')
|
||||||
->assertSee('Needs attention')
|
->assertSee('Needs attention')
|
||||||
->assertSee('Recent operations')
|
->assertSee('Recent operations')
|
||||||
->assertSee('Choose tenant')
|
->assertSee(__('localization.shell.choose_environment'))
|
||||||
->assertSee('Open operations')
|
->assertSee('Open operations')
|
||||||
->assertSee('Open alerts')
|
->assertSee('Open alerts')
|
||||||
->assertSee('Review current and recent workspace-wide operations.')
|
->assertSee('Review current and recent workspace-wide operations.')
|
||||||
|
|||||||
@ -26,13 +26,13 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin')
|
->get(route('admin.workspace.home', ['workspace' => $workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('No accessible tenants in this workspace')
|
->assertSee('No accessible tenants in this workspace')
|
||||||
->assertSee('This workspace is not calm or healthy yet because your current scope has no visible tenants.')
|
->assertSee('This workspace is not calm or healthy yet because your current scope has no visible tenants.')
|
||||||
->assertSee('No recent operations yet')
|
->assertSee('No recent operations yet')
|
||||||
->assertSee('Switch workspace')
|
->assertSee('Switch workspace')
|
||||||
->assertDontSee('Choose tenant');
|
->assertDontSee(__('localization.shell.choose_environment'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does not render a calm state when governance risk exists even if operations are quiet', function (): void {
|
it('does not render a calm state when governance risk exists even if operations are quiet', function (): void {
|
||||||
@ -49,7 +49,7 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get('/admin')
|
->get(route('admin.workspace.home', ['workspace' => $tenant->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Overdue findings')
|
->assertSee('Overdue findings')
|
||||||
->assertDontSee('Nothing urgent in your visible workspace slice')
|
->assertDontSee('Nothing urgent in your visible workspace slice')
|
||||||
@ -69,7 +69,7 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get('/admin')
|
->get(route('admin.workspace.home', ['workspace' => $tenant->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Nothing urgent in your visible workspace slice')
|
->assertSee('Nothing urgent in your visible workspace slice')
|
||||||
->assertSee('Visible governance, backup health, recovery evidence, compare posture, and activity currently look calm.')
|
->assertSee('Visible governance, backup health, recovery evidence, compare posture, and activity currently look calm.')
|
||||||
|
|||||||
@ -20,11 +20,11 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
->get('/admin')
|
->get(route('admin.workspace.home', ['workspace' => $workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Workspace overview')
|
->assertSee('Workspace overview')
|
||||||
->assertSee('Contoso Workspace')
|
->assertSee('Contoso Workspace')
|
||||||
->assertSee('Choose tenant');
|
->assertSee(__('localization.shell.choose_environment'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('sends direct /admin visits without workspace context through the chooser even for a single membership', function (): void {
|
it('sends direct /admin visits without workspace context through the chooser even for a single membership', function (): void {
|
||||||
|
|||||||
@ -0,0 +1,85 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use Illuminate\Support\Facades\App;
|
||||||
|
|
||||||
|
it('keeps in-scope surfaces free of retired tenant-first and provider-first copy', function (): void {
|
||||||
|
$forbiddenCopyByFile = [
|
||||||
|
'apps/platform/app/Filament/Pages/ChooseTenant.php' => [
|
||||||
|
'Choose tenant',
|
||||||
|
],
|
||||||
|
'apps/platform/resources/views/filament/pages/choose-tenant.blade.php' => [
|
||||||
|
'Choose tenant',
|
||||||
|
'Select the tenant for your normal active operating context.',
|
||||||
|
'No tenant selected is still a valid workspace state',
|
||||||
|
'No active tenants available',
|
||||||
|
'View managed tenants',
|
||||||
|
'Add tenant',
|
||||||
|
],
|
||||||
|
'apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php' => [
|
||||||
|
'Managed tenants',
|
||||||
|
],
|
||||||
|
'apps/platform/resources/views/filament/pages/workspaces/managed-tenants-landing.blade.php' => [
|
||||||
|
'No managed tenants yet',
|
||||||
|
'Choose tenant',
|
||||||
|
'Add tenant',
|
||||||
|
'Connect your first Microsoft Entra tenant',
|
||||||
|
],
|
||||||
|
'apps/platform/resources/views/filament/partials/context-bar.blade.php' => [
|
||||||
|
'Tenant scope',
|
||||||
|
'Select tenant',
|
||||||
|
'Selected tenant',
|
||||||
|
'Switch tenant',
|
||||||
|
'Clear tenant scope',
|
||||||
|
'No active tenants',
|
||||||
|
'View managed tenants',
|
||||||
|
'Search tenants',
|
||||||
|
],
|
||||||
|
'apps/platform/app/Support/OperateHub/OperateHubShell.php' => [
|
||||||
|
'ManagedEnvironment scope',
|
||||||
|
'All tenants',
|
||||||
|
],
|
||||||
|
'apps/platform/app/Filament/Pages/Monitoring/Operations.php' => [
|
||||||
|
'Show all tenants',
|
||||||
|
'one tenant inside the active workspace',
|
||||||
|
'all entitled tenants',
|
||||||
|
'tenant-specific context',
|
||||||
|
],
|
||||||
|
'apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php' => [
|
||||||
|
'tenant landing',
|
||||||
|
],
|
||||||
|
'apps/platform/lang/en/localization.php' => [
|
||||||
|
'build this tenant\'s policy inventory',
|
||||||
|
],
|
||||||
|
'apps/platform/app/Support/Baselines/BaselineCompareSummaryAssessor.php' => [
|
||||||
|
'This tenant does not have an assigned baseline yet.',
|
||||||
|
],
|
||||||
|
'apps/platform/app/Support/Baselines/BaselineCompareStats.php' => [
|
||||||
|
'This tenant has no baseline assignment. A workspace manager can assign a baseline profile to this tenant.',
|
||||||
|
],
|
||||||
|
'apps/platform/app/Services/Tenants/TenantActionPolicySurface.php' => [
|
||||||
|
'Add tenant',
|
||||||
|
],
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($forbiddenCopyByFile as $relativePath => $forbiddenStrings) {
|
||||||
|
$absolutePath = repo_path($relativePath);
|
||||||
|
|
||||||
|
expect(is_file($absolutePath))->toBeTrue("Expected file [{$relativePath}] to exist for the copy guard.");
|
||||||
|
|
||||||
|
$contents = (string) file_get_contents($absolutePath);
|
||||||
|
|
||||||
|
foreach ($forbiddenStrings as $forbiddenString) {
|
||||||
|
expect($contents)
|
||||||
|
->not->toContain($forbiddenString, "Found retired copy [{$forbiddenString}] in [{$relativePath}].");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
it('keeps auth-provider wording explicitly Microsoft-owned', function (): void {
|
||||||
|
App::setLocale('en');
|
||||||
|
|
||||||
|
expect(__('localization.auth.sign_in_microsoft'))->toBe('Sign in with Microsoft')
|
||||||
|
->and(__('localization.policy.common.source_microsoft_intune'))->toBe('Source: Microsoft Intune');
|
||||||
|
});
|
||||||
@ -0,0 +1,63 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use App\Models\ManagedEnvironment;
|
||||||
|
use App\Models\User;
|
||||||
|
use App\Models\Workspace;
|
||||||
|
use App\Models\WorkspaceMembership;
|
||||||
|
use App\Support\Workspaces\WorkspaceContext;
|
||||||
|
use Filament\Facades\Filament;
|
||||||
|
|
||||||
|
it('renders environment-first chooser terminology in english', function (): void {
|
||||||
|
$environment = ManagedEnvironment::factory()->active()->create(['name' => 'Chooser Environment']);
|
||||||
|
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner');
|
||||||
|
|
||||||
|
app()->setLocale('en');
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
||||||
|
->get('/admin/choose-tenant')
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Choose environment')
|
||||||
|
->assertSee('Select the environment for your normal active operating context.')
|
||||||
|
->assertSee('No environment selected is still a valid workspace state')
|
||||||
|
->assertDontSee('Choose tenant')
|
||||||
|
->assertDontSee('Select the tenant for your normal active operating context.')
|
||||||
|
->assertDontSee('No tenant selected is still a valid workspace state');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('renders environment-first managed-environments landing terminology in english', function (): void {
|
||||||
|
$workspace = Workspace::factory()->create(['slug' => 'landing-environment-copy']);
|
||||||
|
$user = User::factory()->create();
|
||||||
|
|
||||||
|
WorkspaceMembership::factory()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'user_id' => (int) $user->getKey(),
|
||||||
|
'role' => 'owner',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$environment = ManagedEnvironment::factory()->active()->create([
|
||||||
|
'workspace_id' => (int) $workspace->getKey(),
|
||||||
|
'name' => 'Landing Environment',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$user->tenants()->syncWithoutDetaching([
|
||||||
|
$environment->getKey() => ['role' => 'owner'],
|
||||||
|
]);
|
||||||
|
|
||||||
|
app()->setLocale('en');
|
||||||
|
Filament::setTenant(null, true);
|
||||||
|
|
||||||
|
$this->actingAs($user)
|
||||||
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()])
|
||||||
|
->get(route('admin.workspace.managed-tenants.index', ['workspace' => $workspace]))
|
||||||
|
->assertSuccessful()
|
||||||
|
->assertSee('Managed environments')
|
||||||
|
->assertSee('Choose environment')
|
||||||
|
->assertSee('Add environment')
|
||||||
|
->assertDontSee('Managed tenants')
|
||||||
|
->assertDontSee('Choose tenant')
|
||||||
|
->assertDontSee('Add tenant');
|
||||||
|
});
|
||||||
@ -23,13 +23,13 @@
|
|||||||
(string) $tenant->workspace_id => (int) $tenant->getKey(),
|
(string) $tenant->workspace_id => (int) $tenant->getKey(),
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $tenant->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee($workspaceName ?? 'Select workspace')
|
->assertSee($workspaceName ?? 'Select workspace')
|
||||||
->assertSee(__('localization.shell.search_tenants'))
|
->assertSee(__('localization.shell.search_environments'))
|
||||||
->assertSee('Switch workspace')
|
->assertSee('Switch workspace')
|
||||||
->assertSee('admin/select-tenant')
|
->assertSee('admin/select-tenant')
|
||||||
->assertSee('Clear tenant scope')
|
->assertSee(__('localization.shell.clear_environment_scope'))
|
||||||
->assertSee($tenant->getFilamentName());
|
->assertSee($tenant->getFilamentName());
|
||||||
|
|
||||||
$content = $response->getContent();
|
$content = $response->getContent();
|
||||||
@ -66,7 +66,7 @@
|
|||||||
->get('/admin/workspaces')
|
->get('/admin/workspaces')
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Choose a workspace first.')
|
->assertSee('Choose a workspace first.')
|
||||||
->assertDontSee(__('localization.shell.search_tenants'));
|
->assertDontSee(__('localization.shell.search_environments'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps tenant-scoped pages selector read-only while exposing the clear tenant scope action', function (): void {
|
it('keeps tenant-scoped pages selector read-only while exposing the clear tenant scope action', function (): void {
|
||||||
@ -80,7 +80,7 @@
|
|||||||
->get(route('filament.admin.resources.tenants.index', filamentTenantRouteParams($tenant)))
|
->get(route('filament.admin.resources.tenants.index', filamentTenantRouteParams($tenant)))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee($tenant->getFilamentName())
|
->assertSee($tenant->getFilamentName())
|
||||||
->assertSee('Clear tenant scope');
|
->assertSee(__('localization.shell.clear_environment_scope'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('renders routed tenant resource view pages with a clear tenant scope action but no inline selector list', function (): void {
|
it('renders routed tenant resource view pages with a clear tenant scope action but no inline selector list', function (): void {
|
||||||
@ -111,9 +111,7 @@
|
|||||||
])
|
])
|
||||||
->get(route('filament.admin.resources.tenants.view', ['record' => $routedTenant]))
|
->get(route('filament.admin.resources.tenants.view', ['record' => $routedTenant]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee($routedTenant->getFilamentName())
|
->assertSee(__('localization.shell.clear_environment_scope'));
|
||||||
->assertSee('Switch tenant')
|
|
||||||
->assertSee('Clear tenant scope');
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('filters the header tenant picker to tenants the user can access', function (): void {
|
it('filters the header tenant picker to tenants the user can access', function (): void {
|
||||||
@ -132,7 +130,7 @@
|
|||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
||||||
])
|
])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $tenantA->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee($tenantA->getFilamentName())
|
->assertSee($tenantA->getFilamentName())
|
||||||
->assertDontSee($tenantB->getFilamentName());
|
->assertDontSee($tenantB->getFilamentName());
|
||||||
@ -154,7 +152,7 @@
|
|||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
||||||
])
|
])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $tenantA->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee($tenantA->getFilamentName())
|
->assertSee($tenantA->getFilamentName())
|
||||||
->assertDontSee($tenantB->getFilamentName());
|
->assertDontSee($tenantB->getFilamentName());
|
||||||
@ -181,7 +179,7 @@
|
|||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
||||||
])
|
])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $tenantA->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee($tenantA->getFilamentName())
|
->assertSee($tenantA->getFilamentName())
|
||||||
->assertDontSee($onboardingTenant->getFilamentName());
|
->assertDontSee($onboardingTenant->getFilamentName());
|
||||||
@ -220,7 +218,7 @@
|
|||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
||||||
])
|
])
|
||||||
->get(route('admin.operations.view', ['run' => (int) $runA->getKey()]))
|
->get(route('admin.operations.view', ['workspace' => $tenantA->workspace, 'run' => (int) $runA->getKey()]))
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
expect(Filament::getTenant())->toBeNull();
|
expect(Filament::getTenant())->toBeNull();
|
||||||
@ -229,7 +227,7 @@
|
|||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
||||||
])
|
])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $tenantA->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Policy sync')
|
->assertSee('Policy sync')
|
||||||
->assertSee('Inventory sync')
|
->assertSee('Inventory sync')
|
||||||
@ -262,11 +260,11 @@
|
|||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $runTenant->workspace_id,
|
||||||
])
|
])
|
||||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
->get(route('admin.operations.view', ['workspace' => $runTenant->workspace, 'run' => (int) $run->getKey()]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('ManagedEnvironment scope: Current ManagedEnvironment')
|
->assertSee(__('localization.shell.environment_scope').': Current ManagedEnvironment')
|
||||||
->assertSee('Current tenant context differs from this operation')
|
->assertSee('Current environment context differs from this operation')
|
||||||
->assertSee('Operation tenant: Run ManagedEnvironment.');
|
->assertSee('Operation environment: Run ManagedEnvironment.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows canonical workspace framing on canonical run pages with no selected tenant context', function (): void {
|
it('shows canonical workspace framing on canonical run pages with no selected tenant context', function (): void {
|
||||||
@ -287,9 +285,9 @@
|
|||||||
->withSession([
|
->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||||
])
|
])
|
||||||
->get(route('admin.operations.view', ['run' => (int) $run->getKey()]))
|
->get(route('admin.operations.view', ['workspace' => $tenant->workspace, 'run' => (int) $run->getKey()]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('All tenants')
|
->assertSee(__('localization.shell.all_environments'))
|
||||||
->assertSee('Canonical workspace view')
|
->assertSee('Canonical workspace view')
|
||||||
->assertSee('No tenant context is currently selected.');
|
->assertSee('No environment context is currently selected.');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -26,14 +26,14 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get(route('admin.operations.index'))
|
->get(route('admin.operations.index', ['workspace' => $tenant->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Monitoring landing')
|
->assertSee('Monitoring landing')
|
||||||
->assertSee('Tabs, filters, and row inspection define the active work lane.')
|
->assertSee('Tabs, filters, and row inspection define the active work lane.')
|
||||||
->assertSee('Scope context')
|
->assertSee('Scope context')
|
||||||
->assertSee('Scope reset')
|
->assertSee('Scope reset')
|
||||||
->assertSee('Inspect flow')
|
->assertSee('Inspect flow')
|
||||||
->assertSee('Show all tenants');
|
->assertSee(__('localization.shell.show_all_environments'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('surfaces canonical return context separately from the operations work lane', function (): void {
|
it('surfaces canonical return context separately from the operations work lane', function (): void {
|
||||||
|
|||||||
@ -38,7 +38,7 @@
|
|||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Active Card ManagedEnvironment')
|
->assertSee('Active Card ManagedEnvironment')
|
||||||
->assertSee('Active')
|
->assertSee('Active')
|
||||||
->assertSee('Active tenant available for normal operations.');
|
->assertSee('Active environment available for normal operations.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('labels the onboarding linked tenant action with the canonical lifecycle name', function (): void {
|
it('labels the onboarding linked tenant action with the canonical lifecycle name', function (): void {
|
||||||
|
|||||||
@ -340,7 +340,7 @@
|
|||||||
->get(OperationRunLinks::tenantlessView((int) $run->getKey()))
|
->get(OperationRunLinks::tenantlessView((int) $run->getKey()))
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Workspace-level operation')
|
->assertSee('Workspace-level operation')
|
||||||
->assertSee('This canonical workspace view is not tied to the current tenant context');
|
->assertSee('This canonical workspace view is not tied to the current environment context');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps a canonical run viewer accessible when remembered tenant context is cleared as stale', function (): void {
|
it('keeps a canonical run viewer accessible when remembered tenant context is cleared as stale', function (): void {
|
||||||
@ -381,9 +381,9 @@
|
|||||||
])
|
])
|
||||||
->get(OperationRunLinks::tenantlessView((int) $run->getKey()))
|
->get(OperationRunLinks::tenantlessView((int) $run->getKey()))
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('All tenants')
|
->assertSee(__('localization.shell.all_environments'))
|
||||||
->assertSee('Canonical workspace view')
|
->assertSee('Canonical workspace view')
|
||||||
->assertSee('No tenant context is currently selected.');
|
->assertSee('No environment context is currently selected.');
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps a canonical run viewer accessible when the run tenant is selector-ineligible but the remembered context is valid', function (): void {
|
it('keeps a canonical run viewer accessible when the run tenant is selector-ineligible but the remembered context is valid', function (): void {
|
||||||
@ -424,8 +424,8 @@
|
|||||||
])
|
])
|
||||||
->get(OperationRunLinks::tenantlessView((int) $run->getKey()))
|
->get(OperationRunLinks::tenantlessView((int) $run->getKey()))
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Current tenant context differs from this operation')
|
->assertSee('Current environment context differs from this operation')
|
||||||
->assertSee('Operation tenant: '.$runTenant->name.'.')
|
->assertSee('Operation environment: '.$runTenant->name.'.')
|
||||||
->assertSee('Back to Operations');
|
->assertSee('Back to Operations');
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@ -58,7 +58,7 @@ function crossTenantCompareLaunchQuery(string $url): array
|
|||||||
])
|
])
|
||||||
->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry')
|
->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry')
|
||||||
->and(data_get($query, 'nav.managed_environment_id'))->toBe((string) $targetTenant->getKey())
|
->and(data_get($query, 'nav.managed_environment_id'))->toBe((string) $targetTenant->getKey())
|
||||||
->and(data_get($query, 'nav.back_label'))->toBe('Back to tenant registry')
|
->and(data_get($query, 'nav.back_label'))->toBe(__('localization.shell.back_to_environment_registry'))
|
||||||
->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE)
|
->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE)
|
||||||
->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED)
|
->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED)
|
||||||
->and($backUrl)->toContain('triage_sort='.TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST);
|
->and($backUrl)->toContain('triage_sort='.TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST);
|
||||||
@ -69,7 +69,7 @@ function crossTenantCompareLaunchQuery(string $url): array
|
|||||||
->assertSet('sourceTenantId', null)
|
->assertSet('sourceTenantId', null)
|
||||||
->assertSet('targetTenantId', (string) $targetTenant->getKey())
|
->assertSet('targetTenantId', (string) $targetTenant->getKey())
|
||||||
->assertActionVisible('return_to_origin')
|
->assertActionVisible('return_to_origin')
|
||||||
->assertActionExists('return_to_origin', fn (Action $action): bool => $action->getLabel() === 'Back to tenant registry'
|
->assertActionExists('return_to_origin', fn (Action $action): bool => $action->getLabel() === __('localization.shell.back_to_environment_registry')
|
||||||
&& $action->getUrl() === TenantResource::getUrl(panel: 'admin', parameters: $triageState));
|
&& $action->getUrl() === TenantResource::getUrl(panel: 'admin', parameters: $triageState));
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -110,7 +110,7 @@ function crossTenantCompareLaunchQuery(string $url): array
|
|||||||
'target_tenant_id' => (string) $targetTenant->getKey(),
|
'target_tenant_id' => (string) $targetTenant->getKey(),
|
||||||
])
|
])
|
||||||
->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry')
|
->and(data_get($query, 'nav.source_surface'))->toBe('tenant_registry')
|
||||||
->and(data_get($query, 'nav.back_label'))->toBe('Back to tenant registry')
|
->and(data_get($query, 'nav.back_label'))->toBe(__('localization.shell.back_to_environment_registry'))
|
||||||
->and(data_get($query, 'nav.managed_environment_id'))->toBeNull()
|
->and(data_get($query, 'nav.managed_environment_id'))->toBeNull()
|
||||||
->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE)
|
->and($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE)
|
||||||
->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED)
|
->and($backUrl)->toContain('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED)
|
||||||
@ -170,7 +170,7 @@ function crossTenantCompareLaunchQuery(string $url): array
|
|||||||
->assertSet('targetTenantId', (string) $targetTenant->getKey())
|
->assertSet('targetTenantId', (string) $targetTenant->getKey())
|
||||||
->assertSet('selectedPolicyTypes', ['deviceConfiguration'])
|
->assertSet('selectedPolicyTypes', ['deviceConfiguration'])
|
||||||
->assertActionVisible('return_to_origin')
|
->assertActionVisible('return_to_origin')
|
||||||
->assertActionExists('return_to_origin', fn (Action $action): bool => $action->getLabel() === 'Back to tenant registry'
|
->assertActionExists('return_to_origin', fn (Action $action): bool => $action->getLabel() === __('localization.shell.back_to_environment_registry')
|
||||||
&& $action->getUrl() === $expectedBackUrl);
|
&& $action->getUrl() === $expectedBackUrl);
|
||||||
|
|
||||||
$page = $component->instance();
|
$page = $component->instance();
|
||||||
@ -190,7 +190,7 @@ function crossTenantCompareLaunchQuery(string $url): array
|
|||||||
->and($page->targetTenantId)->toBe((string) $targetTenant->getKey())
|
->and($page->targetTenantId)->toBe((string) $targetTenant->getKey())
|
||||||
->and($page->selectedPolicyTypes)->toBe(['deviceConfiguration'])
|
->and($page->selectedPolicyTypes)->toBe(['deviceConfiguration'])
|
||||||
->and($page->navigationContextPayload)->toBe($query['nav'])
|
->and($page->navigationContextPayload)->toBe($query['nav'])
|
||||||
->and($navigationContext?->backLinkLabel)->toBe('Back to tenant registry')
|
->and($navigationContext?->backLinkLabel)->toBe(__('localization.shell.back_to_environment_registry'))
|
||||||
->and($navigationContext?->backLinkUrl)->toBe($expectedBackUrl);
|
->and($navigationContext?->backLinkUrl)->toBe($expectedBackUrl);
|
||||||
|
|
||||||
Queue::assertPushed(CrossTenantPromotionExecutionJob::class, function (CrossTenantPromotionExecutionJob $job) use ($run): bool {
|
Queue::assertPushed(CrossTenantPromotionExecutionJob::class, function (CrossTenantPromotionExecutionJob $job) use ($run): bool {
|
||||||
|
|||||||
@ -123,8 +123,8 @@
|
|||||||
$response->assertOk();
|
$response->assertOk();
|
||||||
|
|
||||||
// Environment-scoped affordances must still be present on tenant pages.
|
// Environment-scoped affordances must still be present on tenant pages.
|
||||||
$response->assertSee('Switch tenant', false)
|
$response->assertSee(__('localization.shell.switch_environment'), false)
|
||||||
->assertSee('Clear tenant scope', false);
|
->assertSee(__('localization.shell.clear_environment_scope'), false);
|
||||||
});
|
});
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
|||||||
@ -23,7 +23,7 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $tenant->workspace]))
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
expect(Filament::getTenant())->toBe($tenant);
|
expect(Filament::getTenant())->toBe($tenant);
|
||||||
@ -37,9 +37,9 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $tenant->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('All tenants');
|
->assertSee(__('localization.shell.all_environments'));
|
||||||
|
|
||||||
expect(Filament::getTenant())->toBeNull();
|
expect(Filament::getTenant())->toBeNull();
|
||||||
});
|
});
|
||||||
@ -71,7 +71,7 @@
|
|||||||
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id,
|
||||||
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => $lastTenantMap,
|
WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY => $lastTenantMap,
|
||||||
])
|
])
|
||||||
->get("/admin/operations/{$run->getKey()}")
|
->get(route('admin.operations.view', ['workspace' => $tenantB->workspace, 'run' => (int) $run->getKey()]))
|
||||||
->assertOk();
|
->assertOk();
|
||||||
|
|
||||||
expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY))->toBe($lastTenantMap);
|
expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY))->toBe($lastTenantMap);
|
||||||
|
|||||||
@ -22,11 +22,11 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $tenant->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('ManagedEnvironment scope: '.$tenant->name)
|
->assertSee(__('localization.shell.environment_scope').': '.$tenant->name)
|
||||||
->assertSee('Back to '.$tenant->name)
|
->assertSee('Back to '.$tenant->name)
|
||||||
->assertSee('Show all tenants');
|
->assertSee(__('localization.shell.show_all_environments'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('treats stale tenant context as workspace-wide without tenant identity hints', function (): void {
|
it('treats stale tenant context as workspace-wide without tenant identity hints', function (): void {
|
||||||
@ -41,12 +41,12 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $entitledTenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $entitledTenant->workspace_id])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $entitledTenant->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('All tenants')
|
->assertSee(__('localization.shell.all_environments'))
|
||||||
->assertDontSee('Back to '.$staleTenant->name)
|
->assertDontSee('Back to '.$staleTenant->name)
|
||||||
->assertDontSee($staleTenant->name)
|
->assertDontSee($staleTenant->name)
|
||||||
->assertDontSee('Show all tenants');
|
->assertDontSee(__('localization.shell.show_all_environments'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears filament tenant context and last-tenant session state via clear-tenant-context endpoint', function (): void {
|
it('clears filament tenant context and last-tenant session state via clear-tenant-context endpoint', function (): void {
|
||||||
@ -76,10 +76,10 @@
|
|||||||
$this->withSession([
|
$this->withSession([
|
||||||
WorkspaceContext::SESSION_KEY => $workspaceId,
|
WorkspaceContext::SESSION_KEY => $workspaceId,
|
||||||
])
|
])
|
||||||
->get('/admin/operations')
|
->get(route('admin.operations.index', ['workspace' => $tenant->workspace]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('All tenants')
|
->assertSee(__('localization.shell.all_environments'))
|
||||||
->assertDontSee('ManagedEnvironment scope: '.$tenant->name);
|
->assertDontSee(__('localization.shell.environment_scope').': '.$tenant->name);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('clears remembered tenant scope even when the stored tenant is no longer operable', function (): void {
|
it('clears remembered tenant scope even when the stored tenant is no longer operable', function (): void {
|
||||||
@ -107,9 +107,9 @@
|
|||||||
(string) $workspaceId => (int) $onboardingTenant->getKey(),
|
(string) $workspaceId => (int) $onboardingTenant->getKey(),
|
||||||
],
|
],
|
||||||
])
|
])
|
||||||
->from('/admin/operations')
|
->from(route('admin.operations.index', ['workspace' => $activeTenant->workspace]))
|
||||||
->post('/admin/clear-tenant-context')
|
->post('/admin/clear-tenant-context')
|
||||||
->assertRedirect('/admin/operations');
|
->assertRedirect(route('admin.operations.index', ['workspace' => $activeTenant->workspace]));
|
||||||
|
|
||||||
expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, []))
|
expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY, []))
|
||||||
->not->toHaveKey((string) $workspaceId);
|
->not->toHaveKey((string) $workspaceId);
|
||||||
|
|||||||
@ -33,7 +33,7 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get("/admin/operations/{$run->getKey()}")
|
->get(route('admin.operations.view', ['workspace' => $tenant->workspace, 'run' => (int) $run->getKey()]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('← Back to '.$tenant->name)
|
->assertSee('← Back to '.$tenant->name)
|
||||||
->assertSee('Show all operations')
|
->assertSee('Show all operations')
|
||||||
@ -56,7 +56,7 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||||
->get("/admin/operations/{$run->getKey()}")
|
->get(route('admin.operations.view', ['workspace' => $tenant->workspace, 'run' => (int) $run->getKey()]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('Back to Operations')
|
->assertSee('Back to Operations')
|
||||||
->assertDontSee('← Back to ')
|
->assertDontSee('← Back to ')
|
||||||
@ -83,9 +83,9 @@
|
|||||||
|
|
||||||
$this->actingAs($user)
|
$this->actingAs($user)
|
||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $entitledTenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $entitledTenant->workspace_id])
|
||||||
->get("/admin/operations/{$run->getKey()}")
|
->get(route('admin.operations.view', ['workspace' => $entitledTenant->workspace, 'run' => (int) $run->getKey()]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('All tenants')
|
->assertSee(__('localization.shell.all_environments'))
|
||||||
->assertSee('Back to Operations')
|
->assertSee('Back to Operations')
|
||||||
->assertDontSee('← Back to '.$staleTenant->name)
|
->assertDontSee('← Back to '.$staleTenant->name)
|
||||||
->assertDontSee($staleTenant->name)
|
->assertDontSee($staleTenant->name)
|
||||||
|
|||||||
@ -154,5 +154,5 @@
|
|||||||
->assertSee('Header Active ManagedEnvironment')
|
->assertSee('Header Active ManagedEnvironment')
|
||||||
->assertDontSee('Header Onboarding ManagedEnvironment')
|
->assertDontSee('Header Onboarding ManagedEnvironment')
|
||||||
->assertDontSee('Header Archived ManagedEnvironment')
|
->assertDontSee('Header Archived ManagedEnvironment')
|
||||||
->assertSee('No tenant selected');
|
->assertSee(__('localization.shell.no_environment_selected'));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -41,8 +41,8 @@
|
|||||||
->assertSee('Choose Other Active ManagedEnvironment')
|
->assertSee('Choose Other Active ManagedEnvironment')
|
||||||
->assertDontSee('Choose Onboarding ManagedEnvironment')
|
->assertDontSee('Choose Onboarding ManagedEnvironment')
|
||||||
->assertDontSee('Choose Archived ManagedEnvironment')
|
->assertDontSee('Choose Archived ManagedEnvironment')
|
||||||
->assertSee('Select the tenant for your normal active operating context.')
|
->assertSee(__('localization.shell.choose_environment_description'))
|
||||||
->assertSee('No tenant selected is still a valid workspace state');
|
->assertSee(__('localization.shell.workspace_wide_available_without_environment'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shows a workspace-safe empty state when no selectable tenants remain', function (): void {
|
it('shows a workspace-safe empty state when no selectable tenants remain', function (): void {
|
||||||
@ -59,9 +59,9 @@
|
|||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $onboardingTenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $onboardingTenant->workspace_id])
|
||||||
->get('/admin/choose-tenant')
|
->get('/admin/choose-tenant')
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('No active tenants available')
|
->assertSee(__('localization.shell.no_active_environments'))
|
||||||
->assertSee('Workspace-level pages still work with no tenant selected')
|
->assertSee(__('localization.shell.no_active_environments_description'))
|
||||||
->assertSee('View managed tenants');
|
->assertSee(__('localization.shell.view_managed_environments'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('keeps selector eligibility narrower than managed-tenant administrative discoverability', function (): void {
|
it('keeps selector eligibility narrower than managed-tenant administrative discoverability', function (): void {
|
||||||
@ -119,7 +119,7 @@
|
|||||||
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id,
|
||||||
])->get(route('admin.operations.index', ['workspace' => $tenant->workspace]))
|
])->get(route('admin.operations.index', ['workspace' => $tenant->workspace]))
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('All tenants');
|
->assertSee(__('localization.shell.all_environments'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('redirects clear selected tenant from the evidence index to the workspace-safe evidence overview', function (): void {
|
it('redirects clear selected tenant from the evidence index to the workspace-safe evidence overview', function (): void {
|
||||||
|
|||||||
@ -21,9 +21,9 @@
|
|||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee($tenant->workspace()->firstOrFail()->name)
|
->assertSee($tenant->workspace()->firstOrFail()->name)
|
||||||
->assertSee('ManagedEnvironment Panel Entry')
|
->assertSee('ManagedEnvironment Panel Entry')
|
||||||
->assertSee('Switch tenant')
|
->assertSee(__('localization.shell.switch_environment'))
|
||||||
->assertSee('Clear tenant scope')
|
->assertSee(__('localization.shell.clear_environment_scope'))
|
||||||
->assertDontSee(__('localization.shell.search_tenants'))
|
->assertDontSee(__('localization.shell.search_environments'))
|
||||||
->assertDontSee('admin/select-tenant');
|
->assertDontSee('admin/select-tenant');
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -38,6 +38,6 @@
|
|||||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspaceTenant->workspace_id])
|
->withSession([WorkspaceContext::SESSION_KEY => (int) $workspaceTenant->workspace_id])
|
||||||
->get(route('admin.operations.index', ['workspace' => $workspaceTenant->workspace, 'tenant' => $foreignTenant->external_id]))
|
->get(route('admin.operations.index', ['workspace' => $workspaceTenant->workspace, 'tenant' => $foreignTenant->external_id]))
|
||||||
->assertOk()
|
->assertOk()
|
||||||
->assertSee('No tenant selected')
|
->assertSee(__('localization.shell.no_environment_selected'))
|
||||||
->assertDontSee('ManagedEnvironment scope: Rejected Foreign ManagedEnvironment');
|
->assertDontSee(__('localization.shell.environment_scope').': Rejected Foreign ManagedEnvironment');
|
||||||
});
|
});
|
||||||
|
|||||||
@ -40,8 +40,8 @@
|
|||||||
->get(route('admin.workspace.managed-tenants.index', ['workspace' => $workspace]))
|
->get(route('admin.workspace.managed-tenants.index', ['workspace' => $workspace]))
|
||||||
->assertSuccessful()
|
->assertSuccessful()
|
||||||
->assertSee('Spec195 Landing ManagedEnvironment')
|
->assertSee('Spec195 Landing ManagedEnvironment')
|
||||||
->assertSee('Managed tenants')
|
->assertSee(__('localization.shell.managed_environments_title'))
|
||||||
->assertDontSee('No tenant selected');
|
->assertDontSee(__('localization.shell.no_environment_selected'));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('routes the managed-tenants landing back into the chooser flow and open-tenant flow', function (): void {
|
it('routes the managed-tenants landing back into the chooser flow and open-tenant flow', function (): void {
|
||||||
@ -82,7 +82,7 @@
|
|||||||
->assertRedirect(TenantDashboard::getUrl(tenant: $tenant));
|
->assertRedirect(TenantDashboard::getUrl(tenant: $tenant));
|
||||||
});
|
});
|
||||||
|
|
||||||
it('rejects opening a tenant from the landing when the actor lacks tenant entitlement', function (): void {
|
it('allows workspace-scoped access to open an environment from the landing without explicit membership', function (): void {
|
||||||
$workspace = Workspace::factory()->create(['slug' => 'spec195-managed-guard']);
|
$workspace = Workspace::factory()->create(['slug' => 'spec195-managed-guard']);
|
||||||
$user = User::factory()->create();
|
$user = User::factory()->create();
|
||||||
|
|
||||||
@ -103,5 +103,5 @@
|
|||||||
Livewire::actingAs($user)
|
Livewire::actingAs($user)
|
||||||
->test(ManagedTenantsLanding::class, ['workspace' => $workspace])
|
->test(ManagedTenantsLanding::class, ['workspace' => $workspace])
|
||||||
->call('openTenant', $tenant->getKey())
|
->call('openTenant', $tenant->getKey())
|
||||||
->assertNotFound();
|
->assertRedirect(TenantDashboard::getUrl(tenant: $tenant));
|
||||||
});
|
});
|
||||||
|
|||||||
@ -170,7 +170,7 @@
|
|||||||
|
|
||||||
expect($descriptor->label)->toBe($expectedLabel);
|
expect($descriptor->label)->toBe($expectedLabel);
|
||||||
})->with([
|
})->with([
|
||||||
'no drafts' => [0, 'Add tenant'],
|
'no drafts' => [0, 'Add environment'],
|
||||||
'one draft' => [1, 'Resume onboarding'],
|
'one draft' => [1, 'Resume onboarding'],
|
||||||
'multiple drafts' => [2, 'Choose onboarding draft'],
|
'multiple drafts' => [2, 'Choose onboarding draft'],
|
||||||
]);
|
]);
|
||||||
|
|||||||
@ -0,0 +1,60 @@
|
|||||||
|
# Specification Quality Checklist: UI Copy, IA & Localization Neutralization
|
||||||
|
|
||||||
|
**Purpose**: Validate specification completeness, boundedness, and readiness before implementation
|
||||||
|
**Created**: 2026-05-09
|
||||||
|
**Feature**: [spec.md](../spec.md)
|
||||||
|
|
||||||
|
## Content Quality
|
||||||
|
|
||||||
|
- [x] The package stays on one bounded environment-first glossary convergence over repo-real workspace-first admin surfaces plus the pinned `PolicyResource`/`ViewPolicy`/`VersionsRelationManager`/`baseline-compare-landing` helper surfaces instead of inventing a new IA framework, route migration, or localization system.
|
||||||
|
- [x] The spec remains product- and behavior-oriented rather than reading like a low-level rename diff.
|
||||||
|
- [x] The package explicitly names the repo-real anchors it builds on: the current localization catalogs, `ChooseTenant`, `ManagedTenantsLanding`, `TenantDashboard`, `OperateHubShell`, `CanonicalNavigationContext`, and the confirmed widget views.
|
||||||
|
- [x] Mandatory repo sections for scope, RBAC preservation, shared-pattern reuse, testing, proportionality, and candidate rationale are completed.
|
||||||
|
|
||||||
|
## Requirement Completeness
|
||||||
|
|
||||||
|
- [x] No `[NEEDS CLARIFICATION]` markers remain.
|
||||||
|
- [x] Requirements are testable and bounded to chooser, registry, dashboard, shell, widget, and the pinned policy/baseline-compare helper-text neutralization surfaces only.
|
||||||
|
- [x] The package explicitly preserves auth-provider wording, routes/slugs/classes, RBAC semantics, and provider-owned secondary detail.
|
||||||
|
- [x] The package forbids website localization, customer-review localization, provider-capability work, route renames, capability renames, and schema changes.
|
||||||
|
- [x] Canonical proof commands match across `spec.md`, `plan.md`, `quickstart.md`, and `tasks.md`.
|
||||||
|
|
||||||
|
## Candidate Selection Gate
|
||||||
|
|
||||||
|
- [x] The selected candidate exists in `docs/product/roadmap.md` as reserved slot `286`, and the user explicitly requested that slot.
|
||||||
|
- [x] Related anchor specs were checked for completion or prep signals and treated as context only: Specs `279`, `280`, `281`, `282`, `283`, `285`, and `275` are all referenced without being reopened.
|
||||||
|
- [x] The chosen slice is smaller and more bounded than deferred alternatives such as Spec `287`, route/slug renaming, broader provider-surface cleanup, or broader localization work.
|
||||||
|
- [x] The selected slice explicitly closes the remaining workspace-first copy and IA gap without reopening the adjacent foundation or RBAC packages.
|
||||||
|
|
||||||
|
## Feature Readiness
|
||||||
|
|
||||||
|
- [x] The package keeps Filament on Livewire v4, provider registration unchanged in `apps/platform/bootstrap/providers.php`, global search unchanged, and assets unchanged.
|
||||||
|
- [x] The package keeps the current chooser, landing, dashboard, shell, widget, and pinned policy/baseline-compare helper surfaces as the only operator-facing targets.
|
||||||
|
- [x] The package keeps provider-owned auth labels and deeper provider diagnostics out of scope unless the provider is genuinely the subject.
|
||||||
|
- [x] The package explicitly defers broader route/slug/class cleanup, broader provider-surface cleanup, and no-legacy guardrail work.
|
||||||
|
|
||||||
|
## Test Governance
|
||||||
|
|
||||||
|
- [x] Planned proof stays bounded to focused `Feature` coverage plus one bounded `Browser` smoke.
|
||||||
|
- [x] No new heavy-governance family or broad browser family is introduced by default.
|
||||||
|
- [x] Fixture growth remains bounded to existing workspace and environment context plus current admin page rendering.
|
||||||
|
- [x] The review outcome, workflow outcome, and test-governance outcome are carried into `plan.md` and `tasks.md`.
|
||||||
|
|
||||||
|
## Decision-First Guardrails
|
||||||
|
|
||||||
|
- [x] Chooser and landing surfaces keep one dominant environment-selection action and do not duplicate scope summaries.
|
||||||
|
- [x] Dashboard and shell copy stay orientation-first; diagnostics and provider detail remain secondary.
|
||||||
|
- [x] The pinned policy/baseline-compare helper surfaces keep provider detail secondary and do not compete with the primary action or summary noun.
|
||||||
|
|
||||||
|
## Notes
|
||||||
|
|
||||||
|
- Reviewed against `.specify/memory/constitution.md`, `docs/product/roadmap.md`, current repo UI seams, related specs `275`, `279`, `280`, `281`, `282`, `283`, and `285`, and the active `286` prep artifacts on 2026-05-09.
|
||||||
|
- No application implementation was performed while preparing this package.
|
||||||
|
|
||||||
|
## Review Outcome
|
||||||
|
|
||||||
|
- **Outcome class**: `acceptable-special-case`
|
||||||
|
- **Workflow outcome**: `keep`
|
||||||
|
- **Test-governance outcome**: `keep`
|
||||||
|
- **Reason**: The package is a bounded operator-copy and IA convergence slice over already-real workspace-first admin seams. It keeps provider detail bounded, blocks route/RBAC/framework expansion, and is ready for a later implementation loop.
|
||||||
|
- **Workflow result**: Ready for implementation.
|
||||||
@ -0,0 +1,181 @@
|
|||||||
|
openapi: 3.1.0
|
||||||
|
info:
|
||||||
|
title: UI Copy, IA & Localization Neutralization Logical Contract
|
||||||
|
version: 0.1.0
|
||||||
|
summary: Logical view-model contract for environment-first operator copy on bounded workspace-first admin surfaces.
|
||||||
|
paths:
|
||||||
|
/admin/choose-tenant:
|
||||||
|
get:
|
||||||
|
summary: Environment chooser surface
|
||||||
|
operationId: getEnvironmentChooserSurface
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Environment-first chooser contract
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/EnvironmentChooserSurface'
|
||||||
|
/admin/workspaces/{workspace}/managed-tenants:
|
||||||
|
get:
|
||||||
|
summary: Managed environments landing surface
|
||||||
|
operationId: getManagedEnvironmentLandingSurface
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/Workspace'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Managed-environment registry contract
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/ManagedEnvironmentLandingSurface'
|
||||||
|
/admin/workspaces/{workspace}/environments/{environment}:
|
||||||
|
get:
|
||||||
|
summary: Environment dashboard surface
|
||||||
|
operationId: getEnvironmentDashboardSurface
|
||||||
|
parameters:
|
||||||
|
- $ref: '#/components/parameters/Workspace'
|
||||||
|
- $ref: '#/components/parameters/Environment'
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Environment dashboard heading and chip contract
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/EnvironmentDashboardSurface'
|
||||||
|
/__logical/shell/context-bar:
|
||||||
|
get:
|
||||||
|
summary: Shared shell/context-bar copy surface
|
||||||
|
operationId: getEnvironmentShellSurface
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Environment-first shell label contract
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/EnvironmentShellSurface'
|
||||||
|
/__logical/policy/provider-helper-copy:
|
||||||
|
get:
|
||||||
|
summary: Pinned provider-helper copy surfaces
|
||||||
|
operationId: getPolicyProviderHelperSurface
|
||||||
|
responses:
|
||||||
|
'200':
|
||||||
|
description: Policy detail, versions restore, and baseline-compare helper-copy contract
|
||||||
|
content:
|
||||||
|
application/json:
|
||||||
|
schema:
|
||||||
|
$ref: '#/components/schemas/PolicyProviderHelperSurface'
|
||||||
|
components:
|
||||||
|
parameters:
|
||||||
|
Workspace:
|
||||||
|
name: workspace
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
Environment:
|
||||||
|
name: environment
|
||||||
|
in: path
|
||||||
|
required: true
|
||||||
|
schema:
|
||||||
|
type: string
|
||||||
|
schemas:
|
||||||
|
EnvironmentChooserSurface:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
- primaryActionLabel
|
||||||
|
- emptyStateLabel
|
||||||
|
- searchPlaceholder
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- Choose environment
|
||||||
|
- Umgebung auswählen
|
||||||
|
primaryActionLabel:
|
||||||
|
type: string
|
||||||
|
emptyStateLabel:
|
||||||
|
type: string
|
||||||
|
searchPlaceholder:
|
||||||
|
type: string
|
||||||
|
providerOwnedLabelsOutOfScope:
|
||||||
|
type: array
|
||||||
|
items:
|
||||||
|
type: string
|
||||||
|
default:
|
||||||
|
- Sign in with Microsoft
|
||||||
|
ManagedEnvironmentLandingSurface:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
- openActionLabel
|
||||||
|
- registryBackLinkLabel
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- Managed environments
|
||||||
|
- Verwaltete Umgebungen
|
||||||
|
openActionLabel:
|
||||||
|
type: string
|
||||||
|
registryBackLinkLabel:
|
||||||
|
type: string
|
||||||
|
rowContextField:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- environment_label
|
||||||
|
EnvironmentDashboardSurface:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- title
|
||||||
|
- scopeLabel
|
||||||
|
properties:
|
||||||
|
title:
|
||||||
|
type: string
|
||||||
|
enum:
|
||||||
|
- Environment dashboard
|
||||||
|
- Umgebungs-Dashboard
|
||||||
|
scopeLabel:
|
||||||
|
type: string
|
||||||
|
returnLabel:
|
||||||
|
type: string
|
||||||
|
helperCopy:
|
||||||
|
type: object
|
||||||
|
additionalProperties:
|
||||||
|
type: string
|
||||||
|
description: Neutral default copy for bounded restore/capture/baseline-compare entry surfaces. Provider detail remains secondary.
|
||||||
|
EnvironmentShellSurface:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- scopeLabel
|
||||||
|
- returnLabel
|
||||||
|
properties:
|
||||||
|
scopeLabel:
|
||||||
|
type: string
|
||||||
|
returnLabel:
|
||||||
|
type: string
|
||||||
|
duplicateScopeSummaryForbidden:
|
||||||
|
type: boolean
|
||||||
|
const: true
|
||||||
|
PolicyProviderHelperSurface:
|
||||||
|
type: object
|
||||||
|
required:
|
||||||
|
- policyDetailSyncDescription
|
||||||
|
- viewPolicyCaptureSubheading
|
||||||
|
- versionsRestoreLabel
|
||||||
|
- versionsRestoreHeading
|
||||||
|
- baselineCompareRbacSummaryHeading
|
||||||
|
properties:
|
||||||
|
policyDetailSyncDescription:
|
||||||
|
type: string
|
||||||
|
viewPolicyCaptureSubheading:
|
||||||
|
type: string
|
||||||
|
versionsRestoreLabel:
|
||||||
|
type: string
|
||||||
|
versionsRestoreHeading:
|
||||||
|
type: string
|
||||||
|
baselineCompareRbacSummaryHeading:
|
||||||
|
type: string
|
||||||
|
providerDetailSecondaryOnly:
|
||||||
|
type: boolean
|
||||||
|
const: true
|
||||||
@ -0,0 +1,56 @@
|
|||||||
|
# Data Model: UI Copy, IA & Localization Neutralization
|
||||||
|
|
||||||
|
## Overview
|
||||||
|
|
||||||
|
`286` introduces no new persisted entity, table, or lifecycle state. The "data model" for this package is a derived operator-copy contract shared across localization keys, page-title methods, shell labels, surface-local rendered labels/test IDs, and bounded helper text.
|
||||||
|
|
||||||
|
## Canonical Operator Terms
|
||||||
|
|
||||||
|
| Term | Meaning | Where It Is Primary | Where It Is Secondary or Out of Scope |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `Workspace` | top-level SaaS context | chooser precondition, workspace headings, registry context | never the target object of environment-selection actions |
|
||||||
|
| `Environment` | default operator noun for the selected managed target | chooser actions, shell labels, dashboard headings, default action copy | not used where auth provider is the subject |
|
||||||
|
| `Managed environment` | disambiguated environment noun for registry/list headings | registry titles and list headings | not the default noun for chooser/shell/action labels |
|
||||||
|
| `Provider` | generic external system noun | default helper text where provider detail is secondary | not used to hide when the provider is genuinely the subject |
|
||||||
|
| explicit provider name | provider-owned detail | auth-provider labels, secondary helper text, provider diagnostics | not the default-visible noun on environment-scoped admin flows |
|
||||||
|
|
||||||
|
## In-Scope Localization Key Contract
|
||||||
|
|
||||||
|
| Old Contract Family | Canonical Replacement | Purpose |
|
||||||
|
|---|---|---|
|
||||||
|
| `tenant_scope` | `environment_scope` | shell scope label |
|
||||||
|
| `select_tenant` | `select_environment` | chooser CTA |
|
||||||
|
| `selected_tenant` | `selected_environment` | selected-context label |
|
||||||
|
| `no_tenant_selected` | `no_environment_selected` | empty/current-context helper |
|
||||||
|
| `switch_tenant` | `switch_environment` | context-switch affordance |
|
||||||
|
| `clear_tenant_scope` | `clear_environment_scope` | scope reset affordance |
|
||||||
|
| `no_active_tenants` | `no_active_environments` | chooser empty-state message |
|
||||||
|
| `view_managed_tenants` | `view_managed_environments` | landing/registry CTA |
|
||||||
|
| `search_tenants` | `search_environments` | chooser search placeholder |
|
||||||
|
| `tenant_title` | `environment_title` | dashboard heading key |
|
||||||
|
|
||||||
|
## View-Model Contract Expectations
|
||||||
|
|
||||||
|
| Field | Meaning | Example | Notes |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `scope_label` | shell scope copy | `Environment scope: Production EU` | derived from current environment context |
|
||||||
|
| `registry_back_link_label` | registry return label | `Back to environment registry` | route target unchanged |
|
||||||
|
| `dashboard_title_key` | environment dashboard translation key | `localization.dashboard.environment_title` | consumed by dashboard title helpers |
|
||||||
|
| `provider_detail_label` | explicit provider-owned secondary copy | `Microsoft Graph permissions are required.` | only used when provider detail is genuinely explanatory |
|
||||||
|
|
||||||
|
## Invariants
|
||||||
|
|
||||||
|
- Operator-facing copy on the in-scope admin surfaces must never teach `tenant` as the default noun once the surrounding workflow is already environment-scoped.
|
||||||
|
- Auth-provider labels remain provider-owned and unchanged.
|
||||||
|
- Route names, slugs, PHP class names, model names, and capability names remain unchanged.
|
||||||
|
- Shared payload keys outside the enumerated chooser/registry/dashboard/shell/widget surfaces remain unchanged in this slice, even if they still use tenant-shaped internal names.
|
||||||
|
- English and German must expose the same in-scope noun choices.
|
||||||
|
- No alias translation keys are added for the canonical replacement family inside this slice.
|
||||||
|
|
||||||
|
## Out of Scope Data Changes
|
||||||
|
|
||||||
|
- no database migrations
|
||||||
|
- no persisted glossary table
|
||||||
|
- no route-name registry changes
|
||||||
|
- no capability or policy storage changes
|
||||||
|
- no review/export/customer-facing copy contract changes beyond already-existing packages
|
||||||
241
specs/286-ui-copy-ia-localization-neutralization/plan.md
Normal file
241
specs/286-ui-copy-ia-localization-neutralization/plan.md
Normal file
@ -0,0 +1,241 @@
|
|||||||
|
# Implementation Plan: UI Copy, IA & Localization Neutralization
|
||||||
|
|
||||||
|
**Branch**: `286-ui-copy-ia-localization-neutralization` | **Date**: 2026-05-09 | **Spec**: [spec.md](./spec.md)
|
||||||
|
**Input**: Feature specification from `specs/286-ui-copy-ia-localization-neutralization/spec.md`
|
||||||
|
|
||||||
|
## Summary
|
||||||
|
|
||||||
|
Prepare the reserved workspace-first cutover copy slice by converging the operator-facing glossary on a single environment-first vocabulary across the confirmed post-cutover admin surfaces. The narrow implementation path updates shared localization keys, page titles, shared shell labels, dashboard headings, context chips, registry return labels, selected workspace widgets, and bounded provider-neutral helper text while explicitly avoiding route renames, class renames, RBAC/capability renames, schema changes, or broader provider-surface cleanup.
|
||||||
|
|
||||||
|
This plan is intentionally bounded. Filament remains v5 on Livewire v4, provider registration remains in `apps/platform/bootstrap/providers.php`, touched resources keep their current search posture, destructive action behavior is preserved, and deployment assets remain unchanged.
|
||||||
|
|
||||||
|
## Inherited Baseline / Explicit Delta
|
||||||
|
|
||||||
|
### Inherited baseline
|
||||||
|
|
||||||
|
- Spec `279` already established the managed-environment core cutover and is historical prerequisite context only.
|
||||||
|
- Spec `280` already prepared the workspace-first route and page ownership model that `286` must align with rather than redesign.
|
||||||
|
- Spec `281` already prepared provider-neutral provider-connection and target-scope vocabulary; `286` must reuse that boundary rather than rename provider-owned detail blindly.
|
||||||
|
- Spec `282` already frames the adjacent artifact-retargeting lane and remains out of scope except where `286` must avoid contradicting environment-scoped admin nouns.
|
||||||
|
- Spec `283` already prepared provider capability boundaries; `286` must not rename or absorb provider-capability vocabulary.
|
||||||
|
- Spec `285` already captures the adjacent workspace-first RBAC lane; `286` must not absorb role, capability, or policy wording changes.
|
||||||
|
- Spec `275` already covers customer-facing localization adoption on review surfaces and remains related glossary discipline only, not the current admin-surface target.
|
||||||
|
- Current repo runtime already shows environment-shaped seams, including environment routes in `TenantDashboard::getUrl()`, but key operator surfaces still expose titles and labels such as `Choose tenant`, `Managed tenants`, `Tenant scope`, and `All tenants`.
|
||||||
|
|
||||||
|
### Explicit delta in this plan
|
||||||
|
|
||||||
|
- Replace the in-scope operator-facing tenant-first glossary with an environment-first glossary in English and German.
|
||||||
|
- Rename the in-scope translation-key family and only the local display labels/test IDs owned by the enumerated surfaces where `tenant_*` naming still leaks into operator-facing shells.
|
||||||
|
- Neutralize the default-visible helper text on the exact `PolicyResource`, `ViewPolicy`, `VersionsRelationManager`, and `baseline-compare-landing` surfaces while keeping provider detail secondary.
|
||||||
|
- Keep auth-provider wording, route names, slugs, classes, RBAC behavior, and provider-owned deeper detail unchanged.
|
||||||
|
- Keep Spec `287` and any broader provider-surface cleanup explicitly deferred.
|
||||||
|
|
||||||
|
## Technical Context
|
||||||
|
|
||||||
|
**Language/Version**: PHP 8.4.15, Laravel 12.52
|
||||||
|
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, current localization catalogs, current shared shell helpers, current dashboard and widget surfaces
|
||||||
|
**Storage**: No persistence or schema changes; glossary and helper copy remain derived from localization files and view-model output only
|
||||||
|
**Testing**: Pest feature tests and one Pest browser smoke
|
||||||
|
**Validation Lanes**: confidence, browser
|
||||||
|
**Target Platform**: Laravel monolith in `apps/platform`
|
||||||
|
**Project Type**: web application
|
||||||
|
**Performance Goals**: preserve current page responsiveness; this slice changes rendered copy and small view-model contracts only
|
||||||
|
**Constraints**: no route rename, no slug rename, no model/class rename, no schema change, no RBAC rename, no asset change, no new translation framework, no auth-provider wording change, no broader provider cleanup
|
||||||
|
**Scale/Scope**: one bounded environment-first glossary over current workspace-first admin surfaces only
|
||||||
|
|
||||||
|
## Likely Affected Repo Surfaces
|
||||||
|
|
||||||
|
- `apps/platform/lang/en/localization.php`
|
||||||
|
- `apps/platform/lang/de/localization.php`
|
||||||
|
- `apps/platform/lang/en/baseline-compare.php`
|
||||||
|
- `apps/platform/lang/de/baseline-compare.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/ChooseTenant.php`
|
||||||
|
- `apps/platform/resources/views/filament/pages/choose-tenant.blade.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php`
|
||||||
|
- `apps/platform/resources/views/filament/pages/workspaces/managed-tenants-landing.blade.php`
|
||||||
|
- `apps/platform/app/Filament/Pages/TenantDashboard.php`
|
||||||
|
- `apps/platform/resources/views/filament/widgets/dashboard/tenant-dashboard-context-chips.blade.php`
|
||||||
|
- `apps/platform/app/Support/OperateHub/OperateHubShell.php`
|
||||||
|
- `apps/platform/app/Support/Navigation/CanonicalNavigationContext.php`
|
||||||
|
- `apps/platform/resources/views/filament/widgets/workspace/workspace-needs-attention.blade.php`
|
||||||
|
- `apps/platform/resources/views/filament/widgets/workspace/workspace-recent-operations.blade.php`
|
||||||
|
- `apps/platform/app/Filament/Resources/PolicyResource.php`
|
||||||
|
- `apps/platform/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`
|
||||||
|
- `apps/platform/app/Filament/Resources/PolicyResource/RelationManagers/VersionsRelationManager.php`
|
||||||
|
- `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php`
|
||||||
|
- representative proof files under `apps/platform/tests/Feature/Localization/`, `apps/platform/tests/Feature/Filament/`, `apps/platform/tests/Feature/Guards/`, and `apps/platform/tests/Browser/`
|
||||||
|
|
||||||
|
## Filament v5 / Surface Notes
|
||||||
|
|
||||||
|
- **Livewire v4.0+ compliance**: all touched admin surfaces remain on Filament v5 with Livewire v4.
|
||||||
|
- **Provider registration location**: provider registration remains in `apps/platform/bootstrap/providers.php`; nothing moves to `bootstrap/app.php`.
|
||||||
|
- **Global search rule**:
|
||||||
|
- `ChooseTenant` and `ManagedTenantsLanding` are pages, not globally-searchable resources.
|
||||||
|
- `TenantDashboard` remains a page and does not change global-search posture.
|
||||||
|
- If any touched registry resource label is updated during implementation, the resource must keep its existing `View` or `Edit` destination and current global-search behavior.
|
||||||
|
- **Destructive actions**: `286` introduces no new destructive action. Any touched destructive-like actions on adjacent pages keep their existing `->action(...)`, `->requiresConfirmation()`, and server authorization.
|
||||||
|
- **Asset strategy**: no new asset registration or deploy-step change is planned.
|
||||||
|
|
||||||
|
## Neutralization Contract Fit
|
||||||
|
|
||||||
|
- Use `Environment` as the default operator noun for chooser, shell, dashboard, and default action copy.
|
||||||
|
- Use `Managed environment` only where a registry or heading must distinguish the object from a workspace.
|
||||||
|
- Use `Provider` as the generic external-system noun when the action headline does not need a specific provider name.
|
||||||
|
- Keep explicit provider names only when the provider itself is the subject, such as auth-provider flows or secondary helper text.
|
||||||
|
- Replace direct `tenant_*` glossary keys and only local template-owned labels/test IDs on the in-scope admin surfaces instead of preserving alias keys for convenience. Shared cross-surface payload contracts outside the enumerated surface set remain unchanged in `286`.
|
||||||
|
- Do not rename internal route names, slugs, class names, or database fields in `286`.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Plan
|
||||||
|
|
||||||
|
- **Guardrail scope**: changed surfaces
|
||||||
|
- **Native vs custom classification summary**: mostly native Filament pages plus existing Blade views and widget templates
|
||||||
|
- **Shared-family relevance**: navigation, shell scope messaging, dashboard chips, workspace widgets, helper text
|
||||||
|
- **State layers in scope**: shell, page, widget view, localization key, view-model payload
|
||||||
|
- **Audience modes in scope**: operator-MSP, support-platform
|
||||||
|
- **Decision/diagnostic/raw hierarchy plan**: environment-first decision copy, diagnostics-second, provider/raw-third
|
||||||
|
- **Raw/support gating plan**: provider-specific detail remains helper or secondary text; raw/debug detail stays out of the default-visible path
|
||||||
|
- **One-primary-action / duplicate-truth control**: chooser and registry surfaces keep one dominant environment-selection action; headings and shell labels stay orientation-only
|
||||||
|
- **Handling modes by drift class or surface**: review-mandatory
|
||||||
|
- **Repository-signal treatment**: review-mandatory for any remaining in-scope tenant-first or default-visible provider-first noun after implementation
|
||||||
|
- **Special surface test profiles**: standard-native-filament, global-context-shell
|
||||||
|
- **Required tests or manual smoke**: functional-core, browser-smoke
|
||||||
|
- **Exception path and spread control**: none; any need for route or RBAC renaming becomes `follow-up-spec`
|
||||||
|
- **Active feature PR close-out entry**: Smoke Coverage
|
||||||
|
|
||||||
|
## Shared Pattern & System Fit
|
||||||
|
|
||||||
|
- **Cross-cutting feature marker**: yes
|
||||||
|
- **Systems touched**: localization catalogs, shared shell helpers, page-title methods, dashboard chips, workspace widgets, `PolicyResource`, `ViewPolicy`, `VersionsRelationManager`, `baseline-compare-landing`, and their helper text consumers
|
||||||
|
- **Shared abstractions reused**: current translation catalogs, `WorkspaceContext`, `OperateHubShell`, `CanonicalNavigationContext`, current widget view-models, and current Filament translation usage
|
||||||
|
- **New abstraction introduced? why?**: none
|
||||||
|
- **Why the existing abstraction was sufficient or insufficient**: current abstractions already own the right UI seams; they simply expose the wrong nouns today
|
||||||
|
- **Bounded deviation / spread control**: no new local glossary or alias layer; in-scope files converge on the same key family and noun decisions, but shared payload keys outside the enumerated surface set remain unchanged until a later bounded cleanup slice owns them
|
||||||
|
|
||||||
|
## OperationRun UX Impact
|
||||||
|
|
||||||
|
- **Touches OperationRun start/completion/link UX?**: no
|
||||||
|
- **Central contract reused**: `N/A`
|
||||||
|
- **Delegated UX behaviors**: `N/A`
|
||||||
|
- **Surface-owned behavior kept local**: `N/A`
|
||||||
|
- **Queued DB-notification policy**: `N/A`
|
||||||
|
- **Terminal notification path**: `N/A`
|
||||||
|
- **Exception path**: none
|
||||||
|
|
||||||
|
## Provider Boundary & Portability Fit
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes
|
||||||
|
- **Provider-owned seams**: auth-provider labels, provider-specific helper detail, Graph-specific explanations, explicit provider names in secondary copy
|
||||||
|
- **Platform-core seams**: environment chooser labels, shell scope labels, dashboard titles, registry return links, default action/helper nouns on environment-scoped admin surfaces
|
||||||
|
- **Neutral platform terms / contracts preserved**: `workspace`, `environment`, `managed environment`, `provider`, `environment scope`
|
||||||
|
- **Retained provider-specific semantics and why**: provider names remain where the provider itself is the subject of the action or the diagnostic explanation
|
||||||
|
- **Bounded extraction or follow-up path**: no broader provider-surface cleanup in this slice; if additional provider-first copy is discovered outside the bounded surfaces, record it as `follow-up-spec`
|
||||||
|
|
||||||
|
## Constitution Check
|
||||||
|
|
||||||
|
*GATE: Must pass before implementation begins and again after design artifacts are complete.*
|
||||||
|
|
||||||
|
- Inventory-first / snapshot truth: PASS. No persisted truth changes.
|
||||||
|
- Read/write separation: PASS. This is a read-only copy and IA slice.
|
||||||
|
- Graph contract path: PASS. No new Graph usage.
|
||||||
|
- Deterministic capabilities: PASS. Capability and RBAC semantics are unchanged.
|
||||||
|
- RBAC-UX plane separation: PASS. `/admin` versus `/system` remains unchanged.
|
||||||
|
- Workspace isolation: PASS. Workspace selection and environment entitlement remain existing boundaries.
|
||||||
|
- Managed-environment isolation: PASS. Copy changes do not widen discoverability.
|
||||||
|
- Destructive action discipline: PASS by preservation. No new destructive action is introduced.
|
||||||
|
- Global search safety: PASS. No new globally-searchable resource is introduced by this slice.
|
||||||
|
- OperationRun / Ops-UX: PASS. No start/completion/link semantics change.
|
||||||
|
- Data minimization: PASS. No new payload or storage surface.
|
||||||
|
- Test governance: PASS. Proof stays bounded to feature and one browser smoke.
|
||||||
|
- Proportionality / no premature abstraction: PASS. No new abstraction or framework is introduced.
|
||||||
|
- Persisted truth / behavioral state: PASS. No new state or table.
|
||||||
|
- UI semantics / shared pattern first / Filament-native UI: PASS. Existing native surfaces remain primary.
|
||||||
|
- Provider boundary: PASS with implementation condition. Default-visible copy must stay platform-core while provider-specific detail remains secondary.
|
||||||
|
|
||||||
|
**Gate evaluation**: PASS.
|
||||||
|
|
||||||
|
**Post-design re-check**: PASS while `spec.md`, `plan.md`, `tasks.md`, `research.md`, `data-model.md`, `quickstart.md`, `contracts/ui-copy-ia-localization-neutralization.logical.openapi.yaml`, and `checklists/requirements.md` stay aligned on the same noun choices, proof commands, and explicit non-goals.
|
||||||
|
|
||||||
|
## Test Governance Check
|
||||||
|
|
||||||
|
- **Test purpose / classification by changed surface**: Feature, Browser
|
||||||
|
- **Affected validation lanes**: confidence, browser
|
||||||
|
- **Why this lane mix is the narrowest sufficient proof**: in-scope changes are rendered labels and helper text on existing admin surfaces, plus one chooser-to-environment orientation flow
|
||||||
|
- **Narrowest proving command(s)**:
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Localization/EnvironmentContextTerminologyTest.php tests/Feature/Filament/EnvironmentContextSurfaceCopyTest.php tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec286EnvironmentCopyNeutralizationSmokeTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- **Fixture / helper / factory / seed / context cost risks**: low to moderate because proof needs a workspace, one managed environment, and current admin page rendering only
|
||||||
|
- **Expensive defaults or shared helper growth introduced?**: no; any new view-model helper stays local to the bounded admin surfaces
|
||||||
|
- **Heavy-family additions, promotions, or visibility changes**: none beyond one bounded browser smoke
|
||||||
|
- **Surface-class relief / special coverage rule**: standard-native-filament relief for page-title and translation tests; global-context-shell coverage for shell labels and chooser flow
|
||||||
|
- **Closing validation and reviewer handoff**: rerun the commands above, verify that no in-scope surface still renders `tenant` or default-visible `Microsoft` nouns where a platform noun is sufficient, verify auth-provider labels remain unchanged, and verify no route or RBAC symbol changed under the guise of copy cleanup
|
||||||
|
- **Budget / baseline / trend follow-up**: contained feature-local increase only
|
||||||
|
- **Review-stop questions**: did the implementation rename routes, slugs, classes, or capabilities; did it neutralize auth-provider labels that should remain provider-owned; did it leave tenant-first nouns in in-scope surfaces
|
||||||
|
- **Escalation path**: `follow-up-spec` for any broader symbol or provider-surface cleanup discovered during implementation
|
||||||
|
- **Active feature PR close-out entry**: Smoke Coverage
|
||||||
|
- **Why no dedicated follow-up spec is needed**: the reserved guardrail pack already exists as Spec `287`; `286` only needs the bounded operator-copy slice
|
||||||
|
|
||||||
|
## Review Checklist Status
|
||||||
|
|
||||||
|
- **Review checklist artifact**: `checklists/requirements.md`
|
||||||
|
- **Review outcome class**: `acceptable-special-case`
|
||||||
|
- **Workflow outcome**: `keep`
|
||||||
|
- **Test-governance outcome**: `keep`
|
||||||
|
- **Resolution note**: the package is implementation-ready as a bounded copy and IA convergence slice over already-real workspace-first admin seams; no route, RBAC, provider, or schema expansion is required
|
||||||
|
- **Escalation rule**: if implementation requires route or slug or class renames, capability renames, or broad provider-surface cleanup, split that work into a follow-up spec instead of widening `286`
|
||||||
|
|
||||||
|
## Rollout Considerations
|
||||||
|
|
||||||
|
- Land the shared shell and chooser noun changes together so operators do not see mixed tenant/environment wording during the transition.
|
||||||
|
- Update translation keys and directly surfaced view-model labels before or alongside Blade/template consumers so raw translation keys or mismatched labels do not leak.
|
||||||
|
- Keep provider-neutral helper text scoped to the confirmed restore/capture/baseline-compare entry surfaces only; do not chase provider wording across the entire codebase in the same slice.
|
||||||
|
|
||||||
|
## Risk Controls
|
||||||
|
|
||||||
|
- Reject any implementation that renames routes, slugs, classes, capabilities, or database fields.
|
||||||
|
- Reject any implementation that neutralizes auth-provider wording where the provider is genuinely the subject.
|
||||||
|
- Reject any implementation that introduces translation-key aliases instead of canonically replacing the in-scope key family.
|
||||||
|
- Reject any implementation that broadens into website localization, customer-review localization, or provider-capability work.
|
||||||
|
|
||||||
|
## Research & Design Outputs
|
||||||
|
|
||||||
|
- `research.md` records the glossary decisions, retained provider-owned detail, and rejected alternatives.
|
||||||
|
- `data-model.md` captures the derived copy contract, translation-key family changes, and view-model label expectations.
|
||||||
|
- `quickstart.md` gives reviewers the bounded proof flow and exact commands.
|
||||||
|
- `contracts/ui-copy-ia-localization-neutralization.logical.openapi.yaml` models the logical GET/view-model surfaces for chooser, registry, dashboard, shell/context-bar labels, and the pinned policy/baseline-compare helper surfaces.
|
||||||
|
- `checklists/requirements.md` records package readiness and review outcome.
|
||||||
|
|
||||||
|
## Project Structure
|
||||||
|
|
||||||
|
### Documentation (this feature)
|
||||||
|
|
||||||
|
```text
|
||||||
|
specs/286-ui-copy-ia-localization-neutralization/
|
||||||
|
├── checklists/
|
||||||
|
│ └── requirements.md
|
||||||
|
├── contracts/
|
||||||
|
│ └── ui-copy-ia-localization-neutralization.logical.openapi.yaml
|
||||||
|
├── data-model.md
|
||||||
|
├── plan.md
|
||||||
|
├── quickstart.md
|
||||||
|
├── research.md
|
||||||
|
├── spec.md
|
||||||
|
└── tasks.md
|
||||||
|
```
|
||||||
|
|
||||||
|
### Source Code (repository root)
|
||||||
|
|
||||||
|
```text
|
||||||
|
apps/platform/
|
||||||
|
├── app/
|
||||||
|
│ ├── Filament/Pages/
|
||||||
|
│ ├── Support/OperateHub/
|
||||||
|
│ └── Support/Navigation/
|
||||||
|
├── lang/
|
||||||
|
├── resources/views/filament/
|
||||||
|
└── tests/
|
||||||
|
├── Browser/
|
||||||
|
└── Feature/
|
||||||
|
```
|
||||||
|
|
||||||
|
**Structure Decision**: keep all implementation inside the existing Laravel admin surface, localization catalogs, and current test directories. No new base directory is needed.
|
||||||
@ -0,0 +1,46 @@
|
|||||||
|
# Quickstart: UI Copy, IA & Localization Neutralization
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Implement the bounded environment-first glossary convergence on the confirmed post-workspace-first admin surfaces only.
|
||||||
|
|
||||||
|
## Preconditions
|
||||||
|
|
||||||
|
1. Work on a branch that already contains the current workspace-first route and environment dashboard seams.
|
||||||
|
2. Do not widen into route/slug/class renaming.
|
||||||
|
3. Keep auth-provider labels and deeper provider-specific diagnostics out of scope unless the spec explicitly names them.
|
||||||
|
|
||||||
|
## Recommended Implementation Order
|
||||||
|
|
||||||
|
1. Update the in-scope localization catalogs and directly surfaced key family from `tenant_*` to `environment_*`.
|
||||||
|
2. Apply the new glossary to chooser and landing surfaces.
|
||||||
|
3. Apply the same glossary to dashboard headings, shared shell labels, registry return labels, and workspace widgets.
|
||||||
|
4. Neutralize the bounded provider helper text on `PolicyResource`, `ViewPolicy`, `VersionsRelationManager`, and `baseline-compare-landing` only.
|
||||||
|
5. Run the exact proof commands below.
|
||||||
|
|
||||||
|
## Proof Commands
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Localization/EnvironmentContextTerminologyTest.php tests/Feature/Filament/EnvironmentContextSurfaceCopyTest.php tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec286EnvironmentCopyNeutralizationSmokeTest.php
|
||||||
|
```
|
||||||
|
|
||||||
|
```bash
|
||||||
|
export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual Review Prompts
|
||||||
|
|
||||||
|
- Confirm the chooser and registry no longer say `tenant` in their primary title, CTA, or empty-state copy.
|
||||||
|
- Confirm dashboard and shell labels say `environment` while route targets remain unchanged.
|
||||||
|
- Confirm auth-provider labels such as `Sign in with Microsoft` remain unchanged.
|
||||||
|
- Confirm provider-specific helper text remains available only as secondary detail on `PolicyResource`, `ViewPolicy`, `VersionsRelationManager`, and `baseline-compare-landing` where the provider is genuinely the subject.
|
||||||
|
|
||||||
|
## Explicit Stop Conditions
|
||||||
|
|
||||||
|
- If implementation requires route/slug/class renames, stop and split the work.
|
||||||
|
- If implementation requires RBAC capability renaming, stop and split the work.
|
||||||
|
- If implementation discovers broader provider-owned copy cleanup outside the bounded admin surfaces, record it as follow-up rather than widening `286`.
|
||||||
61
specs/286-ui-copy-ia-localization-neutralization/research.md
Normal file
61
specs/286-ui-copy-ia-localization-neutralization/research.md
Normal file
@ -0,0 +1,61 @@
|
|||||||
|
# Research: UI Copy, IA & Localization Neutralization
|
||||||
|
|
||||||
|
## Decision 1: `Environment` is the default operator noun
|
||||||
|
|
||||||
|
- Use `Environment` on chooser, shell, dashboard, and default action copy.
|
||||||
|
- Use `Managed environment` only where a registry or heading must distinguish the object from the workspace.
|
||||||
|
- Do not keep `tenant` as an operator-visible default noun on the in-scope post-workspace-first admin surfaces.
|
||||||
|
|
||||||
|
## Decision 2: Provider-specific names remain secondary unless the provider is the subject
|
||||||
|
|
||||||
|
- Keep auth-provider wording such as `Sign in with Microsoft` unchanged.
|
||||||
|
- Keep provider-specific detail such as Graph-specific helper text or explicit provider names only as secondary explanatory content.
|
||||||
|
- Neutralize default-visible action/helper copy on the bounded restore/capture/baseline-compare entry surfaces where the platform noun is sufficient for the first operator decision.
|
||||||
|
|
||||||
|
## Decision 3: `286` is copy and IA only, not a symbol-renaming slice
|
||||||
|
|
||||||
|
- Do not rename routes, slugs, class names, model names, capability names, or database fields.
|
||||||
|
- Internal tenant-shaped symbols may remain temporarily while operator-facing copy becomes environment-first.
|
||||||
|
- Any need for symbol renaming belongs to a later guardrail or cleanup spec, not this package.
|
||||||
|
|
||||||
|
## Decision 4: Canonical replacement beats aliasing for the in-scope key family
|
||||||
|
|
||||||
|
- Replace in-scope translation keys and directly surfaced view-model labels that still encode tenant-first operator nouns.
|
||||||
|
- Do not preserve alias keys for convenience where the key name itself remains a shared semantic contract.
|
||||||
|
- Keep the replacement bounded to the confirmed admin surfaces in this package.
|
||||||
|
|
||||||
|
## Decision 5: Stay out of adjacent specs
|
||||||
|
|
||||||
|
- Do not absorb RBAC wording from Spec `285`.
|
||||||
|
- Do not absorb provider-capability or provider-identity work from Specs `281` and `283`.
|
||||||
|
- Do not absorb no-legacy enforcement from Spec `287`.
|
||||||
|
- Do not expand into website or customer-review localization already covered elsewhere.
|
||||||
|
|
||||||
|
## Rejected Alternatives
|
||||||
|
|
||||||
|
### Rejected: value-only translation updates without key cleanup
|
||||||
|
|
||||||
|
That would keep tenant-first semantics in shared key families and test contracts, which is the same drift in a different place.
|
||||||
|
|
||||||
|
### Rejected: route or slug renaming in the same slice
|
||||||
|
|
||||||
|
That widens the package from copy/IA into deeper runtime cutover work and should be handled separately if still needed.
|
||||||
|
|
||||||
|
### Rejected: repo-wide provider-neutral string sweep
|
||||||
|
|
||||||
|
The slot is explicitly bounded. Repo-wide cleanup would collapse multiple provider-owned and customer-facing follow-up lanes into one package.
|
||||||
|
|
||||||
|
## Evidence Anchors
|
||||||
|
|
||||||
|
- `apps/platform/lang/en/localization.php` still exposes `tenant_scope`, `select_tenant`, `selected_tenant`, `no_tenant_selected`, `no_active_tenants`, `view_managed_tenants`, `workspace_wide_available`, and `tenant_title`.
|
||||||
|
- `apps/platform/app/Filament/Pages/ChooseTenant.php` still sets `Choose tenant` as the page title.
|
||||||
|
- `apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php` still sets `Managed tenants` as the page title.
|
||||||
|
- `apps/platform/app/Support/OperateHub/OperateHubShell.php` still exposes `ManagedEnvironment scope` and `All tenants` as shell labels.
|
||||||
|
- `apps/platform/app/Support/Navigation/CanonicalNavigationContext.php` still exposes `Back to tenant registry`.
|
||||||
|
- `apps/platform/app/Filament/Pages/TenantDashboard.php` still consumes `localization.dashboard.tenant_title`.
|
||||||
|
|
||||||
|
## Implementation Boundary Summary
|
||||||
|
|
||||||
|
- The package is implementation-ready for bounded operator-copy convergence.
|
||||||
|
- It is not a prerequisite-unblocking package for routes, RBAC, or provider capabilities.
|
||||||
|
- If implementation uncovers a genuine need for route or symbol renaming, stop and split that work into a follow-up spec.
|
||||||
359
specs/286-ui-copy-ia-localization-neutralization/spec.md
Normal file
359
specs/286-ui-copy-ia-localization-neutralization/spec.md
Normal file
@ -0,0 +1,359 @@
|
|||||||
|
# Feature Specification: UI Copy, IA & Localization Neutralization
|
||||||
|
|
||||||
|
**Feature Branch**: `286-ui-copy-ia-localization-neutralization`
|
||||||
|
**Created**: 2026-05-09
|
||||||
|
**Status**: Ready
|
||||||
|
**Input**: User description: "Follow instructions in #prompt:SKILL.md with these arguments: mach 286 als nächstes"
|
||||||
|
|
||||||
|
## Spec Candidate Check *(mandatory — SPEC-GATE-001)*
|
||||||
|
|
||||||
|
- **Problem**: The repo already exposes workspace-first and environment-shaped runtime seams, including `/admin/workspaces/{workspace}/environments/{environment}` routes, environment-backed dashboards, and managed-environment domain models, but operator-facing copy still teaches the older tenant-first and Microsoft-shaped mental model. Shared shell labels, chooser titles, dashboard headings, navigation affordances, widget labels, and helper text still say `tenant`, `Managed tenants`, `All tenants`, or `Restore to Microsoft Intune` even where the surrounding workflow is already workspace-first and provider-neutral.
|
||||||
|
- **Today's failure**: Operators can be routed into environment-scoped admin surfaces while the UI still tells them they are choosing or switching a tenant. That creates cross-surface contradiction, makes the cutover look incomplete, and silently re-teaches provider-owned nouns as if they were platform-core truth.
|
||||||
|
- **User-visible improvement**: Operators choose, switch, and inspect environments with one consistent environment-first vocabulary in English and German. Provider-specific terms remain visible only where the underlying provider is the subject of the action or the supporting detail, not as the default noun for the surrounding workflow.
|
||||||
|
- **Smallest enterprise-capable version**: Neutralize operator-facing copy on the confirmed post-workspace-first admin surfaces only: shared shell labels, environment chooser and landing pages, environment dashboard headings, context chips, registry back links, workspace widgets that show current environment context, and the concrete provider-neutral helper texts on `PolicyResource`, `ViewPolicy`, `VersionsRelationManager`, and `baseline-compare-landing`. Reuse the current localization and Filament surface structure. Do not rename routes, page slugs, PHP classes, Eloquent models, capabilities, or database columns.
|
||||||
|
- **Explicit non-goals**: No route or slug rename, no PHP namespace or class rename, no RBAC or capability vocabulary rewrite from Spec `285`, no provider-capability or provider-identity work from Specs `281` and `283`, no artifact-source work from Spec `284`, no no-legacy enforcement pack from Spec `287`, no website localization, no customer-review localization expansion beyond the existing customer-facing package in Spec `275`, no auth-provider copy change such as `Sign in with Microsoft`, and no new translation framework or storage.
|
||||||
|
- **Permanent complexity imported**: One bounded operator glossary decision, one renamed localization-key family for environment-shell terms, small local template/test-id cleanup on the enumerated surfaces only, and focused feature/guard/browser proof. No new model, table, state family, or shared infrastructure is introduced.
|
||||||
|
- **Why now**: The roadmap explicitly reserves `286` inside the workspace-first cutover pack so the product does not ship workspace-first routing and managed-environment foundations with tenant-first or Microsoft-first UI language. Without this slice, the cutover remains semantically incomplete even when route and policy work are done.
|
||||||
|
- **Why not local**: The drift is shared. The same wrong nouns live in localization catalogs, page titles, shared shell helpers, widget payloads, navigation context, and selected helper text. A page-local copy patch would only move the contradiction around.
|
||||||
|
- **Approval class**: Core Enterprise
|
||||||
|
- **Red flags triggered**: multiple surfaces, terminology cleanup theme, and localization-key renaming. Defense: the slice is tightly bounded to already-real post-cutover operator flows, changes no runtime truth or schema, and directly prevents false product statements about the platform's core nouns.
|
||||||
|
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexität: 1 | Produktnähe: 2 | Wiederverwendung: 1 | **Gesamt: 10/12**
|
||||||
|
- **Decision**: approve
|
||||||
|
|
||||||
|
## Review Outcome
|
||||||
|
|
||||||
|
- **Outcome class**: acceptable-special-case
|
||||||
|
- **Workflow outcome**: keep
|
||||||
|
- **Test-governance outcome**: keep
|
||||||
|
- **Reason**: This package is a bounded vocabulary and IA convergence slice over repo-real workspace-first seams. It changes operator-facing language and disclosure only, keeps provider detail bounded, and explicitly blocks route, RBAC, schema, and framework expansion.
|
||||||
|
- **Workflow result**: Ready for implementation.
|
||||||
|
|
||||||
|
## Spec Scope Fields *(mandatory)*
|
||||||
|
|
||||||
|
- **Scope**: canonical-view
|
||||||
|
- **Primary Routes**:
|
||||||
|
- existing admin-plane environment chooser at `/admin/choose-tenant`
|
||||||
|
- existing workspace-scoped managed-environment landing surface at `/admin/workspaces/{workspace}/managed-tenants`
|
||||||
|
- existing environment dashboard route at `/admin/workspaces/{workspace}/environments/{environment}`
|
||||||
|
- existing shared shell and navigation affordances rendered on environment-scoped admin pages through `OperateHubShell` and `CanonicalNavigationContext`
|
||||||
|
- existing workspace widgets and the concrete policy-detail and baseline-compare helper surfaces at `apps/platform/app/Filament/Resources/PolicyResource.php`, `apps/platform/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`, `apps/platform/app/Filament/Resources/PolicyResource/RelationManagers/VersionsRelationManager.php`, and `apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php`
|
||||||
|
- **Data Ownership**: No new persisted truth is introduced. Existing workspace, managed-environment, operation, backup, and compare records remain authoritative. This slice only changes derived localization catalogs, page titles, surface-local display labels, widget copy, and helper text on the enumerated surfaces.
|
||||||
|
- **RBAC**:
|
||||||
|
- workspace membership remains the first isolation boundary
|
||||||
|
- managed-environment entitlement remains the second isolation boundary where the current surface requires it
|
||||||
|
- capability denials remain unchanged and keep current `403` behavior after entitlement is established
|
||||||
|
- this slice must not widen access, soften `404` boundaries, or change the current auth-plane separation
|
||||||
|
|
||||||
|
For canonical-view specs, the spec MUST define:
|
||||||
|
|
||||||
|
- **Default filter behavior when tenant-context is active**: When a managed environment is active, the shell and page copy must describe that state as the selected environment. The feature must not reset or reinterpret the current workspace or environment context.
|
||||||
|
- **Explicit entitlement checks preventing cross-tenant leakage**: Copy changes must not expose inaccessible workspaces or managed environments. Non-members and out-of-scope actors keep inherited `404` behavior; the new wording does not create new discoverability hints.
|
||||||
|
|
||||||
|
## Cross-Cutting / Shared Pattern Reuse *(mandatory when the feature touches notifications, status messaging, action links, header actions, dashboard signals/cards, alerts, navigation entry points, evidence/report viewers, or any other existing shared operator interaction family; otherwise write `N/A - no shared interaction family touched`)*
|
||||||
|
|
||||||
|
- **Cross-cutting feature?**: yes
|
||||||
|
- **Interaction class(es)**: navigation entry points, page titles, shell scope labels, context chips, action labels, empty-state copy, and provider-neutral helper text
|
||||||
|
- **Systems touched**: `apps/platform/lang/en/localization.php`, `apps/platform/lang/de/localization.php`, `apps/platform/lang/en/baseline-compare.php`, `apps/platform/lang/de/baseline-compare.php`, `ChooseTenant`, `ManagedTenantsLanding`, `TenantDashboard`, `OperateHubShell`, `CanonicalNavigationContext`, dashboard/workspace widget views, `PolicyResource`, `ViewPolicy`, `VersionsRelationManager`, and `baseline-compare-landing`
|
||||||
|
- **Existing pattern(s) to extend**: current locale resolution and translation catalogs, current workspace-context shell, current Filament page-title pattern, current dashboard heading pattern, and the existing provider-detail disclosure pattern where provider-owned terms are already shown as secondary context
|
||||||
|
- **Shared contract / presenter / builder / renderer to reuse**: `localization.shell.*`, `localization.dashboard.*`, `WorkspaceContext`, `OperateHubShell`, `CanonicalNavigationContext`, existing widget view-models, and the current Filament page/view translation usage
|
||||||
|
- **Why the existing shared path is sufficient or insufficient**: the infrastructure already exists. The gap is that those shared paths still carry the wrong nouns. `286` should converge them, not introduce a second glossary or page-local override system.
|
||||||
|
- **Allowed deviation and why**: none. Hardcoded page-local neutralization or alias-only fallback keys would extend the drift instead of resolving it.
|
||||||
|
- **Consistency impact**: environment chooser labels, managed-environment landing labels, dashboard headings, shell scope labels, registry return links, workspace widget context labels, and selected provider-neutral helper text must all use the same primary nouns in both supported locales.
|
||||||
|
- **Review focus**: reviewers must block any new tenant-first copy on in-scope operator surfaces, any new default-visible Microsoft-owned noun where the platform noun is sufficient, and any auth-provider label being neutralized even though the provider is genuinely the subject.
|
||||||
|
|
||||||
|
## OperationRun UX Impact *(mandatory when the feature creates, queues, deduplicates, resumes, blocks, completes, or deep-links to an `OperationRun`; otherwise write `N/A - no OperationRun start or link semantics touched`)*
|
||||||
|
|
||||||
|
N/A - this slice may rename environment context labels inside existing widgets or operation summaries, but it does not change `OperationRun` start, completion, deep-link, or notification semantics.
|
||||||
|
|
||||||
|
## Provider Boundary / Platform Core Check *(mandatory when the feature changes shared provider/platform seams, identity scope, governed-subject taxonomy, compare strategy selection, provider connection descriptors, or operator vocabulary that may leak provider-specific semantics into platform-core truth; otherwise write `N/A - no shared provider/platform boundary touched`)*
|
||||||
|
|
||||||
|
- **Shared provider/platform boundary touched?**: yes
|
||||||
|
- **Boundary classification**: mixed
|
||||||
|
- **Seams affected**: platform-core operator nouns for managed environments and workspace-scoped shell context; default helper text for provider actions on restore/capture or baseline-compare entry surfaces; nested provider detail wording on those same surfaces
|
||||||
|
- **Neutral platform terms preserved or introduced**: `workspace`, `environment`, `managed environment` for registry disambiguation only, `provider`, `environment scope`, `selected environment`, `all environments`
|
||||||
|
- **Provider-specific semantics retained and why**: `Sign in with Microsoft`, `Microsoft not configured`, explicit provider names in secondary detail, and provider-owned diagnostic copy such as Graph-specific explanations remain intact because the provider itself is the subject in those contexts.
|
||||||
|
- **Why this does not deepen provider coupling accidentally**: the slice moves default-visible copy away from Microsoft-specific nouns and keeps provider labels only where the provider genuinely owns the action or the evidence.
|
||||||
|
- **Follow-up path**: broader no-legacy enforcement belongs to Spec `287`; broader provider-specific surface cleanup or auth-provider wording changes belong to later provider-owned work.
|
||||||
|
|
||||||
|
## UI / Surface Guardrail Impact *(mandatory when operator-facing surfaces are changed; otherwise write `N/A`)*
|
||||||
|
|
||||||
|
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||||
|
|---|---|---|---|---|---|---|
|
||||||
|
| Environment chooser page | yes | Native Filament page + existing Blade view | chooser labels, empty state, primary action text | page, view, localization key | no | current slug and class name remain unchanged |
|
||||||
|
| Managed environments landing page | yes | Native Filament page + existing Blade view | landing title, empty state, selection CTA, supporting text | page, view, localization key | no | registry purpose stays the same; nouns change only |
|
||||||
|
| Environment dashboard heading and context chips | yes | Mixed native Filament page + existing widget views | dashboard title, context chip labels, current scope disclosure | page, widget view, localization key | no | no new widget family or card pattern |
|
||||||
|
| Shared operate-hub shell and navigation return affordances | yes | Native Filament header action + shared navigation helper | scope label, return label, registry back-link wording | shell, header action, navigation helper | no | no change to route targets or action hierarchy |
|
||||||
|
| Policy detail capture/restore helper text and baseline-compare RBAC summary heading | yes | Native Filament actions + existing Blade section | default action/helper text, secondary provider detail, summary heading | modal copy, helper text, section heading, localization key | no | provider-owned detail remains nested and explicit |
|
||||||
|
|
||||||
|
## Decision-First Surface Role *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Environment chooser page | Primary Decision Surface | Decide which environment to enter after workspace selection | `Choose environment`, active-availability wording, one selection CTA | deeper lifecycle or operability detail remains secondary | Primary because this is the first place the operator commits to an environment context | follows workspace-first entry workflow | removes mental translation from route/context into UI |
|
||||||
|
| Managed environments landing page | Primary Decision Surface | Decide which managed environment to inspect or administer | registry title, current workspace context, environment labels | deeper lifecycle or inactive detail remains lower priority | Primary because it is the registry surface for this context | matches post-workspace browsing flow | removes duplicate tenant terminology from the environment registry |
|
||||||
|
| Environment dashboard heading and context chips | Secondary Context Surface | Confirm the currently selected environment before acting | environment title, current posture pill, selected environment chip | deeper diagnostic detail remains on the page body | Secondary because the decision to enter the environment has already happened | supports orientation inside environment pages | avoids relearning tenant-first nouns after selection |
|
||||||
|
| Shared operate-hub shell and navigation return affordances | Secondary Context Surface | Confirm scope and navigate back to the correct registry | environment scope label, return affordance | raw route or nav payload remains hidden | Secondary because it orients an existing workflow rather than starting one | keeps context coherent across shared admin pages | removes noisy tenant-vs-environment translation work |
|
||||||
|
| Policy detail capture/restore helper text and baseline-compare RBAC summary heading | Tertiary Evidence / Diagnostics | Confirm what action targets the environment versus the provider | neutral primary action label and brief helper copy, plus one provider-neutral summary heading | provider-specific detail is disclosed secondarily | Tertiary because it supports a flow that already has a primary page context | follows current admin action flow | prevents provider name from competing with the actual operator decision |
|
||||||
|
|
||||||
|
## Audience-Aware Disclosure *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|
||||||
|
|---|---|---|---|---|---|---|---|
|
||||||
|
| Environment chooser page | operator-MSP, support-platform | choose environment title, selected-workspace context, one selection action | lifecycle or operability explanations | raw environment IDs or debug payloads | `Choose environment` | IDs and debug semantics remain hidden | default path states the scope once and does not restate it in multiple cards |
|
||||||
|
| Managed environments landing page | operator-MSP, support-platform | managed environments title, available environment rows, current workspace context | archived or unavailable reasons | raw metadata | `Open environment` | raw metadata remains secondary | registry title and row labels use one noun family |
|
||||||
|
| Environment dashboard heading and context chips | operator-MSP, support-platform | environment title, posture pill, selected environment chip | page-body diagnostics and operation details | raw payloads | `Open` actions stay on the page body, not in the heading | low-level diagnostics stay outside the heading | heading owns only orientation, not duplicate status narratives |
|
||||||
|
| Shared operate-hub shell and navigation return affordances | operator-MSP, support-platform | environment scope and one return label | route context or nav payloads | raw nav payloads | `Back to environment registry` or `Back to <environment>` | raw nav payloads remain hidden | shell labels do not reintroduce `tenant` beside environment copy |
|
||||||
|
| Policy detail capture/restore helper text and baseline-compare RBAC summary heading | operator-MSP, support-platform | neutral action label, concise target explanation, and provider-neutral section heading | provider-specific constraints, provider API notes | raw provider payloads | action label remains dominant | provider-owned details stay helper or secondary text | default action does not repeat provider detail as the headline |
|
||||||
|
|
||||||
|
## UI/UX Surface Classification *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Environment chooser page | Navigation / Drilldown / Context | Global-context Shell | choose an environment | explicit chooser action per row or card | forbidden | none beyond secondary helper copy | none | `/admin/choose-tenant` | `/admin/workspaces/{workspace}/environments/{environment}` | workspace context, availability | Environment | selected scope and availability | slug remains tenant-named, operator copy does not |
|
||||||
|
| Managed environments landing page | Navigation / Drilldown / Registry | Read-only registry chooser | open an environment | explicit open action on row/card | allowed if already the current pattern | secondary lifecycle detail stays below title | none | `/admin/workspaces/{workspace}/managed-tenants` | `/admin/workspaces/{workspace}/environments/{environment}` | workspace, environment lifecycle | Managed environment | registry purpose and available environments | registry route remains tenant-named, copy does not |
|
||||||
|
| Environment dashboard heading and context chips | Record / Detail / Overview | Overview-first page | confirm current environment context | page heading plus chips | n/a | page-body actions remain below heading | none | `/admin/workspaces/{workspace}/environments/{environment}` | same page | workspace, environment, posture | Environment dashboard | selected environment and posture | none |
|
||||||
|
| Shared operate-hub shell and navigation return affordances | Navigation / Drilldown / Context | Global-context Shell | return to the right registry or environment | explicit header action labels | n/a | secondary nav stays contextual | none | inherited current page route | inherited current page route | environment scope, return target | Environment scope | current scope and return target | none |
|
||||||
|
| Policy detail capture/restore helper text and baseline-compare RBAC summary heading | Record / Detail / Action | Action-supporting detail | confirm target before executing or scanning the compare summary | current action or section heading | n/a | provider detail stays helper text or secondary section | existing destructive-like actions remain unchanged | inherited current page route | inherited current page route | target environment, provider detail | Environment | default target and effect | none |
|
||||||
|
|
||||||
|
## Operator Surface Contract *(mandatory when operator-facing surfaces are changed)*
|
||||||
|
|
||||||
|
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||||
|
|---|---|---|---|---|---|---|---|---|---|---|
|
||||||
|
| Environment chooser page | Workspace operator | choose which environment to enter | chooser page | Which environment should I operate in now? | workspace context, available environments, one clear chooser label | deeper lifecycle or operability detail | availability, lifecycle | none | choose environment | none |
|
||||||
|
| Managed environments landing page | Workspace operator | open the correct managed environment | registry page | Which managed environment belongs to this workspace and is available? | workspace title, environment registry, availability cues | archived or unavailable explanations | lifecycle, availability | none | open environment | none |
|
||||||
|
| Environment dashboard heading and context chips | Environment operator | confirm the selected environment before using page actions | overview heading | Am I in the right environment? | environment name, posture pill, context chips | lower page diagnostics | posture, scope | none | inherited page actions only | none |
|
||||||
|
| Shared operate-hub shell and navigation return affordances | Environment operator | recover orientation and navigate back safely | shared shell helper | What scope am I in and where do I go back? | environment scope label and return label | raw route or nav details | scope, context | none | return affordance only | none |
|
||||||
|
| Policy detail capture/restore helper text and baseline-compare RBAC summary heading | Environment operator | confirm target and wording before execution or interpretation | action helper text / section heading | Does this action or summary target the environment or the provider detail? | neutral action label, concise target explanation, and provider-neutral summary heading | provider-specific detail and diagnostic notes | target, provider context | inherited existing action scope | inherited existing action | inherited existing dangerous actions |
|
||||||
|
|
||||||
|
## Proportionality Review *(mandatory when structural complexity is introduced)*
|
||||||
|
|
||||||
|
- **New source of truth?**: no
|
||||||
|
- **New persisted entity/table/artifact?**: no
|
||||||
|
- **New abstraction?**: no
|
||||||
|
- **New enum/state/reason family?**: no
|
||||||
|
- **New cross-domain UI framework/taxonomy?**: no
|
||||||
|
- **Current operator problem**: the product already routes and scopes around environments, but the default-visible copy still teaches tenant-first and Microsoft-first operator truth.
|
||||||
|
- **Existing structure is insufficient because**: existing localization catalogs and shared helpers are exactly where the wrong nouns currently live.
|
||||||
|
- **Narrowest correct implementation**: rename the in-scope operator glossary and only the local display labels/test IDs on the enumerated surfaces, keep provider-specific nouns nested where they already belong, and leave shared payload contracts, routes, models, and RBAC unchanged.
|
||||||
|
- **Ownership cost**: updating translation keys, touching a small number of shared surfaces, and keeping guard coverage for the environment-first glossary.
|
||||||
|
- **Alternative intentionally rejected**: one-off value-only copy changes without key or view-model cleanup were rejected because they preserve the same semantic drift in shared helpers and test contracts.
|
||||||
|
- **Release truth**: current-release cutover follow-through, not future-release preparation.
|
||||||
|
|
||||||
|
### Compatibility posture
|
||||||
|
|
||||||
|
This feature assumes a pre-production environment.
|
||||||
|
|
||||||
|
Backward compatibility, translation-key aliases, route aliases, slug migration, and compatibility-specific tests are out of scope unless a later guardrail spec explicitly requires them.
|
||||||
|
|
||||||
|
Canonical replacement of the in-scope operator glossary is preferred over preserving tenant-first key families for convenience.
|
||||||
|
|
||||||
|
## Testing / Lane / Runtime Impact *(mandatory for runtime behavior changes)*
|
||||||
|
|
||||||
|
- **Test purpose / classification**: Feature, Browser
|
||||||
|
- **Validation lane(s)**: confidence, browser
|
||||||
|
- **Why this classification and these lanes are sufficient**: the slice changes rendered operator-facing copy, page titles, shared shell labels, and helper text on existing admin surfaces. Focused feature coverage plus one bounded browser smoke are the narrowest honest proof. No new heavy-governance family is justified.
|
||||||
|
- **New or expanded test families**: one localization feature family for environment terminology, one Filament surface-copy feature family for shared admin surfaces, one guard family for forbidden default-visible strings on in-scope files, and one browser smoke for the chooser-to-environment flow
|
||||||
|
- **Fixture / helper cost impact**: low to moderate because proof needs workspace context, at least one managed environment, and the existing admin surfaces only
|
||||||
|
- **Heavy-family visibility / justification**: one browser smoke only; no new heavy-governance coverage
|
||||||
|
- **Special surface test profile**: standard-native-filament, global-context-shell
|
||||||
|
- **Standard-native relief or required special coverage**: ordinary Filament feature coverage is sufficient for page titles and translation output; the shared shell and chooser flow need one explicit global-context-shell smoke
|
||||||
|
- **Reviewer handoff**: reviewers must verify that no in-scope operator surface still uses `tenant` or default-visible `Microsoft` nouns where a platform noun is sufficient, that auth-provider labels remain provider-owned where appropriate, that route targets remain unchanged, that Filament stays v5 on Livewire v4, and that provider registration remains in `apps/platform/bootstrap/providers.php`
|
||||||
|
- **Budget / baseline / trend impact**: contained feature-local increase only
|
||||||
|
- **Escalation needed**: `follow-up-spec` if implementation uncovers route-slug renames, RBAC capability renames, or broader provider-surface cleanup beyond the bounded glossary slice
|
||||||
|
- **Active feature PR close-out entry**: Smoke Coverage
|
||||||
|
- **Planned validation commands**:
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Localization/EnvironmentContextTerminologyTest.php tests/Feature/Filament/EnvironmentContextSurfaceCopyTest.php tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec286EnvironmentCopyNeutralizationSmokeTest.php`
|
||||||
|
- `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
|
||||||
|
## Scope Boundaries
|
||||||
|
|
||||||
|
### In Scope
|
||||||
|
|
||||||
|
- environment-first terminology on the confirmed shared shell labels, chooser and landing titles, dashboard titles, context chips, registry return labels, workspace widgets, and selected helper copy
|
||||||
|
- English and German localization catalog updates for the in-scope operator glossary
|
||||||
|
- canonically replacing the in-scope translation-key family and only local display labels/test IDs owned by the enumerated surfaces; shared cross-surface payload contracts outside those surfaces remain unchanged
|
||||||
|
- provider-neutral default action/helper text on the exact PolicyResource sync/capture surfaces, the policy versions restore action, and the baseline-compare landing RBAC summary heading where the provider is supporting context rather than the primary noun
|
||||||
|
- focused guard and browser coverage that pins the environment-first glossary on the in-scope admin surfaces
|
||||||
|
|
||||||
|
### Non-Goals
|
||||||
|
|
||||||
|
- route, slug, or PHP class renaming
|
||||||
|
- RBAC or capability vocabulary changes
|
||||||
|
- provider-identity or provider-capability rewrites
|
||||||
|
- artifact-source or report-surface taxonomy work
|
||||||
|
- website localization or customer-review localization beyond Spec `275`
|
||||||
|
- auth-provider wording such as `Sign in with Microsoft`
|
||||||
|
- new localization infrastructure, translation storage, or asset changes
|
||||||
|
|
||||||
|
## Candidate Selection Rationale
|
||||||
|
|
||||||
|
- **Selected candidate**: `286 - UI Copy, IA & Localization Neutralization`
|
||||||
|
- **Source locations**:
|
||||||
|
- `docs/product/roadmap.md` under the workspace-first managed-environment core cutover pack
|
||||||
|
- explicit user-directed manual promotion for reserved slot `286`
|
||||||
|
- **Why selected**: the user explicitly requested the next reserved slot `286`, the slot is present in the roadmap, and repo truth confirms the smallest remaining gap is no longer route or policy plumbing but the operator-facing language that still teaches the retired tenant-first model.
|
||||||
|
- **Why close alternatives were deferred**:
|
||||||
|
- Spec `287` remains the guardrail and no-legacy enforcement pack and should not be absorbed into this bounded copy slice
|
||||||
|
- broader customer-facing localization remains owned by Spec `275`
|
||||||
|
- route or slug renames remain outside this bounded neutralization slice
|
||||||
|
- broader provider-surface cleanup remains separate from the shared operator glossary cutover
|
||||||
|
- **Smallest viable implementation slice**: replace in-scope tenant-first and default-visible Microsoft-first copy on the confirmed workspace-first admin surfaces, canonically replace the in-scope translation-key family, update only local display labels/test IDs owned by those surfaces, and keep shared payload contracts, routes, models, RBAC, and provider-owned detail unchanged.
|
||||||
|
- **Documented deviations from raw candidate wording**:
|
||||||
|
- current repo truth already exposes environment-shaped route and dashboard seams, so `286` is not a greenfield IA redesign
|
||||||
|
- auth-provider labels remain explicitly provider-owned and are intentionally not neutralized in this slice
|
||||||
|
- internal class and route symbols may remain tenant-named while operator-facing copy becomes environment-first
|
||||||
|
|
||||||
|
## Completed-Spec Guardrail Result
|
||||||
|
|
||||||
|
- `specs/279-workspace-managed-environment-core/` contains implementation-close-out history and remains historical prerequisite context only.
|
||||||
|
- `specs/280-workspace-tenancy-environment-routing/` is `Status: Ready` and remains adjacent prepared context only.
|
||||||
|
- `specs/281-provider-connection-scope/` is `Status: Ready` and remains adjacent prepared context only.
|
||||||
|
- `specs/282-governance-artifact-retargeting/` is `Status: Prepared - blocked by Spec 280 runtime prerequisite` and remains adjacent prepared context only.
|
||||||
|
- `specs/283-provider-capability-registry/` is `Status: Ready` and remains adjacent prepared context only.
|
||||||
|
- `specs/285-workspace-rbac-environment-access/` currently has a spec-level status of `Blocked by external prerequisites` while its tasks artifact already carries a review outcome of `implemented-and-validated`; `286` treats that split as adjacent context only and remains independent as long as it does not absorb any RBAC renaming.
|
||||||
|
- `specs/275-customer-facing-localization-adoption/` remains related localization context only for glossary discipline and does not get refreshed here.
|
||||||
|
- The target package `specs/286-ui-copy-ia-localization-neutralization/` did not exist before this prep run and is the sole new package created here.
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- current implementation branches should already contain the workspace-first route and environment dashboard seams prepared by Spec `280`
|
||||||
|
- current provider-boundary seams from Spec `281` and provider-capability seams from Spec `283` must remain intact so `286` can keep provider-owned detail secondary instead of redefining it
|
||||||
|
- any runtime branch used for later implementation must keep RBAC terminology bounded to Spec `285`; `286` does not require `285` close-out to be rewritten or fully normalized, but it must not absorb role or capability renaming
|
||||||
|
|
||||||
|
## Assumptions
|
||||||
|
|
||||||
|
- `Environment` is the primary operator noun for chooser, shell, dashboard, and default action copy.
|
||||||
|
- `Managed environment` may remain on registry or disambiguation surfaces where it clarifies the object against a workspace.
|
||||||
|
- Provider names remain visible only when the provider itself is the subject, such as auth-provider actions or explicit secondary detail.
|
||||||
|
- Internal PHP classes, route names, slugs, and database columns may remain tenant-shaped in this slice.
|
||||||
|
- English and German remain the only supported locales in scope.
|
||||||
|
|
||||||
|
## Risks
|
||||||
|
|
||||||
|
- Partial glossary cleanup can leave environment-first and tenant-first nouns side by side if the shared shell or widget files are not all updated together.
|
||||||
|
- Neutralizing provider helper text too aggressively can hide when Microsoft is genuinely the acting system on an action surface.
|
||||||
|
- Renaming translation keys without updating all in-scope call sites can break copy rendering or produce raw keys.
|
||||||
|
- Scope pressure can try to turn the slice into route renaming, RBAC renaming, or broader provider-surface cleanup.
|
||||||
|
|
||||||
|
## Follow-Up Candidates Explicitly Kept Out of Scope
|
||||||
|
|
||||||
|
- `287 - Cutover Quality Gates & No-Legacy Enforcement`
|
||||||
|
- broader provider-owned surface cleanup beyond the in-scope helper text and headings
|
||||||
|
- route-slug or PHP class renaming for tenant-shaped symbols
|
||||||
|
- website localization and customer-review localization beyond the current `275` package
|
||||||
|
- broader RBAC capability or role copy convergence beyond the bounded environment-first glossary
|
||||||
|
|
||||||
|
## User Scenarios & Testing *(mandatory)*
|
||||||
|
|
||||||
|
### User Story 1 - Choose an environment with environment-first language (Priority: P1)
|
||||||
|
|
||||||
|
As a workspace operator, I want the chooser and registry surfaces to tell me I am selecting an environment so the UI matches the workspace-first route and object model I am already using.
|
||||||
|
|
||||||
|
**Why this priority**: This is the first operator-facing moment where the cutover is visible, and it is where the current terminology drift is most confusing.
|
||||||
|
|
||||||
|
**Independent Test**: Open the chooser and managed-environment landing pages for a workspace, then verify the titles, empty states, selection actions, and supporting text use environment-first language in English and German while the underlying routes stay unchanged.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** a workspace has selectable managed environments, **When** the operator opens the chooser or landing page, **Then** the page says `Choose environment` or `Managed environments` instead of `Choose tenant` or `Managed tenants`.
|
||||||
|
2. **Given** a workspace has no active selectable managed environments, **When** the operator opens the chooser, **Then** the empty-state wording says no active environments are available and does not mention tenants.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 2 - Stay oriented inside an environment-scoped admin surface (Priority: P1)
|
||||||
|
|
||||||
|
As an environment operator, I want the dashboard heading, context chips, and shared shell labels to confirm that I am inside an environment scope so I do not have to reinterpret tenant-first labels on every page.
|
||||||
|
|
||||||
|
**Why this priority**: The cutover fails if the operator gets environment-shaped routes but tenant-shaped shell and heading copy.
|
||||||
|
|
||||||
|
**Independent Test**: Open an environment dashboard and a shared operate-hub page, then verify the heading, context chip, scope label, and registry return affordance all use the same environment-first glossary.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** an entitled managed environment is active, **When** the operator opens the dashboard, **Then** the title and context chip speak about the selected environment, not the selected tenant.
|
||||||
|
2. **Given** an operate-hub page is rendered inside an active environment context, **When** the operator reads the shell labels, **Then** the shell says `Environment scope` or `All environments` and the return link points back to the environment registry without tenant-first wording.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### User Story 3 - Read provider actions with neutral default copy and explicit provider detail (Priority: P2)
|
||||||
|
|
||||||
|
As an environment operator, I want restore/capture or baseline-compare helper text to use a neutral default noun so the product explains the target environment clearly while still preserving provider detail where it genuinely matters.
|
||||||
|
|
||||||
|
**Why this priority**: Default-visible helper copy currently lets Microsoft-specific nouns dominate actions that are otherwise framed as environment-scoped admin workflows.
|
||||||
|
|
||||||
|
**Independent Test**: Open `PolicyResource` sync/capture surfaces, the policy versions restore action, and the baseline-compare landing RBAC summary section, then verify the default-visible action/helper text or section heading uses provider-neutral wording while provider-specific detail remains secondary and explicit.
|
||||||
|
|
||||||
|
**Acceptance Scenarios**:
|
||||||
|
|
||||||
|
1. **Given** the `VersionsRelationManager` restore action or `ViewPolicy` capture snapshot modal is shown on an environment-scoped policy surface, **When** the operator reads the action and helper text, **Then** the default headline speaks about the environment or provider action neutrally rather than `Restore to Microsoft Intune`.
|
||||||
|
2. **Given** `baseline-compare-landing` shows the RBAC summary section or a provider-specific note is required on the policy surfaces, **When** the operator opens the same surface, **Then** the heading or helper text remains provider-neutral by default and the provider name stays available only as secondary detail.
|
||||||
|
|
||||||
|
### Edge Cases
|
||||||
|
|
||||||
|
- A workspace has no active selectable managed environments and the shell must still avoid tenant-first nouns.
|
||||||
|
- The underlying route or class remains `choose-tenant` or `TenantDashboard`, but operator-facing copy must still be environment-first.
|
||||||
|
- A provider-owned auth action such as `Sign in with Microsoft` must remain provider-specific and must not be neutralized accidentally.
|
||||||
|
- A helper string needs both a neutral primary noun and a provider-specific secondary explanation without sounding contradictory.
|
||||||
|
- A widget or view still passes `tenant_label` in a view-model contract even though the operator-facing label must become `environment_label`.
|
||||||
|
|
||||||
|
## Requirements
|
||||||
|
|
||||||
|
**Constitution alignment (required):** `286` changes operator-facing vocabulary and shared surface disclosure only. It introduces no Graph calls, no write/change workflow, no queue/background work, and no new persisted truth.
|
||||||
|
|
||||||
|
**Constitution alignment (PROP-001 / ABSTR-001 / PROV-001):** This package reuses existing localization and shell infrastructure, keeps provider-specific semantics out of platform-core default-visible copy, and forbids a broader IA or translation framework.
|
||||||
|
|
||||||
|
**Constitution alignment (XCUT-001 / UI-FIL-001 / DECIDE-001):** The slice reuses native Filament pages and current widget shells, keeps one dominant chooser or navigation action per surface, and does not invent a second glossary or a custom design system.
|
||||||
|
|
||||||
|
### Functional Requirements
|
||||||
|
|
||||||
|
- **FR-001**: In-scope chooser and registry surfaces MUST use `environment` or `managed environment` as the primary operator noun instead of `tenant`.
|
||||||
|
- **FR-002**: Shared shell labels MUST use `Environment scope`, `All environments`, and equivalent environment-first terms where the active managed-environment context is the primary truth.
|
||||||
|
- **FR-003**: Environment dashboard headings and context chips MUST describe the active scope as an environment.
|
||||||
|
- **FR-004**: Registry return links and related navigation affordances MUST point to the same routes they do today while using environment-first wording.
|
||||||
|
- **FR-005**: English and German localization catalogs MUST expose the same in-scope environment-first glossary.
|
||||||
|
- **FR-006**: The in-scope localization-key family and only the local display labels/test IDs owned by the enumerated surfaces that still encode tenant-first operator nouns MUST be renamed or replaced canonically, not aliased.
|
||||||
|
- **FR-007**: Provider-neutral action/helper copy MUST become the default-visible wording on the exact `PolicyResource`, `ViewPolicy`, `VersionsRelationManager`, and `baseline-compare-landing` surfaces named in this spec.
|
||||||
|
- **FR-008**: Provider-specific names MUST remain available only where the provider is genuinely the subject of the auth flow or the secondary explanatory detail.
|
||||||
|
- **FR-009**: Route targets, slugs, PHP class names, database columns, model names, and capability names MUST remain unchanged in this slice.
|
||||||
|
- **FR-010**: This slice MUST NOT change RBAC outcomes, route access, or `404` versus `403` semantics.
|
||||||
|
- **FR-011**: The implementation MUST NOT introduce a new localization framework, storage, asset pipeline change, or panel configuration change.
|
||||||
|
- **FR-012**: The implementation MUST include focused feature/guard coverage for the in-scope environment-first glossary and one browser smoke for the chooser-to-environment flow.
|
||||||
|
|
||||||
|
### UX / IA Requirements
|
||||||
|
|
||||||
|
- **UX-001**: `Environment` is the default operator noun on chooser, shell, dashboard, and default action copy.
|
||||||
|
- **UX-002**: `Managed environment` is reserved for registry/disambiguation surfaces where the object needs to be distinguished from the workspace.
|
||||||
|
- **UX-003**: The chooser and landing surfaces keep one dominant selection action and must not duplicate the same scope summary across multiple visible regions.
|
||||||
|
- **UX-004**: Dashboard headings and shell labels stay orientation-first and must not compete with page-body actions.
|
||||||
|
- **UX-005**: Provider-specific terms stay secondary where a platform noun is sufficient for the first operator decision.
|
||||||
|
|
||||||
|
### Localization Requirements
|
||||||
|
|
||||||
|
- **L10N-001**: English and German keys for the in-scope shell and dashboard glossary remain parity-matched.
|
||||||
|
- **L10N-002**: Raw translation keys must not leak on in-scope surfaces after the key-family neutralization.
|
||||||
|
- **L10N-003**: Auth-provider copy such as `Sign in with Microsoft` remains out of scope and unchanged.
|
||||||
|
|
||||||
|
### Non-Functional Requirements
|
||||||
|
|
||||||
|
- **NFR-001**: The slice remains implementation-bounded to current admin surfaces and avoids broader website or customer-review localization.
|
||||||
|
- **NFR-002**: Guard coverage must stay limited to the in-scope files and must not become a repo-wide blanket string ban.
|
||||||
|
- **NFR-003**: No deploy or asset changes are required beyond the existing platform flow.
|
||||||
|
|
||||||
|
## Acceptance Criteria
|
||||||
|
|
||||||
|
- The chooser and managed-environment landing pages no longer show tenant-first nouns in their primary titles, CTAs, or empty-state copy.
|
||||||
|
- Environment dashboard headings, context chips, and shared shell labels use environment-first wording consistently.
|
||||||
|
- In-scope provider helper text uses neutral default copy while retaining provider-specific secondary detail where needed.
|
||||||
|
- English and German translations are aligned for the in-scope glossary and no raw translation keys appear.
|
||||||
|
- Routes, slugs, classes, RBAC behavior, and provider registration remain unchanged.
|
||||||
|
|
||||||
|
## Success Criteria
|
||||||
|
|
||||||
|
- Operators can move from workspace selection into an environment-scoped page without encountering tenant-first copy on the in-scope surfaces.
|
||||||
|
- The platform no longer teaches `tenant` or default-visible `Microsoft` nouns as the first explanation of an environment-scoped admin workflow.
|
||||||
|
- The bounded test and browser proof succeeds without widening into adjacent specs.
|
||||||
|
|
||||||
|
## Open Questions
|
||||||
|
|
||||||
|
- None. This package resolves the primary noun choice as `Environment`, keeps `Managed environment` for registry disambiguation only, and keeps auth-provider labels explicitly out of scope.
|
||||||
212
specs/286-ui-copy-ia-localization-neutralization/tasks.md
Normal file
212
specs/286-ui-copy-ia-localization-neutralization/tasks.md
Normal file
@ -0,0 +1,212 @@
|
|||||||
|
---
|
||||||
|
description: "Task list for UI Copy, IA & Localization Neutralization"
|
||||||
|
---
|
||||||
|
|
||||||
|
# Tasks: UI Copy, IA & Localization Neutralization
|
||||||
|
|
||||||
|
**Input**: Design documents from `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/`
|
||||||
|
**Prerequisites**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/plan.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/spec.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/checklists/requirements.md` (required), `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/research.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/data-model.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/contracts/ui-copy-ia-localization-neutralization.logical.openapi.yaml`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/quickstart.md`
|
||||||
|
|
||||||
|
**Review Artifact**: `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/checklists/requirements.md` is the outcome-of-record for the review outcome class, workflow outcome, and test-governance outcome. If implementation widens into route/slug/class renaming, auth-provider wording changes, RBAC vocabulary changes, or broader provider-surface cleanup, update that artifact before continuing.
|
||||||
|
|
||||||
|
**Tests**: Required (Pest) for runtime behavior changes. Keep proof in the existing `confidence` lane plus one bounded `browser` smoke only because this slice changes rendered operator-facing copy, shared shell labels, chooser semantics, and helper-text disclosure on existing admin surfaces.
|
||||||
|
**Operations**: No new `OperationRun`, queue, remote call, or background workflow is introduced.
|
||||||
|
**RBAC**: Workspace and managed-environment entitlement behavior remains unchanged. Reuse existing policy and capability seams; do not add raw capability strings or role-name checks.
|
||||||
|
**Shared Pattern Reuse**: Reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/baseline-compare.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/baseline-compare.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/ChooseTenant.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/TenantDashboard.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/OperateHub/OperateHubShell.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Navigation/CanonicalNavigationContext.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/PolicyResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/PolicyResource/RelationManagers/VersionsRelationManager.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php`. Do not add a new localization framework, alias-only translation layer, route/slug rename, or provider-cleanup sweep.
|
||||||
|
**Filament / Panel Guardrails**: Filament remains v5 on Livewire v4. Provider registration remains unchanged in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/bootstrap/providers.php`. No new globally-searchable resource, no new panel, and no asset-strategy change are allowed in this slice.
|
||||||
|
**Organization**: Tasks are grouped by user story so chooser/registry copy, dashboard/shell copy, and provider-neutral helper copy remain independently testable after the shared glossary is settled.
|
||||||
|
**Review Outcome**: `acceptable-special-case`
|
||||||
|
**Workflow Outcome**: `keep`
|
||||||
|
**Test-governance Outcome**: `keep`
|
||||||
|
|
||||||
|
## Test Governance Checklist
|
||||||
|
|
||||||
|
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
|
||||||
|
- [x] New or changed tests stay in the smallest honest family, and any browser addition remains explicit and bounded.
|
||||||
|
- [x] Shared helpers, widgets, translation catalogs, and environment context defaults stay cheap by default.
|
||||||
|
- [x] Planned validation commands cover the change without pulling in unrelated lane cost.
|
||||||
|
- [x] Surface test profile stays explicit: `standard-native-filament` for page-title and localization output, `global-context-shell` for shell labels and chooser flow.
|
||||||
|
- [x] Dominant CTA, diagnostics-second ordering, secondary provider detail, and no duplicate visible scope summary are verified explicitly on the chooser, landing, dashboard, shell, and pinned US3 helper surfaces.
|
||||||
|
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active feature package or PR close-out.
|
||||||
|
|
||||||
|
## Phase 1: Setup (Shared Context)
|
||||||
|
|
||||||
|
**Purpose**: Lock the bounded glossary slice, explicit non-goals, and proving lanes before runtime edits begin.
|
||||||
|
|
||||||
|
- [x] T001 Review the bounded slice, explicit non-goals, completed-spec guardrails, and repo-fit outcome in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/spec.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/plan.md`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/checklists/requirements.md`
|
||||||
|
- [x] T002 [P] Review the glossary decisions, key-family replacement rules, and provider-detail boundary in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/research.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/data-model.md`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/contracts/ui-copy-ia-localization-neutralization.logical.openapi.yaml`
|
||||||
|
- [x] T003 [P] Confirm the focused Sail/Pest validation commands and bounded browser smoke in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/quickstart.md`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Localization/`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Filament/`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Guards/`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Browser/`
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 2: Foundational (Blocking Prerequisites)
|
||||||
|
|
||||||
|
**Purpose**: Settle the shared environment-first glossary and bounded provider-detail rule before user-story implementation starts.
|
||||||
|
|
||||||
|
**Critical**: No user-story work should begin until this phase is complete.
|
||||||
|
|
||||||
|
- [x] T004 [P] Inventory the in-scope tenant-first and default-visible provider-first strings across `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/baseline-compare.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/baseline-compare.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/ChooseTenant.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/TenantDashboard.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/OperateHub/OperateHubShell.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Navigation/CanonicalNavigationContext.php`, and the in-scope widget views so later story work reuses one glossary only
|
||||||
|
- [x] T005 [P] Add or extend bounded guard coverage in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php` for in-scope default-visible strings such as `tenant scope`, `select tenant`, `all tenants`, `managed tenants`, and `restore to Microsoft Intune`, while explicitly excluding auth-provider copy like `Sign in with Microsoft`
|
||||||
|
- [x] T006 Canonically replace the in-scope translation-key family and only the local display labels/test IDs owned by the enumerated surfaces with environment-first names in the smallest shared seams first, without adding alias keys or broad repo-wide grep bans
|
||||||
|
|
||||||
|
**Checkpoint**: The glossary inventory, guard boundaries, and canonical replacement rules are fixed before surface-specific story work begins.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 3: User Story 1 - Choose an environment with environment-first language (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Let workspace operators choose and open environments from chooser/registry surfaces whose copy matches the current workspace-first route model.
|
||||||
|
|
||||||
|
**Independent Test**: Open the chooser and managed-environment landing pages and verify the titles, empty states, CTAs, and supporting text use environment-first wording in English and German while the routes stay unchanged.
|
||||||
|
|
||||||
|
### Tests for User Story 1
|
||||||
|
|
||||||
|
- [x] T007 [P] [US1] Add or extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Localization/EnvironmentContextTerminologyTest.php` for chooser and landing titles, empty states, search placeholders, and selection actions in English and German
|
||||||
|
|
||||||
|
### Implementation for User Story 1
|
||||||
|
|
||||||
|
- [x] T008 [US1] Replace the in-scope chooser and landing glossary keys in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php` so `tenant_*` admin-shell terms become `environment_*` equivalents
|
||||||
|
- [x] T009 [US1] Apply the new glossary to `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/ChooseTenant.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/choose-tenant.blade.php` so the chooser title, CTA, empty state, and search placeholder become environment-first without changing the route slug
|
||||||
|
- [x] T010 [US1] Apply the same glossary to `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/workspaces/managed-tenants-landing.blade.php` so the registry title and opening affordances say `Managed environments` instead of `Managed tenants`
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 1 is independently functional when chooser and registry surfaces no longer teach tenant-first admin nouns.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 4: User Story 2 - Stay oriented inside environment-scoped admin surfaces (Priority: P1)
|
||||||
|
|
||||||
|
**Goal**: Let operators confirm the active environment through consistent dashboard, shell, and navigation labels once they enter an environment-scoped page.
|
||||||
|
|
||||||
|
**Independent Test**: Open an environment dashboard and one shared operate-hub page, then verify the dashboard heading, context chips, shell scope label, and return affordance all use the same environment-first glossary.
|
||||||
|
|
||||||
|
### Tests for User Story 2
|
||||||
|
|
||||||
|
- [x] T011 [P] [US2] Add or extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Filament/EnvironmentContextSurfaceCopyTest.php` for dashboard heading keys, shell scope labels, registry return links, and workspace widget environment labels
|
||||||
|
|
||||||
|
### Implementation for User Story 2
|
||||||
|
|
||||||
|
- [x] T012 [US2] Update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Pages/TenantDashboard.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/widgets/dashboard/tenant-dashboard-context-chips.blade.php` so headings and chips speak about the selected environment
|
||||||
|
- [x] T013 [US2] Update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/OperateHub/OperateHubShell.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Support/Navigation/CanonicalNavigationContext.php` so shell scope labels and registry back links become environment-first while route targets stay unchanged
|
||||||
|
- [x] T014 [US2] Update `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/widgets/workspace/workspace-needs-attention.blade.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/widgets/workspace/workspace-recent-operations.blade.php` so directly rendered environment labels become environment-first without widening shared workspace overview payload contracts
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 2 is independently functional when the shell and dashboard no longer make operators reinterpret environment routes as tenant routes.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 5: User Story 3 - Read provider actions with neutral default copy and explicit provider detail (Priority: P2)
|
||||||
|
|
||||||
|
**Goal**: Let operators read a neutral environment-first default action/helper path on the bounded restore/capture/baseline-compare surfaces while still preserving provider-specific secondary detail.
|
||||||
|
|
||||||
|
**Independent Test**: Open one in-scope restore/capture or baseline-compare entry surface and verify the default-visible headline/helper text is neutralized while provider-specific secondary detail remains explicit and auth-provider wording stays unchanged.
|
||||||
|
|
||||||
|
### Tests for User Story 3
|
||||||
|
|
||||||
|
- [x] T015 [P] [US3] Extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php` for the exact `PolicyResource`, `ViewPolicy`, `VersionsRelationManager`, and `baseline-compare-landing` helper surfaces, plus the explicit exclusion of auth-provider labels
|
||||||
|
- [x] T016 [P] [US3] Add or extend `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/tests/Browser/Spec286EnvironmentCopyNeutralizationSmokeTest.php` for the chooser-to-environment path plus the pinned policy-detail/helper and baseline-compare summary surfaces
|
||||||
|
|
||||||
|
### Implementation for User Story 3
|
||||||
|
|
||||||
|
- [x] T017 [US3] Update the bounded provider-neutral helper strings in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/localization.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/en/baseline-compare.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/lang/de/baseline-compare.php` so `PolicyResource`, `ViewPolicy`, `VersionsRelationManager`, and `baseline-compare-landing` stop using provider-first nouns as their default-visible headings or helper text where a platform noun is sufficient
|
||||||
|
- [x] T018 [US3] Reuse `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/PolicyResource.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/PolicyResource/Pages/ViewPolicy.php`, `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/app/Filament/Resources/PolicyResource/RelationManagers/VersionsRelationManager.php`, and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php` so provider detail stays secondary and no auth-provider wording is changed
|
||||||
|
|
||||||
|
**Checkpoint**: User Story 3 is independently functional when provider-neutral helper text no longer dominates the first operator decision on the bounded action surfaces.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Phase 6: Polish & Cross-Cutting Validation
|
||||||
|
|
||||||
|
**Purpose**: Run the canonical proof commands, format touched files, and keep any discovered spillover explicit instead of absorbing it into this slice.
|
||||||
|
|
||||||
|
- [x] T019 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Localization/EnvironmentContextTerminologyTest.php tests/Feature/Filament/EnvironmentContextSurfaceCopyTest.php tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php` exactly as recorded in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/plan.md` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/quickstart.md`
|
||||||
|
- [x] T020 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Browser/Spec286EnvironmentCopyNeutralizationSmokeTest.php` exactly as recorded in `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/plan.md` and `/Users/ahmeddarrazi/Documents/projects/wt-plattform/specs/286-ui-copy-ia-localization-neutralization/quickstart.md`
|
||||||
|
- [x] T021 Run `export PATH="/bin:/usr/bin:/usr/local/bin:$PATH" && cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent`
|
||||||
|
- [x] T022 Review the touched localization files, page classes, shell helpers, widget views, pinned policy/baseline-compare helper surfaces, and the review artifact to confirm no route/slug/class/capability changed, chooser and landing still keep one dominant CTA, dashboard/shell remain orientation-first, provider detail stays secondary, no duplicate visible scope summary was introduced, no auth-provider wording was neutralized, and any broader provider-surface cleanup was recorded as follow-up instead of absorbed here
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Dependencies & Execution Order
|
||||||
|
|
||||||
|
### Phase Dependencies
|
||||||
|
|
||||||
|
- **Phase 1 (Setup)**: no dependencies; start immediately.
|
||||||
|
- **Phase 2 (Foundational)**: depends on Phase 1 and blocks all user stories until glossary and guard boundaries are settled.
|
||||||
|
- **Phase 3 (US1)**: depends on Phase 2 and delivers the first environment-first operator surface increment.
|
||||||
|
- **Phase 4 (US2)**: depends on Phase 2 and should follow US1 because shell and dashboard copy should consume the final chooser vocabulary.
|
||||||
|
- **Phase 5 (US3)**: depends on Phase 2 and is safest after US1 and US2 because provider-neutral helper copy should align with the final environment-first glossary.
|
||||||
|
- **Phase 6 (Polish)**: depends on all implemented stories.
|
||||||
|
|
||||||
|
### User Story Dependencies
|
||||||
|
|
||||||
|
- **US1 (P1)**: first independently testable increment once the glossary and guard boundaries exist.
|
||||||
|
- **US2 (P1)**: independently testable after Phase 2, but should merge after US1 because shell and dashboard labels must not diverge from chooser vocabulary.
|
||||||
|
- **US3 (P2)**: independently testable after Phase 2, but should merge after US1 and US2 because helper text should inherit the final platform nouns.
|
||||||
|
|
||||||
|
### Within Each User Story
|
||||||
|
|
||||||
|
- Write the listed Pest coverage first and make it fail for the intended gap before runtime implementation.
|
||||||
|
- Reuse the existing localization and shell seams before adding any new helper.
|
||||||
|
- Re-run the narrowest relevant validation command after each story checkpoint before moving to the next story.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Parallel Execution Examples
|
||||||
|
|
||||||
|
### Phase 1
|
||||||
|
|
||||||
|
- T002 and T003 can run in parallel after T001 confirms the bounded slice.
|
||||||
|
|
||||||
|
### Phase 2
|
||||||
|
|
||||||
|
- T004 and T005 can run in parallel while T006 finalizes the canonical replacement rule.
|
||||||
|
|
||||||
|
### User Story 1
|
||||||
|
|
||||||
|
- T007 can run alongside any last Phase 2 cleanup.
|
||||||
|
- T008 can proceed before T009 and T010 finish wiring the glossary into page classes and views.
|
||||||
|
|
||||||
|
### User Story 2
|
||||||
|
|
||||||
|
- T011 can run alongside the first US1 validation.
|
||||||
|
- T012 and T014 can proceed in parallel before T013 finalizes the shared shell labels.
|
||||||
|
|
||||||
|
### User Story 3
|
||||||
|
|
||||||
|
- T015 and T016 can run in parallel because they cover feature and browser proof separately.
|
||||||
|
- T017 can proceed before T018 finalizes the bounded helper-text consumers.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Strategy
|
||||||
|
|
||||||
|
### Suggested MVP Scope
|
||||||
|
|
||||||
|
- MVP = **Phase 2 + US1 + US2**. The product becomes semantically coherent once the chooser, registry, dashboard, and shell all teach the same environment-first admin vocabulary.
|
||||||
|
|
||||||
|
### Incremental Delivery
|
||||||
|
|
||||||
|
1. Complete Phase 1 and Phase 2.
|
||||||
|
2. Deliver US1 and validate the chooser/registry surfaces.
|
||||||
|
3. Deliver US2 and validate the dashboard/shell surfaces.
|
||||||
|
4. Deliver US3 and validate the bounded provider-neutral helper text plus browser smoke.
|
||||||
|
5. Finish with Phase 6 validation, formatting, and explicit spillover review.
|
||||||
|
|
||||||
|
### Team Strategy
|
||||||
|
|
||||||
|
1. Settle the glossary and guard boundaries before changing multiple shared surfaces.
|
||||||
|
2. Parallelize test authoring inside each story before converging on the shared localization files.
|
||||||
|
3. Serialize merges around `localization.php`, `TenantDashboard.php`, and `OperateHubShell.php` because they are the most likely merge hotspots for this slice.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Explicit Follow-Ups / Out of Scope
|
||||||
|
|
||||||
|
- broader route/slug/class renaming remains a separate follow-up
|
||||||
|
- broader provider-owned surface cleanup remains a separate follow-up
|
||||||
|
- auth-provider wording changes remain out of scope
|
||||||
|
- website localization and customer-review localization remain out of scope for this package
|
||||||
|
- no-legacy enforcement and broader guard-pack work remain in Spec `287`
|
||||||
|
|
||||||
|
## Close-out Notes
|
||||||
|
|
||||||
|
- 2026-05-10: Manual integrated-browser investigation found a live runtime issue on the `Capture snapshot` action where the Livewire request fires but the Filament action modal host remains empty. That behavior is treated as discovered out-of-scope runtime debt for Spec `286` and is intentionally not part of this package's copy / IA / localization acceptance scope.
|
||||||
|
- Spec `286` closes on the bounded environment-first copy, IA, and localization neutralization surfaces plus the targeted Pest validations defined above.
|
||||||
Loading…
Reference in New Issue
Block a user