Applied the decision-first diagnostic surface IA contract to EnvironmentDiagnostics and SupportDiagnostics bundles. Added recommended_first_check and separated technical metadata as per Spec 373. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #444
206 lines
8.2 KiB
PHP
206 lines
8.2 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages;
|
|
|
|
use App\Filament\Concerns\ResolvesPanelTenantContext;
|
|
use App\Models\User;
|
|
use App\Services\Auth\ManagedEnvironmentDiagnosticsService;
|
|
use App\Services\Auth\ManagedEnvironmentMembershipManager;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Rbac\Actions\ResolvesUiActionContext;
|
|
use App\Support\Rbac\Actions\UiActionContext;
|
|
use App\Support\Rbac\UiEnforcement;
|
|
use App\Support\Rbac\UiTooltips;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
|
use Filament\Actions\Action;
|
|
use Filament\Pages\Page;
|
|
|
|
class EnvironmentDiagnostics extends Page
|
|
{
|
|
use ResolvesPanelTenantContext;
|
|
use ResolvesUiActionContext;
|
|
|
|
protected static bool $shouldRegisterNavigation = false;
|
|
|
|
protected static ?string $slug = 'diagnostics';
|
|
|
|
protected string $view = 'filament.pages.environment-diagnostics';
|
|
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
|
|
->satisfy(ActionSurfaceSlot::ListHeader, 'Header exposes capability-gated tenant repair actions when inconsistent membership state is detected.')
|
|
->exempt(ActionSurfaceSlot::InspectAffordance, 'ManagedEnvironment diagnostics is already the singleton diagnostic surface for the active tenant.')
|
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The diagnostics page does not render row-level secondary actions.')
|
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The diagnostics page does not expose bulk actions.')
|
|
->exempt(ActionSurfaceSlot::ListEmptyState, 'Diagnostics content is always rendered instead of a list-style empty state.');
|
|
}
|
|
|
|
public bool $missingOwner = false;
|
|
|
|
public bool $hasDuplicateMembershipsForCurrentUser = false;
|
|
|
|
public function mount(): void
|
|
{
|
|
$tenant = static::resolveTenantContextForCurrentPanelOrFail();
|
|
$this->missingOwner = app(ManagedEnvironmentDiagnosticsService::class)->tenantHasNoOwners($tenant);
|
|
|
|
$user = auth()->user();
|
|
if (! $user instanceof User) {
|
|
abort(403, 'Not allowed');
|
|
}
|
|
|
|
$this->hasDuplicateMembershipsForCurrentUser = app(ManagedEnvironmentDiagnosticsService::class)
|
|
->userHasDuplicateMemberships($tenant, $user);
|
|
}
|
|
|
|
/**
|
|
* @return array{
|
|
* headline: string,
|
|
* body: string,
|
|
* status: string,
|
|
* color: string,
|
|
* impact: string,
|
|
* next_check: string,
|
|
* primary_action_label: ?string,
|
|
* secondary_action_label: ?string,
|
|
* blockers: list<array{
|
|
* key: string,
|
|
* label: string,
|
|
* failed_condition: string,
|
|
* impact: string,
|
|
* next_check: string,
|
|
* action_label: string,
|
|
* action_role: string
|
|
* }>
|
|
* }
|
|
*/
|
|
public function diagnosticSummary(): array
|
|
{
|
|
$blockers = [];
|
|
|
|
if ($this->missingOwner) {
|
|
$blockers[] = [
|
|
'key' => 'missing_owner',
|
|
'label' => 'Missing owner',
|
|
'failed_condition' => 'No Owner membership is currently visible for this managed environment.',
|
|
'impact' => 'Tenant repair and accountability workflows need an owner before support can treat access as complete.',
|
|
'next_check' => 'Confirm workspace role recovery, then use Bootstrap owner only when the current administrator is authorized to repair tenant scope.',
|
|
'action_label' => 'Bootstrap owner',
|
|
'action_role' => 'Primary repair path',
|
|
];
|
|
}
|
|
|
|
if ($this->hasDuplicateMembershipsForCurrentUser) {
|
|
$blockers[] = [
|
|
'key' => 'duplicate_memberships',
|
|
'label' => 'Duplicate memberships',
|
|
'failed_condition' => 'The current user has more than one tenant membership row for this managed environment.',
|
|
'impact' => 'Duplicate access scope rows make authorization support harder to reason about for this user.',
|
|
'next_check' => 'Merge the duplicate rows, then reload the diagnostics page to confirm only one membership remains.',
|
|
'action_label' => 'Merge duplicate access scopes',
|
|
'action_role' => count($blockers) === 0 ? 'Primary repair path' : 'Secondary repair path',
|
|
];
|
|
}
|
|
|
|
$blockerCount = count($blockers);
|
|
|
|
if ($blockerCount === 0) {
|
|
return [
|
|
'headline' => 'No diagnostic action is required',
|
|
'body' => 'No actionable tenant membership defect is visible for the current managed-environment context.',
|
|
'status' => 'No action required',
|
|
'color' => 'gray',
|
|
'impact' => 'This page did not find a tenant membership repair to run for the current user and environment.',
|
|
'next_check' => 'Review provider, operation, or audit surfaces only when another page reports a blocker.',
|
|
'primary_action_label' => null,
|
|
'secondary_action_label' => null,
|
|
'blockers' => [],
|
|
];
|
|
}
|
|
|
|
$primaryBlocker = $blockers[0];
|
|
$secondaryBlocker = $blockers[1] ?? null;
|
|
|
|
return [
|
|
'headline' => $blockerCount === 1
|
|
? '1 diagnostic blocker needs attention'
|
|
: $blockerCount.' diagnostic blockers need attention',
|
|
'body' => 'Resolve the highest-impact tenant membership blocker first; lower repair paths remain visible for context.',
|
|
'status' => $blockerCount === 1 ? 'Action needed' : $blockerCount.' blockers',
|
|
'color' => 'warning',
|
|
'impact' => $primaryBlocker['impact'],
|
|
'next_check' => $primaryBlocker['next_check'],
|
|
'primary_action_label' => $primaryBlocker['action_label'],
|
|
'secondary_action_label' => $secondaryBlocker['action_label'] ?? null,
|
|
'blockers' => $blockers,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
UiEnforcement::forScopedAction(
|
|
Action::make('bootstrapOwner')
|
|
->label('Bootstrap owner')
|
|
->requiresConfirmation()
|
|
->action(fn () => $this->bootstrapOwner()),
|
|
fn (): UiActionContext => static::tenantUiActionContext(),
|
|
)
|
|
->requireCapability(Capabilities::TENANT_MANAGE)
|
|
->destructive()
|
|
->tooltip(UiTooltips::INSUFFICIENT_PERMISSION)
|
|
->apply()
|
|
->visible(fn (): bool => $this->missingOwner),
|
|
|
|
UiEnforcement::forScopedAction(
|
|
Action::make('mergeDuplicateMemberships')
|
|
->label('Merge duplicate access scopes')
|
|
->requiresConfirmation()
|
|
->action(fn () => $this->mergeDuplicateMemberships()),
|
|
fn (): UiActionContext => static::tenantUiActionContext(),
|
|
)
|
|
->requireCapability(Capabilities::TENANT_MANAGE)
|
|
->destructive()
|
|
->tooltip(UiTooltips::INSUFFICIENT_PERMISSION)
|
|
->apply()
|
|
->visible(fn (): bool => $this->hasDuplicateMembershipsForCurrentUser),
|
|
];
|
|
}
|
|
|
|
public function bootstrapOwner(): void
|
|
{
|
|
$tenant = static::resolveTenantContextForCurrentPanelOrFail();
|
|
|
|
$user = auth()->user();
|
|
if (! $user instanceof User) {
|
|
abort(403, 'Not allowed');
|
|
}
|
|
|
|
app(ManagedEnvironmentMembershipManager::class)->grantScope($tenant, $user, $user, sourceRef: 'diagnostic');
|
|
|
|
$this->mount();
|
|
}
|
|
|
|
public function mergeDuplicateMemberships(): void
|
|
{
|
|
$tenant = static::resolveTenantContextForCurrentPanelOrFail();
|
|
|
|
$user = auth()->user();
|
|
if (! $user instanceof User) {
|
|
abort(403, 'Not allowed');
|
|
}
|
|
|
|
app(ManagedEnvironmentDiagnosticsService::class)->mergeDuplicateMembershipsForUser($tenant, $user, $user);
|
|
|
|
$this->mount();
|
|
}
|
|
}
|