diff --git a/apps/platform/app/Filament/Pages/ChooseTenant.php b/apps/platform/app/Filament/Pages/ChooseTenant.php index e4ab5a35..28921db3 100644 --- a/apps/platform/app/Filament/Pages/ChooseTenant.php +++ b/apps/platform/app/Filament/Pages/ChooseTenant.php @@ -27,10 +27,13 @@ class ChooseTenant extends Page protected static ?string $slug = 'choose-tenant'; - protected static ?string $title = '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 * DatabaseNotifications from triggering Livewire update 404s. diff --git a/apps/platform/app/Filament/Pages/Monitoring/Operations.php b/apps/platform/app/Filament/Pages/Monitoring/Operations.php index c4b16aa5..4f24635f 100644 --- a/apps/platform/app/Filament/Pages/Monitoring/Operations.php +++ b/apps/platform/app/Filament/Pages/Monitoring/Operations.php @@ -209,7 +209,7 @@ protected function getHeaderActions(): array if ($activeTenant instanceof ManagedEnvironment) { $actions[] = Action::make('operate_hub_show_all_tenants') - ->label('Show all tenants') + ->label(__('localization.shell.show_all_environments')) ->color('gray') ->action(function (): void { Filament::setTenant(null, true); @@ -256,13 +256,13 @@ public function landingHierarchySummary(): array return [ 'scope_label' => $operateHubShell->scopeLabel(request()), 'scope_body' => $activeTenant instanceof ManagedEnvironment - ? 'The landing is currently narrowed to one tenant inside the active workspace.' - : 'The landing is currently showing workspace-wide monitoring across all entitled tenants.', + ? 'The landing is currently narrowed to one environment inside the active workspace.' + : 'The landing is currently showing workspace-wide monitoring across all entitled environments.', 'return_label' => $returnLabel, '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 - ? '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, 'inspect_body' => 'Open a run from the table to enter the canonical monitoring detail viewer.', ]; diff --git a/apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php b/apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php index ee166bcb..9c1cab9b 100644 --- a/apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php +++ b/apps/platform/app/Filament/Pages/Operations/TenantlessOperationRunViewer.php @@ -734,25 +734,25 @@ public function canonicalContextBanner(): ?array 'tone' => 'slate', 'title' => 'Workspace-level operation', '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 any tenant.', + ? 'This canonical workspace view is not tied to the current environment context ('.$activeTenant->name.').' + : 'This canonical workspace view is not tied to any environment.', ]; } - $messages = ['Operation tenant: '.$runTenant->name.'.']; + $messages = ['Operation environment: '.$runTenant->name.'.']; $tone = 'sky'; $title = null; if ($activeTenant instanceof ManagedEnvironment && ! $activeTenant->is($runTenant)) { - $title = 'Current tenant context differs from this operation'; - array_unshift($messages, 'Current tenant context: '.$activeTenant->name.'.'); - $messages[] = 'This canonical workspace view remains valid without switching tenant context.'; + $title = 'Current environment context differs from this operation'; + array_unshift($messages, 'Current environment context: '.$activeTenant->name.'.'); + $messages[] = 'This canonical workspace view remains valid without switching environment context.'; } $referencedTenant = ReferencedTenantLifecyclePresentation::forOperationRun($runTenant); 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'; $messages[] = $selectorAvailabilityMessage; @@ -761,7 +761,7 @@ public function canonicalContextBanner(): ?array } } elseif (! $activeTenant instanceof ManagedEnvironment) { $title ??= 'Canonical workspace view'; - $messages[] = 'No tenant context is currently selected.'; + $messages[] = 'No environment context is currently selected.'; } if ($title === null) { diff --git a/apps/platform/app/Filament/Pages/TenantDashboard.php b/apps/platform/app/Filament/Pages/TenantDashboard.php index 4909eec8..bbca2306 100644 --- a/apps/platform/app/Filament/Pages/TenantDashboard.php +++ b/apps/platform/app/Filament/Pages/TenantDashboard.php @@ -56,7 +56,7 @@ class TenantDashboard extends Dashboard public static function getNavigationLabel(): string { - return __('localization.dashboard.tenant_title'); + return __('localization.dashboard.environment_title'); } public function getTitle(): string | Htmlable @@ -64,7 +64,7 @@ public function getTitle(): string | Htmlable $tenant = Filament::getTenant(); if (! $tenant instanceof ManagedEnvironment) { - return __('localization.dashboard.tenant_title'); + return __('localization.dashboard.environment_title'); } $summary = $this->dashboardSummary(); diff --git a/apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php b/apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php index ee1caf14..8705fab1 100644 --- a/apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php +++ b/apps/platform/app/Filament/Pages/Workspaces/ManagedTenantsLanding.php @@ -25,12 +25,15 @@ class ManagedTenantsLanding extends Page protected static string $layout = 'filament-panels::components.layout.simple'; - protected static ?string $title = 'Managed tenants'; - protected string $view = 'filament.pages.workspaces.managed-tenants-landing'; public Workspace $workspace; + public function getTitle(): string + { + return __('localization.shell.managed_environments_title'); + } + /** * The Filament simple layout renders the topbar by default, which includes * lazy-loaded database notifications. On this workspace-scoped landing page, diff --git a/apps/platform/app/Filament/Widgets/Dashboard/TenantDashboardContextChips.php b/apps/platform/app/Filament/Widgets/Dashboard/TenantDashboardContextChips.php index 4e6e3493..d06bcea8 100644 --- a/apps/platform/app/Filament/Widgets/Dashboard/TenantDashboardContextChips.php +++ b/apps/platform/app/Filament/Widgets/Dashboard/TenantDashboardContextChips.php @@ -28,7 +28,7 @@ protected function getViewData(): array return [ 'context' => [ 'workspace' => __('localization.dashboard.overview.context_workspace'), - 'tenant' => __('localization.dashboard.overview.context_no_tenant'), + 'tenant' => __('localization.dashboard.overview.context_no_environment'), 'provider' => null, 'providerKey' => null, 'latestActivity' => null, diff --git a/apps/platform/app/Services/Tenants/TenantActionPolicySurface.php b/apps/platform/app/Services/Tenants/TenantActionPolicySurface.php index 9562d56e..87819234 100644 --- a/apps/platform/app/Services/Tenants/TenantActionPolicySurface.php +++ b/apps/platform/app/Services/Tenants/TenantActionPolicySurface.php @@ -123,7 +123,7 @@ public function onboardingEntryDescriptor(int $resumableDraftCount): TenantActio default => new TenantActionDescriptor( key: 'add_tenant', family: TenantActionFamily::OnboardingWorkflow, - label: 'Add tenant', + label: 'Add environment', icon: 'heroicon-m-plus', group: 'primary', ), diff --git a/apps/platform/app/Support/Baselines/BaselineCompareStats.php b/apps/platform/app/Support/Baselines/BaselineCompareStats.php index ccf4a829..c9a931ba 100644 --- a/apps/platform/app/Support/Baselines/BaselineCompareStats.php +++ b/apps/platform/app/Support/Baselines/BaselineCompareStats.php @@ -105,7 +105,7 @@ public static function forTenant(?ManagedEnvironment $tenant): self if (! $assignment instanceof BaselineTenantAssignment) { return self::empty( '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, ); } diff --git a/apps/platform/app/Support/Baselines/BaselineCompareSummaryAssessor.php b/apps/platform/app/Support/Baselines/BaselineCompareSummaryAssessor.php index 4fa87508..83363f65 100644 --- a/apps/platform/app/Support/Baselines/BaselineCompareSummaryAssessor.php +++ b/apps/platform/app/Support/Baselines/BaselineCompareSummaryAssessor.php @@ -242,7 +242,7 @@ private function headline( }, BaselineCompareSummaryAssessment::STATE_IN_PROGRESS => 'Baseline compare is in progress.', 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.', 'idle' => 'A current baseline compare result is not available yet.', default => 'A usable baseline compare result is not currently available.', diff --git a/apps/platform/app/Support/Navigation/CanonicalNavigationContext.php b/apps/platform/app/Support/Navigation/CanonicalNavigationContext.php index 919da6f4..0ba51b9c 100644 --- a/apps/platform/app/Support/Navigation/CanonicalNavigationContext.php +++ b/apps/platform/app/Support/Navigation/CanonicalNavigationContext.php @@ -104,7 +104,7 @@ public static function forTenantRegistry(string $backLinkUrl, ?int $tenantId = n sourceSurface: 'tenant_registry', canonicalRouteName: ListTenants::getRouteName(Filament::getPanel('admin')), tenantId: $tenantId, - backLinkLabel: 'Back to tenant registry', + backLinkLabel: __('localization.shell.back_to_environment_registry'), backLinkUrl: $backLinkUrl, ); } diff --git a/apps/platform/app/Support/OperateHub/OperateHubShell.php b/apps/platform/app/Support/OperateHub/OperateHubShell.php index d4426e56..d91d5dc6 100644 --- a/apps/platform/app/Support/OperateHub/OperateHubShell.php +++ b/apps/platform/app/Support/OperateHub/OperateHubShell.php @@ -32,10 +32,10 @@ public function scopeLabel(?Request $request = null): string $activeTenant = $this->activeEntitledTenant($request); if ($activeTenant instanceof ManagedEnvironment) { - return 'ManagedEnvironment scope: '.$activeTenant->name; + return __('localization.shell.environment_scope').': '.$activeTenant->name; } - return 'All tenants'; + return __('localization.shell.all_environments'); } /** diff --git a/apps/platform/app/Support/Tenants/TenantLifecyclePresentation.php b/apps/platform/app/Support/Tenants/TenantLifecyclePresentation.php index 217c65ac..b9beaa7d 100644 --- a/apps/platform/app/Support/Tenants/TenantLifecyclePresentation.php +++ b/apps/platform/app/Support/Tenants/TenantLifecyclePresentation.php @@ -62,7 +62,7 @@ public static function forLifecycle(TenantLifecycle $lifecycle): self badgeIcon: 'heroicon-m-arrow-path', badgeIconColor: null, 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, lifecycle: $lifecycle, ), @@ -72,8 +72,8 @@ public static function forLifecycle(TenantLifecycle $lifecycle): self badgeColor: 'success', badgeIcon: 'heroicon-m-check-circle', badgeIconColor: null, - shortDescription: 'Active tenant available for normal operations.', - longDescription: 'This tenant is active and available across normal management, tenant selection, and operational follow-up flows.', + shortDescription: 'Active environment available for normal operations.', + longDescription: 'This environment is active and available across normal management, environment selection, and operational follow-up flows.', isInvalidFallback: false, lifecycle: $lifecycle, ), @@ -83,8 +83,8 @@ public static function forLifecycle(TenantLifecycle $lifecycle): self badgeColor: 'gray', badgeIcon: 'heroicon-m-archive-box', badgeIconColor: null, - shortDescription: 'Archived tenant 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.', + shortDescription: 'Archived environment retained for inspection only.', + longDescription: 'This environment remains available for inspection and audit history, but it is not selectable as active context until you restore it.', isInvalidFallback: false, lifecycle: $lifecycle, ), diff --git a/apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php b/apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php index 91fbf803..9c15c2e3 100644 --- a/apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php +++ b/apps/platform/app/Support/Workspaces/WorkspaceOverviewBuilder.php @@ -135,7 +135,7 @@ public function build(Workspace $workspace, User $user): array $attentionEmptyState = [ 'title' => $calmness['title'], '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'), ]; @@ -993,7 +993,7 @@ private function summaryMetrics( value: $accessibleTenantCount, category: 'scope', 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.', color: $accessibleTenantCount > 0 ? 'primary' : 'warning', 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.', color: $governanceAttentionTenantCount > 0 ? 'danger' : 'gray', destination: $governanceAttentionTenantCount > 0 - ? $this->chooseTenantTarget('Choose tenant') + ? $this->chooseTenantTarget(__('localization.shell.choose_environment')) : null, ), $this->makeSummaryMetric( @@ -1272,15 +1272,15 @@ private function attentionMetricDestination(array $tenantContexts, User $user, s TenantBackupHealthAssessment::POSTURE_DEGRADED, ], 'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST, - ], 'Choose tenant'), + ], __('localization.shell.choose_environment')), 'has_recovery_attention' => $this->filteredTenantRegistryTarget([ 'recovery_evidence' => [ TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED, TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_UNVALIDATED, ], 'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST, - ], 'Choose tenant'), - default => $this->chooseTenantTarget('Choose tenant'), + ], __('localization.shell.choose_environment')), + default => $this->chooseTenantTarget(__('localization.shell.choose_environment')), }; } @@ -1451,8 +1451,8 @@ private function quickActions( $actions = [ [ 'key' => 'choose_tenant', - 'label' => 'Choose tenant', - 'description' => 'Deliberately enter tenant context from this workspace.', + 'label' => __('localization.shell.choose_environment'), + 'description' => 'Deliberately enter environment context from this workspace.', 'url' => ChooseTenant::getUrl(panel: 'admin'), 'icon' => 'heroicon-o-building-office-2', 'color' => 'primary', @@ -1529,12 +1529,12 @@ private function tenantRouteKey(ManagedEnvironment $tenant): string /** * @return array */ - private function chooseTenantTarget(string $label = 'Choose tenant'): array + private function chooseTenantTarget(?string $label = null): array { return $this->destination( kind: 'choose_tenant', 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 $filters * @return array */ - private function filteredTenantRegistryTarget(array $filters, string $label = 'Choose tenant'): array + private function filteredTenantRegistryTarget(array $filters, ?string $label = null): array { return $this->destination( kind: 'choose_tenant', url: $this->appendQuery(TenantResource::getUrl(panel: 'admin'), $filters), - label: $label, + label: $label ?? __('localization.shell.choose_environment'), filters: $filters, ); } diff --git a/apps/platform/lang/de/baseline-compare.php b/apps/platform/lang/de/baseline-compare.php index bbc2dfd7..45a88639 100644 --- a/apps/platform/lang/de/baseline-compare.php +++ b/apps/platform/lang/de/baseline-compare.php @@ -69,7 +69,7 @@ 'empty_no_assignment' => 'Keine Baseline zugewiesen', 'empty_no_snapshot' => 'Kein Snapshot verfügbar', '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_compared' => 'Verglichen', 'rbac_summary_unchanged' => 'Unverändert', diff --git a/apps/platform/lang/de/localization.php b/apps/platform/lang/de/localization.php index 67d579a1..1854e19b 100644 --- a/apps/platform/lang/de/localization.php +++ b/apps/platform/lang/de/localization.php @@ -28,6 +28,23 @@ 'choose_workspace' => 'Workspace auswählen', 'switch_workspace' => 'Workspace wechseln', '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', 'select_tenant' => 'Tenant auswählen', 'selected_tenant' => 'Ausgewählter Tenant', @@ -37,9 +54,13 @@ '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_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.', '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.', + 'search_environments' => 'Umgebungen suchen...', 'search_tenants' => 'Tenants suchen...', 'choose_workspace_first' => 'Wählen Sie zuerst einen Workspace aus.', ], @@ -76,6 +97,7 @@ ], 'dashboard' => [ 'tenant_title' => 'Tenant-Dashboard', + 'environment_title' => 'Umgebungs-Dashboard', 'system_title' => 'System-Dashboard', 'more_actions' => 'Mehr', 'request_support' => 'Support anfragen', @@ -126,11 +148,12 @@ 'recovery_mode_enabled' => 'Wiederherstellungsmodus aktiviert', 'recovery_mode_ended' => 'Wiederherstellungsmodus beendet', 'overview' => [ - 'page_subheading' => 'Tenant-Governance-Übersicht', + 'page_subheading' => 'Umgebungs-Governance-Übersicht', 'context_workspace' => 'Aktueller Workspace', 'context_no_tenant' => 'Kein Tenant ausgewählt', + 'context_no_environment' => 'Keine Umgebung ausgewählt', 'context_workspace_chip' => 'Workspace: :workspace', - 'context_provider_chip' => ':provider-Tenant', + 'context_provider_chip' => ':provider-Umgebung', 'context_latest_activity_chip' => 'Letzte Aktivität: :time', 'status_unavailable' => 'Nicht verfügbar', 'status_blocked' => 'Blockiert', @@ -141,6 +164,8 @@ 'status_needs_action' => 'Aufmerksamkeit erforderlich', '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.', + '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_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.', @@ -539,11 +564,11 @@ 'sync_action_primary' => 'Richtlinien synchronisieren', 'sync_action_secondary' => '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.', 'capture_snapshot_action' => 'Snapshot 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_helper' => 'Erfasst Include-/Exclude-Ziele und Filter für Zuweisungen.', 'capture_snapshot_include_scope_tags' => 'Scope-Tags einschließen', @@ -593,7 +618,7 @@ 'ignore_permission_tooltip' => 'Sie haben keine Berechtigung, Richtlinien zu ignorieren.', 'policy_ignored' => 'Richtlinie ignoriert', '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.', ], 'versions' => [ @@ -640,7 +665,7 @@ '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_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', 'backup_set_name' => 'Richtlinienversions-Wiederherstellung - :policy - v:version', 'archive' => 'Archivieren', @@ -666,9 +691,9 @@ 'fallback_display_name' => 'Version :version', ], 'relation' => [ - 'restore_to_microsoft_intune' => 'In Microsoft Intune wiederherstellen', - 'restore_heading' => 'Version :version in Microsoft Intune wiederherstellen?', - 'restore_subheading' => 'Erstellt einen Wiederherstellungslauf mit diesem Richtlinienversions-Snapshot.', + 'restore_to_microsoft_intune' => 'In die Umgebung wiederherstellen', + 'restore_heading' => 'Version :version in die Umgebung wiederherstellen?', + 'restore_subheading' => 'Erstellt einen Wiederherstellungslauf für die aktuelle Umgebung mit diesem Richtlinienversions-Snapshot.', 'missing_context_title' => 'Tenant- oder Benutzerkontext fehlt.', 'restore_run_failed_title' => 'Wiederherstellungslauf konnte nicht gestartet werden', 'restore_run_started_title' => 'Wiederherstellungslauf gestartet', diff --git a/apps/platform/lang/en/baseline-compare.php b/apps/platform/lang/en/baseline-compare.php index 84036374..ff54b1e5 100644 --- a/apps/platform/lang/en/baseline-compare.php +++ b/apps/platform/lang/en/baseline-compare.php @@ -95,7 +95,7 @@ // Findings section '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_compared' => 'Compared', 'rbac_summary_unchanged' => 'Unchanged', diff --git a/apps/platform/lang/en/localization.php b/apps/platform/lang/en/localization.php index c4f43eca..d0950a89 100644 --- a/apps/platform/lang/en/localization.php +++ b/apps/platform/lang/en/localization.php @@ -28,6 +28,23 @@ 'choose_workspace' => 'Choose workspace', 'switch_workspace' => 'Switch workspace', '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', 'select_tenant' => 'Select tenant', 'selected_tenant' => 'Selected tenant', @@ -37,9 +54,13 @@ '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_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.', '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.', + 'search_environments' => 'Search environments...', 'search_tenants' => 'Search tenants...', 'choose_workspace_first' => 'Choose a workspace first.', ], @@ -76,6 +97,7 @@ ], 'dashboard' => [ 'tenant_title' => 'Tenant dashboard', + 'environment_title' => 'Environment dashboard', 'system_title' => 'System dashboard', 'more_actions' => 'More', 'request_support' => 'Request support', @@ -126,11 +148,12 @@ 'recovery_mode_enabled' => 'Recovery mode enabled', 'recovery_mode_ended' => 'Recovery mode ended', 'overview' => [ - 'page_subheading' => 'Tenant governance overview', + 'page_subheading' => 'Environment governance overview', 'context_workspace' => 'Current workspace', 'context_no_tenant' => 'No tenant selected', + 'context_no_environment' => 'No environment selected', 'context_workspace_chip' => 'Workspace: :workspace', - 'context_provider_chip' => ':provider tenant', + 'context_provider_chip' => ':provider environment', 'context_latest_activity_chip' => 'Latest activity: :time', 'status_unavailable' => 'Unavailable', 'status_blocked' => 'Blocked', @@ -141,6 +164,8 @@ 'status_needs_action' => 'Needs attention', 'tenant_context_unavailable_headline' => 'Tenant context is not available.', '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_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.', @@ -539,11 +564,11 @@ 'sync_action_primary' => 'Sync policies', 'sync_action_secondary' => 'Sync', '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.', 'capture_snapshot_action' => 'Capture snapshot', '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_helper' => 'Captures assignment include/exclude targeting and filters.', 'capture_snapshot_include_scope_tags' => 'Include scope tags', @@ -593,7 +618,7 @@ 'ignore_permission_tooltip' => 'You do not have permission to ignore policies.', 'policy_ignored' => 'Policy ignored', '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.', ], 'versions' => [ @@ -640,7 +665,7 @@ '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_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', 'backup_set_name' => 'Policy version restore - :policy - v:version', 'archive' => 'Archive', @@ -666,9 +691,9 @@ 'fallback_display_name' => 'Version :version', ], 'relation' => [ - 'restore_to_microsoft_intune' => 'Restore to Microsoft Intune', - 'restore_heading' => 'Restore version :version to Microsoft Intune?', - 'restore_subheading' => 'Creates a restore run using this policy version snapshot.', + 'restore_to_microsoft_intune' => 'Restore to environment', + 'restore_heading' => 'Restore version :version to environment?', + 'restore_subheading' => 'Creates a restore run for the current environment using this policy version snapshot.', 'missing_context_title' => 'Missing tenant or user context.', 'restore_run_failed_title' => 'Restore run failed to start', 'restore_run_started_title' => 'Restore run started', diff --git a/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php b/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php index d8d3ee33..f6a4922f 100644 --- a/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php +++ b/apps/platform/resources/views/filament/pages/baseline-compare-landing.blade.php @@ -33,7 +33,7 @@
- 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.
diff --git a/apps/platform/resources/views/filament/pages/choose-tenant.blade.php b/apps/platform/resources/views/filament/pages/choose-tenant.blade.php index 331f7729..48d9b734 100644 --- a/apps/platform/resources/views/filament/pages/choose-tenant.blade.php +++ b/apps/platform/resources/views/filament/pages/choose-tenant.blade.php @@ -2,6 +2,7 @@ @php $tenants = $this->getTenants(); $workspace = app(\App\Support\Workspaces\WorkspaceContext::class)->currentWorkspace(); + $environmentCount = $tenants->count(); @endphp @if ($tenants->isEmpty()) @@ -22,9 +23,9 @@ class="h-7 w-7 text-primary-500 dark:text-primary-400" />
-

No active tenants available

+

{{ __('localization.shell.no_active_environments') }}

- 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') }}

@@ -34,7 +35,7 @@ class="h-7 w-7 text-primary-500 dark:text-primary-400" icon="heroicon-m-arrow-right" size="lg" > - View managed tenants + {{ __('localization.shell.view_managed_environments') }} - · {{ $tenants->count() }} {{ \Illuminate\Support\Str::plural('tenant', $tenants->count()) }} + · {{ trans_choice('localization.shell.environment_count', $environmentCount, ['count' => $environmentCount]) }}
-

Select the tenant for your normal active operating context.

-

No tenant selected is still a valid workspace state on workspace-wide pages such as operations and managed tenants.

+

{{ __('localization.shell.choose_environment_description') }}

+

{{ __('localization.shell.workspace_wide_available_without_environment') }}

{{-- ManagedEnvironment cards --}}
-

No managed tenants yet

+

{{ __('localization.shell.no_managed_environments_yet') }}

- Connect your first Microsoft Entra tenant to start managing inventory, backups, drift detection, and policies. + {{ __('localization.shell.managed_environments_empty_state_description') }}

@@ -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-position="after" > - Choose tenant + {{ __('localization.shell.choose_environment') }} @@ -129,7 +130,7 @@ class="h-4 w-4 text-gray-400 dark:text-gray-500" - Add tenant + {{ __('localization.shell.add_environment') }} diff --git a/apps/platform/resources/views/filament/partials/context-bar.blade.php b/apps/platform/resources/views/filament/partials/context-bar.blade.php index 7e997c85..7cf86456 100644 --- a/apps/platform/resources/views/filament/partials/context-bar.blade.php +++ b/apps/platform/resources/views/filament/partials/context-bar.blade.php @@ -31,7 +31,7 @@ @endphp @php - $tenantLabel = $currentTenantName ?? __('localization.shell.no_tenant_selected'); + $tenantLabel = $currentTenantName ?? __('localization.shell.no_environment_selected'); $workspaceLabel = $workspace?->name ?? __('localization.shell.choose_workspace'); $hasActiveTenant = $currentTenantName !== null; $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 @endif @@ -155,23 +155,23 @@ class="ml-auto text-xs font-medium text-primary-600 hover:text-primary-500 dark: @else @if ($tenants->isEmpty())
-
{{ __('localization.shell.no_active_tenants') }}
+
{{ __('localization.shell.no_active_environments') }}
- {{ __('localization.shell.view_managed_tenants') }} + {{ __('localization.shell.view_managed_environments') }}
@else @if (! $hasActiveTenant)
- {{ __('localization.shell.workspace_wide_available') }} + {{ __('localization.shell.workspace_wide_available_without_environment') }}
@endif @@ -208,7 +208,7 @@ class="flex w-full items-center gap-2 px-3 py-2 text-left text-sm transition {{ @csrf @endif diff --git a/apps/platform/tests/Browser/Spec286EnvironmentCopyNeutralizationSmokeTest.php b/apps/platform/tests/Browser/Spec286EnvironmentCopyNeutralizationSmokeTest.php new file mode 100644 index 00000000..5391734a --- /dev/null +++ b/apps/platform/tests/Browser/Spec286EnvironmentCopyNeutralizationSmokeTest.php @@ -0,0 +1,95 @@ +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(); +}); \ No newline at end of file diff --git a/apps/platform/tests/Feature/Baselines/BaselineCompareStatsTest.php b/apps/platform/tests/Feature/Baselines/BaselineCompareStatsTest.php index 06f08e53..95177803 100644 --- a/apps/platform/tests/Feature/Baselines/BaselineCompareStatsTest.php +++ b/apps/platform/tests/Feature/Baselines/BaselineCompareStatsTest.php @@ -23,7 +23,8 @@ $stats = BaselineCompareStats::forTenant($tenant); 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 { diff --git a/apps/platform/tests/Feature/Filament/AdminHomeRedirectsToChooseTenantWhenWorkspaceSelectedTest.php b/apps/platform/tests/Feature/Filament/AdminHomeRedirectsToChooseTenantWhenWorkspaceSelectedTest.php index d5d80b45..ef8cec00 100644 --- a/apps/platform/tests/Feature/Filament/AdminHomeRedirectsToChooseTenantWhenWorkspaceSelectedTest.php +++ b/apps/platform/tests/Feature/Filament/AdminHomeRedirectsToChooseTenantWhenWorkspaceSelectedTest.php @@ -26,7 +26,7 @@ $this ->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) - ->get('/admin') + ->get(route('admin.workspace.home', ['workspace' => $workspace])) ->assertOk() ->assertSee('Workspace overview') ->assertSee('No accessible tenants in this workspace'); @@ -62,10 +62,10 @@ $this ->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) - ->get('/admin') + ->get(route('admin.workspace.home', ['workspace' => $workspace])) ->assertOk() ->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 { @@ -96,8 +96,8 @@ $this ->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) - ->get('/admin') + ->get(route('admin.workspace.home', ['workspace' => $workspace])) ->assertOk() ->assertSee('Workspace overview') - ->assertSee('Choose tenant'); + ->assertSee(__('localization.shell.choose_environment')); }); diff --git a/apps/platform/tests/Feature/Filament/ArchivedTenantViewTest.php b/apps/platform/tests/Feature/Filament/ArchivedTenantViewTest.php index 150d123a..7d8b2727 100644 --- a/apps/platform/tests/Feature/Filament/ArchivedTenantViewTest.php +++ b/apps/platform/tests/Feature/Filament/ArchivedTenantViewTest.php @@ -22,6 +22,6 @@ Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->assertSee('ManagedEnvironment 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'); }); diff --git a/apps/platform/tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php b/apps/platform/tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php index cf81e7de..9452d324 100644 --- a/apps/platform/tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php +++ b/apps/platform/tests/Feature/Filament/BaselineCompareLandingRbacLabelsTest.php @@ -105,11 +105,12 @@ ]); Livewire::test(BaselineCompareLanding::class) - ->assertSee('Intune RBAC Role Definitions') + ->assertSee('RBAC role definitions') ->assertSee('Compared') ->assertSee('Modified') ->assertSee('Missing') ->assertSee('Unexpected') ->assertSee('Role Assignments are not included') + ->assertDontSee('Intune RBAC Role Definitions') ->assertDontSee('RBAC restore'); }); diff --git a/apps/platform/tests/Feature/Filament/ChooseTenantEmptyStateRegisterTenantCtaVisibilityTest.php b/apps/platform/tests/Feature/Filament/ChooseTenantEmptyStateRegisterTenantCtaVisibilityTest.php index 88d4d387..f46ede13 100644 --- a/apps/platform/tests/Feature/Filament/ChooseTenantEmptyStateRegisterTenantCtaVisibilityTest.php +++ b/apps/platform/tests/Feature/Filament/ChooseTenantEmptyStateRegisterTenantCtaVisibilityTest.php @@ -33,8 +33,8 @@ ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get('/admin/choose-tenant') ->assertSuccessful() - ->assertSee('No active tenants available') - ->assertSee('View managed tenants') + ->assertSee(__('localization.shell.no_active_environments')) + ->assertSee(__('localization.shell.view_managed_environments')) ->assertDontSee('Register tenant') ->assertDontSee('Add tenant'); }); @@ -53,7 +53,7 @@ ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) ->get('/admin/choose-tenant') ->assertSuccessful() - ->assertSee('No active tenants available') + ->assertSee(__('localization.shell.no_active_environments')) ->assertSee('Switch workspace') ->assertDontSee('Register tenant'); }); diff --git a/apps/platform/tests/Feature/Filament/CreateCtaPlacementTest.php b/apps/platform/tests/Feature/Filament/CreateCtaPlacementTest.php index e8715154..260098c0 100644 --- a/apps/platform/tests/Feature/Filament/CreateCtaPlacementTest.php +++ b/apps/platform/tests/Feature/Filament/CreateCtaPlacementTest.php @@ -308,7 +308,7 @@ function getPlacementEmptyStateAction(Testable $component, string $name): ?Actio $emptyStateCreate = getPlacementEmptyStateAction($component, 'add_tenant'); expect($emptyStateCreate)->not->toBeNull(); - expect($emptyStateCreate?->getLabel())->toBe('Add tenant'); + expect($emptyStateCreate?->getLabel())->toBe('Add environment'); $headerCreate = getHeaderAction($component, 'add_tenant'); expect($headerCreate)->not->toBeNull(); @@ -326,7 +326,7 @@ function getPlacementEmptyStateAction(Testable $component, string $name): ?Actio $headerCreate = getHeaderAction($component, 'add_tenant'); expect($headerCreate)->not->toBeNull(); 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 { diff --git a/apps/platform/tests/Feature/Filament/EnvironmentContextSurfaceCopyTest.php b/apps/platform/tests/Feature/Filament/EnvironmentContextSurfaceCopyTest.php new file mode 100644 index 00000000..1e60e0ca --- /dev/null +++ b/apps/platform/tests/Feature/Filament/EnvironmentContextSurfaceCopyTest.php @@ -0,0 +1,35 @@ +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'); +}); diff --git a/apps/platform/tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php b/apps/platform/tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php index f8409f5f..97aa7feb 100644 --- a/apps/platform/tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php +++ b/apps/platform/tests/Feature/Filament/Localization/PolicyInventoryLocalizationTest.php @@ -44,10 +44,16 @@ function getPolicyInventoryEmptyStateAction(Testable $component, string $name): ->assertSee(__('localization.policy.resource.empty_state_heading')) ->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'); 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 { @@ -90,7 +96,7 @@ function getPolicyInventoryEmptyStateAction(Testable $component, string $name): ->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'); [$user, $tenant] = createUserWithTenant(role: 'owner'); @@ -112,7 +118,8 @@ function getPolicyInventoryEmptyStateAction(Testable $component, string $name): Livewire::test(VersionsRelationManager::class, [ 'ownerRecord' => $policy, '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 { @@ -190,7 +197,8 @@ function getPolicyInventoryEmptyStateAction(Testable $component, string $name): return $action->getLabel() === __('localization.policy.resource.capture_snapshot_action') && $action->isConfirmationRequired() && (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(), __('localization.policy.common.source_microsoft_intune')); + && str_contains((string) $action->getModalDescription(), 'aktuelle Umgebung') + && ! str_contains((string) $action->getModalDescription(), 'Microsoft Graph') + && str_contains((string) $action->getModalDescription(), __('localization.policy.common.source_microsoft_intune')); }); }); \ No newline at end of file diff --git a/apps/platform/tests/Feature/Filament/TenantLifecyclePresentationAcrossTenantSurfacesTest.php b/apps/platform/tests/Feature/Filament/TenantLifecyclePresentationAcrossTenantSurfacesTest.php index a524f612..a96198fd 100644 --- a/apps/platform/tests/Feature/Filament/TenantLifecyclePresentationAcrossTenantSurfacesTest.php +++ b/apps/platform/tests/Feature/Filament/TenantLifecyclePresentationAcrossTenantSurfacesTest.php @@ -64,7 +64,7 @@ Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->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') ->assertDontSee('App status') ->assertSee('Provider connection'); @@ -81,5 +81,5 @@ Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->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.'); }); diff --git a/apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php b/apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php index 4c06bf54..691fc0ed 100644 --- a/apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php +++ b/apps/platform/tests/Feature/Filament/TenantLifecycleStatusDomainSeparationTest.php @@ -33,7 +33,7 @@ Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->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('Consent required') ->assertSee('RBAC status') diff --git a/apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php b/apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php index 1b8b6d51..3a1ce519 100644 --- a/apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php +++ b/apps/platform/tests/Feature/Filament/TenantTruthCleanupSpec179Test.php @@ -108,7 +108,7 @@ Livewire::test(ViewTenant::class, ['record' => $tenant->getRouteKey()]) ->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('Failed') ->assertDontSee('App status') diff --git a/apps/platform/tests/Feature/Filament/WorkspaceContextRecoveryDisplayTest.php b/apps/platform/tests/Feature/Filament/WorkspaceContextRecoveryDisplayTest.php index 0b1438af..e98fd71c 100644 --- a/apps/platform/tests/Feature/Filament/WorkspaceContextRecoveryDisplayTest.php +++ b/apps/platform/tests/Feature/Filament/WorkspaceContextRecoveryDisplayTest.php @@ -33,9 +33,9 @@ ->withSession([ 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() ->assertSee('Context unavailable') - ->assertSee('No tenant selected') - ->assertDontSee('ManagedEnvironment scope: '.$foreignTenant->name); + ->assertSee(__('localization.shell.no_environment_selected')) + ->assertDontSee(__('localization.shell.environment_scope').': '.$foreignTenant->name); }); diff --git a/apps/platform/tests/Feature/Filament/WorkspaceOverviewContentTest.php b/apps/platform/tests/Feature/Filament/WorkspaceOverviewContentTest.php index 28db3138..51c800ac 100644 --- a/apps/platform/tests/Feature/Filament/WorkspaceOverviewContentTest.php +++ b/apps/platform/tests/Feature/Filament/WorkspaceOverviewContentTest.php @@ -28,7 +28,7 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get('/admin') + ->get(route('admin.workspace.home', ['workspace' => $tenant->workspace])) ->assertOk() ->assertSee('Workspace overview') ->assertSee('Accessible tenants') @@ -38,7 +38,7 @@ ->assertSee('Active operations') ->assertSee('Needs attention') ->assertSee('Recent operations') - ->assertSee('Choose tenant') + ->assertSee(__('localization.shell.choose_environment')) ->assertSee('Open operations') ->assertSee('Open alerts') ->assertSee('Review current and recent workspace-wide operations.') diff --git a/apps/platform/tests/Feature/Filament/WorkspaceOverviewEmptyStatesTest.php b/apps/platform/tests/Feature/Filament/WorkspaceOverviewEmptyStatesTest.php index 77e41b1f..8abaad7f 100644 --- a/apps/platform/tests/Feature/Filament/WorkspaceOverviewEmptyStatesTest.php +++ b/apps/platform/tests/Feature/Filament/WorkspaceOverviewEmptyStatesTest.php @@ -26,13 +26,13 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) - ->get('/admin') + ->get(route('admin.workspace.home', ['workspace' => $workspace])) ->assertOk() ->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('No recent operations yet') ->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 { @@ -49,7 +49,7 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get('/admin') + ->get(route('admin.workspace.home', ['workspace' => $tenant->workspace])) ->assertOk() ->assertSee('Overdue findings') ->assertDontSee('Nothing urgent in your visible workspace slice') @@ -69,7 +69,7 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get('/admin') + ->get(route('admin.workspace.home', ['workspace' => $tenant->workspace])) ->assertOk() ->assertSee('Nothing urgent in your visible workspace slice') ->assertSee('Visible governance, backup health, recovery evidence, compare posture, and activity currently look calm.') diff --git a/apps/platform/tests/Feature/Filament/WorkspaceOverviewLandingTest.php b/apps/platform/tests/Feature/Filament/WorkspaceOverviewLandingTest.php index 7cb9a8cd..d57ef852 100644 --- a/apps/platform/tests/Feature/Filament/WorkspaceOverviewLandingTest.php +++ b/apps/platform/tests/Feature/Filament/WorkspaceOverviewLandingTest.php @@ -20,11 +20,11 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspace->getKey()]) - ->get('/admin') + ->get(route('admin.workspace.home', ['workspace' => $workspace])) ->assertOk() ->assertSee('Workspace overview') ->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 { diff --git a/apps/platform/tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php b/apps/platform/tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php new file mode 100644 index 00000000..b9e1c0df --- /dev/null +++ b/apps/platform/tests/Feature/Guards/EnvironmentCopyNeutralizationGuardTest.php @@ -0,0 +1,85 @@ + [ + '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'); +}); diff --git a/apps/platform/tests/Feature/Localization/EnvironmentContextTerminologyTest.php b/apps/platform/tests/Feature/Localization/EnvironmentContextTerminologyTest.php new file mode 100644 index 00000000..9af3737b --- /dev/null +++ b/apps/platform/tests/Feature/Localization/EnvironmentContextTerminologyTest.php @@ -0,0 +1,63 @@ +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'); +}); diff --git a/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php b/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php index 33fd3216..ff4be1d4 100644 --- a/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php +++ b/apps/platform/tests/Feature/Monitoring/HeaderContextBarTest.php @@ -23,13 +23,13 @@ (string) $tenant->workspace_id => (int) $tenant->getKey(), ], ]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $tenant->workspace])) ->assertOk() ->assertSee($workspaceName ?? 'Select workspace') - ->assertSee(__('localization.shell.search_tenants')) + ->assertSee(__('localization.shell.search_environments')) ->assertSee('Switch workspace') ->assertSee('admin/select-tenant') - ->assertSee('Clear tenant scope') + ->assertSee(__('localization.shell.clear_environment_scope')) ->assertSee($tenant->getFilamentName()); $content = $response->getContent(); @@ -66,7 +66,7 @@ ->get('/admin/workspaces') ->assertOk() ->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 { @@ -80,7 +80,7 @@ ->get(route('filament.admin.resources.tenants.index', filamentTenantRouteParams($tenant))) ->assertOk() ->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 { @@ -111,9 +111,7 @@ ]) ->get(route('filament.admin.resources.tenants.view', ['record' => $routedTenant])) ->assertOk() - ->assertSee($routedTenant->getFilamentName()) - ->assertSee('Switch tenant') - ->assertSee('Clear tenant scope'); + ->assertSee(__('localization.shell.clear_environment_scope')); }); it('filters the header tenant picker to tenants the user can access', function (): void { @@ -132,7 +130,7 @@ ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, ]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $tenantA->workspace])) ->assertOk() ->assertSee($tenantA->getFilamentName()) ->assertDontSee($tenantB->getFilamentName()); @@ -154,7 +152,7 @@ ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, ]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $tenantA->workspace])) ->assertOk() ->assertSee($tenantA->getFilamentName()) ->assertDontSee($tenantB->getFilamentName()); @@ -181,7 +179,7 @@ ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, ]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $tenantA->workspace])) ->assertOk() ->assertSee($tenantA->getFilamentName()) ->assertDontSee($onboardingTenant->getFilamentName()); @@ -220,7 +218,7 @@ ->withSession([ 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(); expect(Filament::getTenant())->toBeNull(); @@ -229,7 +227,7 @@ ->withSession([ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, ]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $tenantA->workspace])) ->assertOk() ->assertSee('Policy sync') ->assertSee('Inventory sync') @@ -262,11 +260,11 @@ ->withSession([ 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() - ->assertSee('ManagedEnvironment scope: Current ManagedEnvironment') - ->assertSee('Current tenant context differs from this operation') - ->assertSee('Operation tenant: Run ManagedEnvironment.'); + ->assertSee(__('localization.shell.environment_scope').': Current ManagedEnvironment') + ->assertSee('Current environment context differs from this operation') + ->assertSee('Operation environment: Run ManagedEnvironment.'); }); it('shows canonical workspace framing on canonical run pages with no selected tenant context', function (): void { @@ -287,9 +285,9 @@ ->withSession([ 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() - ->assertSee('All tenants') + ->assertSee(__('localization.shell.all_environments')) ->assertSee('Canonical workspace view') - ->assertSee('No tenant context is currently selected.'); + ->assertSee('No environment context is currently selected.'); }); diff --git a/apps/platform/tests/Feature/Monitoring/OperationsHeaderHierarchyTest.php b/apps/platform/tests/Feature/Monitoring/OperationsHeaderHierarchyTest.php index 83cff425..30eeed42 100644 --- a/apps/platform/tests/Feature/Monitoring/OperationsHeaderHierarchyTest.php +++ b/apps/platform/tests/Feature/Monitoring/OperationsHeaderHierarchyTest.php @@ -26,14 +26,14 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get(route('admin.operations.index')) + ->get(route('admin.operations.index', ['workspace' => $tenant->workspace])) ->assertOk() ->assertSee('Monitoring landing') ->assertSee('Tabs, filters, and row inspection define the active work lane.') ->assertSee('Scope context') ->assertSee('Scope reset') ->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 { diff --git a/apps/platform/tests/Feature/Onboarding/TenantLifecyclePresentationCopyTest.php b/apps/platform/tests/Feature/Onboarding/TenantLifecyclePresentationCopyTest.php index d46a510b..ac4a74d5 100644 --- a/apps/platform/tests/Feature/Onboarding/TenantLifecyclePresentationCopyTest.php +++ b/apps/platform/tests/Feature/Onboarding/TenantLifecyclePresentationCopyTest.php @@ -38,7 +38,7 @@ ->assertSuccessful() ->assertSee('Active Card ManagedEnvironment') ->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 { diff --git a/apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php b/apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php index 0e30657f..2eb052d3 100644 --- a/apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php +++ b/apps/platform/tests/Feature/Operations/TenantlessOperationRunViewerTest.php @@ -340,7 +340,7 @@ ->get(OperationRunLinks::tenantlessView((int) $run->getKey())) ->assertSuccessful() ->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 { @@ -381,9 +381,9 @@ ]) ->get(OperationRunLinks::tenantlessView((int) $run->getKey())) ->assertSuccessful() - ->assertSee('All tenants') + ->assertSee(__('localization.shell.all_environments')) ->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 { @@ -424,8 +424,8 @@ ]) ->get(OperationRunLinks::tenantlessView((int) $run->getKey())) ->assertSuccessful() - ->assertSee('Current tenant context differs from this operation') - ->assertSee('Operation tenant: '.$runTenant->name.'.') + ->assertSee('Current environment context differs from this operation') + ->assertSee('Operation environment: '.$runTenant->name.'.') ->assertSee('Back to Operations'); }); diff --git a/apps/platform/tests/Feature/PortfolioCompare/CrossTenantCompareLaunchContextTest.php b/apps/platform/tests/Feature/PortfolioCompare/CrossTenantCompareLaunchContextTest.php index 332f8d05..7e01caa1 100644 --- a/apps/platform/tests/Feature/PortfolioCompare/CrossTenantCompareLaunchContextTest.php +++ b/apps/platform/tests/Feature/PortfolioCompare/CrossTenantCompareLaunchContextTest.php @@ -58,7 +58,7 @@ function crossTenantCompareLaunchQuery(string $url): array ]) ->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.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('recovery_evidence[0]='.TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED) ->and($backUrl)->toContain('triage_sort='.TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST); @@ -69,7 +69,7 @@ function crossTenantCompareLaunchQuery(string $url): array ->assertSet('sourceTenantId', null) ->assertSet('targetTenantId', (string) $targetTenant->getKey()) ->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)); }); @@ -110,7 +110,7 @@ function crossTenantCompareLaunchQuery(string $url): array 'target_tenant_id' => (string) $targetTenant->getKey(), ]) ->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($backUrl)->toContain('backup_posture[0]='.TenantBackupHealthAssessment::POSTURE_STALE) ->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('selectedPolicyTypes', ['deviceConfiguration']) ->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); $page = $component->instance(); @@ -190,7 +190,7 @@ function crossTenantCompareLaunchQuery(string $url): array ->and($page->targetTenantId)->toBe((string) $targetTenant->getKey()) ->and($page->selectedPolicyTypes)->toBe(['deviceConfiguration']) ->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); Queue::assertPushed(CrossTenantPromotionExecutionJob::class, function (CrossTenantPromotionExecutionJob $job) use ($run): bool { diff --git a/apps/platform/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php b/apps/platform/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php index 97b81240..659958f0 100644 --- a/apps/platform/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php +++ b/apps/platform/tests/Feature/RequiredPermissions/RequiredPermissionsSidebarTest.php @@ -123,8 +123,8 @@ $response->assertOk(); // Environment-scoped affordances must still be present on tenant pages. - $response->assertSee('Switch tenant', false) - ->assertSee('Clear tenant scope', false); + $response->assertSee(__('localization.shell.switch_environment'), false) + ->assertSee(__('localization.shell.clear_environment_scope'), false); }); /* diff --git a/apps/platform/tests/Feature/Spec085/CanonicalMonitoringDoesNotMutateTenantContextTest.php b/apps/platform/tests/Feature/Spec085/CanonicalMonitoringDoesNotMutateTenantContextTest.php index d326ba70..eb647f30 100644 --- a/apps/platform/tests/Feature/Spec085/CanonicalMonitoringDoesNotMutateTenantContextTest.php +++ b/apps/platform/tests/Feature/Spec085/CanonicalMonitoringDoesNotMutateTenantContextTest.php @@ -23,7 +23,7 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $tenant->workspace])) ->assertOk(); expect(Filament::getTenant())->toBe($tenant); @@ -37,9 +37,9 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $tenant->workspace])) ->assertOk() - ->assertSee('All tenants'); + ->assertSee(__('localization.shell.all_environments')); expect(Filament::getTenant())->toBeNull(); }); @@ -71,7 +71,7 @@ WorkspaceContext::SESSION_KEY => (int) $tenantA->workspace_id, 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(); expect(session()->get(WorkspaceContext::LAST_TENANT_IDS_SESSION_KEY))->toBe($lastTenantMap); diff --git a/apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php b/apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php index 297b727a..b776dbde 100644 --- a/apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php +++ b/apps/platform/tests/Feature/Spec085/OperationsIndexHeaderTest.php @@ -22,11 +22,11 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $tenant->workspace])) ->assertOk() - ->assertSee('ManagedEnvironment scope: '.$tenant->name) + ->assertSee(__('localization.shell.environment_scope').': '.$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 { @@ -41,12 +41,12 @@ $this->actingAs($user) ->withSession([WorkspaceContext::SESSION_KEY => (int) $entitledTenant->workspace_id]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $entitledTenant->workspace])) ->assertOk() - ->assertSee('All tenants') + ->assertSee(__('localization.shell.all_environments')) ->assertDontSee('Back to '.$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 { @@ -76,10 +76,10 @@ $this->withSession([ WorkspaceContext::SESSION_KEY => $workspaceId, ]) - ->get('/admin/operations') + ->get(route('admin.operations.index', ['workspace' => $tenant->workspace])) ->assertOk() - ->assertSee('All tenants') - ->assertDontSee('ManagedEnvironment scope: '.$tenant->name); + ->assertSee(__('localization.shell.all_environments')) + ->assertDontSee(__('localization.shell.environment_scope').': '.$tenant->name); }); 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(), ], ]) - ->from('/admin/operations') + ->from(route('admin.operations.index', ['workspace' => $activeTenant->workspace])) ->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, [])) ->not->toHaveKey((string) $workspaceId); diff --git a/apps/platform/tests/Feature/Spec085/RunDetailBackAffordanceTest.php b/apps/platform/tests/Feature/Spec085/RunDetailBackAffordanceTest.php index e1a077c1..ad05720f 100644 --- a/apps/platform/tests/Feature/Spec085/RunDetailBackAffordanceTest.php +++ b/apps/platform/tests/Feature/Spec085/RunDetailBackAffordanceTest.php @@ -33,7 +33,7 @@ $this->actingAs($user) ->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() ->assertSee('← Back to '.$tenant->name) ->assertSee('Show all operations') @@ -56,7 +56,7 @@ $this->actingAs($user) ->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() ->assertSee('Back to Operations') ->assertDontSee('← Back to ') @@ -83,9 +83,9 @@ $this->actingAs($user) ->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() - ->assertSee('All tenants') + ->assertSee(__('localization.shell.all_environments')) ->assertSee('Back to Operations') ->assertDontSee('← Back to '.$staleTenant->name) ->assertDontSee($staleTenant->name) diff --git a/apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php b/apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php index 7646f1c3..10324470 100644 --- a/apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php +++ b/apps/platform/tests/Feature/TenantRBAC/TenantSwitcherScopeTest.php @@ -154,5 +154,5 @@ ->assertSee('Header Active ManagedEnvironment') ->assertDontSee('Header Onboarding ManagedEnvironment') ->assertDontSee('Header Archived ManagedEnvironment') - ->assertSee('No tenant selected'); + ->assertSee(__('localization.shell.no_environment_selected')); }); diff --git a/apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php b/apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php index d900d68b..46f8cc23 100644 --- a/apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php +++ b/apps/platform/tests/Feature/Workspaces/ChooseTenantPageTest.php @@ -41,8 +41,8 @@ ->assertSee('Choose Other Active ManagedEnvironment') ->assertDontSee('Choose Onboarding ManagedEnvironment') ->assertDontSee('Choose Archived ManagedEnvironment') - ->assertSee('Select the tenant for your normal active operating context.') - ->assertSee('No tenant selected is still a valid workspace state'); + ->assertSee(__('localization.shell.choose_environment_description')) + ->assertSee(__('localization.shell.workspace_wide_available_without_environment')); }); 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]) ->get('/admin/choose-tenant') ->assertSuccessful() - ->assertSee('No active tenants available') - ->assertSee('Workspace-level pages still work with no tenant selected') - ->assertSee('View managed tenants'); + ->assertSee(__('localization.shell.no_active_environments')) + ->assertSee(__('localization.shell.no_active_environments_description')) + ->assertSee(__('localization.shell.view_managed_environments')); }); it('keeps selector eligibility narrower than managed-tenant administrative discoverability', function (): void { @@ -119,7 +119,7 @@ WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id, ])->get(route('admin.operations.index', ['workspace' => $tenant->workspace])) ->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 { diff --git a/apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php b/apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php index f3e99ee4..b3c8314c 100644 --- a/apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php +++ b/apps/platform/tests/Feature/Workspaces/GlobalContextShellContractTest.php @@ -21,9 +21,9 @@ ->assertOk() ->assertSee($tenant->workspace()->firstOrFail()->name) ->assertSee('ManagedEnvironment Panel Entry') - ->assertSee('Switch tenant') - ->assertSee('Clear tenant scope') - ->assertDontSee(__('localization.shell.search_tenants')) + ->assertSee(__('localization.shell.switch_environment')) + ->assertSee(__('localization.shell.clear_environment_scope')) + ->assertDontSee(__('localization.shell.search_environments')) ->assertDontSee('admin/select-tenant'); }); @@ -38,6 +38,6 @@ ->withSession([WorkspaceContext::SESSION_KEY => (int) $workspaceTenant->workspace_id]) ->get(route('admin.operations.index', ['workspace' => $workspaceTenant->workspace, 'tenant' => $foreignTenant->external_id])) ->assertOk() - ->assertSee('No tenant selected') - ->assertDontSee('ManagedEnvironment scope: Rejected Foreign ManagedEnvironment'); + ->assertSee(__('localization.shell.no_environment_selected')) + ->assertDontSee(__('localization.shell.environment_scope').': Rejected Foreign ManagedEnvironment'); }); diff --git a/apps/platform/tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php b/apps/platform/tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php index 038bd16c..3c6c2fd3 100644 --- a/apps/platform/tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php +++ b/apps/platform/tests/Feature/Workspaces/Spec195ManagedTenantsLandingTest.php @@ -40,8 +40,8 @@ ->get(route('admin.workspace.managed-tenants.index', ['workspace' => $workspace])) ->assertSuccessful() ->assertSee('Spec195 Landing ManagedEnvironment') - ->assertSee('Managed tenants') - ->assertDontSee('No tenant selected'); + ->assertSee(__('localization.shell.managed_environments_title')) + ->assertDontSee(__('localization.shell.no_environment_selected')); }); 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)); }); -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']); $user = User::factory()->create(); @@ -103,5 +103,5 @@ Livewire::actingAs($user) ->test(ManagedTenantsLanding::class, ['workspace' => $workspace]) ->call('openTenant', $tenant->getKey()) - ->assertNotFound(); + ->assertRedirect(TenantDashboard::getUrl(tenant: $tenant)); }); diff --git a/apps/platform/tests/Unit/Tenants/TenantActionPolicySurfaceTest.php b/apps/platform/tests/Unit/Tenants/TenantActionPolicySurfaceTest.php index d958e3fd..7acb26ce 100644 --- a/apps/platform/tests/Unit/Tenants/TenantActionPolicySurfaceTest.php +++ b/apps/platform/tests/Unit/Tenants/TenantActionPolicySurfaceTest.php @@ -170,7 +170,7 @@ expect($descriptor->label)->toBe($expectedLabel); })->with([ - 'no drafts' => [0, 'Add tenant'], + 'no drafts' => [0, 'Add environment'], 'one draft' => [1, 'Resume onboarding'], 'multiple drafts' => [2, 'Choose onboarding draft'], ]); diff --git a/specs/286-ui-copy-ia-localization-neutralization/checklists/requirements.md b/specs/286-ui-copy-ia-localization-neutralization/checklists/requirements.md new file mode 100644 index 00000000..d1ca6619 --- /dev/null +++ b/specs/286-ui-copy-ia-localization-neutralization/checklists/requirements.md @@ -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. \ No newline at end of file diff --git a/specs/286-ui-copy-ia-localization-neutralization/contracts/ui-copy-ia-localization-neutralization.logical.openapi.yaml b/specs/286-ui-copy-ia-localization-neutralization/contracts/ui-copy-ia-localization-neutralization.logical.openapi.yaml new file mode 100644 index 00000000..4d0937ec --- /dev/null +++ b/specs/286-ui-copy-ia-localization-neutralization/contracts/ui-copy-ia-localization-neutralization.logical.openapi.yaml @@ -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 \ No newline at end of file diff --git a/specs/286-ui-copy-ia-localization-neutralization/data-model.md b/specs/286-ui-copy-ia-localization-neutralization/data-model.md new file mode 100644 index 00000000..b243bd64 --- /dev/null +++ b/specs/286-ui-copy-ia-localization-neutralization/data-model.md @@ -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 \ No newline at end of file diff --git a/specs/286-ui-copy-ia-localization-neutralization/plan.md b/specs/286-ui-copy-ia-localization-neutralization/plan.md new file mode 100644 index 00000000..33fd37eb --- /dev/null +++ b/specs/286-ui-copy-ia-localization-neutralization/plan.md @@ -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. \ No newline at end of file diff --git a/specs/286-ui-copy-ia-localization-neutralization/quickstart.md b/specs/286-ui-copy-ia-localization-neutralization/quickstart.md new file mode 100644 index 00000000..454492cf --- /dev/null +++ b/specs/286-ui-copy-ia-localization-neutralization/quickstart.md @@ -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`. \ No newline at end of file diff --git a/specs/286-ui-copy-ia-localization-neutralization/research.md b/specs/286-ui-copy-ia-localization-neutralization/research.md new file mode 100644 index 00000000..523ad683 --- /dev/null +++ b/specs/286-ui-copy-ia-localization-neutralization/research.md @@ -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. \ No newline at end of file diff --git a/specs/286-ui-copy-ia-localization-neutralization/spec.md b/specs/286-ui-copy-ia-localization-neutralization/spec.md new file mode 100644 index 00000000..43143d69 --- /dev/null +++ b/specs/286-ui-copy-ia-localization-neutralization/spec.md @@ -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 ` | 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. \ No newline at end of file diff --git a/specs/286-ui-copy-ia-localization-neutralization/tasks.md b/specs/286-ui-copy-ia-localization-neutralization/tasks.md new file mode 100644 index 00000000..91cbef36 --- /dev/null +++ b/specs/286-ui-copy-ia-localization-neutralization/tasks.md @@ -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. \ No newline at end of file