TenantAtlas/apps/platform/app/Filament/Pages/EnvironmentDiagnostics.php
ahmido 0a1ecf99c9 feat(ui): implement diagnostic entry point consolidation (#445)
Applied diagnostic surface contract rules to Audit Log inspect modal and Support Diagnostics action context, consolidating raw diagnostic data into safe modals according to Spec 374.

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #445
2026-06-13 01:16:00 +00:00

321 lines
12 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages;
use App\Filament\Concerns\ResolvesPanelTenantContext;
use App\Models\ManagedEnvironment;
use App\Models\User;
use App\Services\Audit\WorkspaceAuditLogger;
use App\Services\Auth\ManagedEnvironmentDiagnosticsService;
use App\Services\Auth\ManagedEnvironmentMembershipManager;
use App\Support\Auth\Capabilities;
use App\Support\ProductTelemetry\ProductTelemetryRecorder;
use App\Support\ProductTelemetry\ProductUsageEventCatalog;
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\SupportDiagnostics\SupportDiagnosticBundleBuilder;
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;
use Illuminate\Contracts\View\View;
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 function getTitle(): string
{
return 'Repair diagnostics';
}
public function getSubheading(): string
{
return 'Checks supported TenantPilot access and membership repair conditions only.';
}
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
{
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly)
->satisfy(ActionSurfaceSlot::ListHeader, 'Header exposes a read-only support diagnostics modal plus capability-gated tenant repair actions when inconsistent membership state is detected.')
->exempt(ActionSurfaceSlot::InspectAffordance, 'Repair diagnostics is already the singleton repair diagnostics 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;
/**
* @var list<string>
*/
public array $supportDiagnosticsAuditKeys = [];
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 repair diagnostics are active',
'body' => 'No supported access or membership repair is active for this managed environment.',
'status' => 'No repair action',
'color' => 'gray',
'impact' => 'Repair diagnostics only checks existing TenantPilot access and membership repair conditions; it is not a generic environment health hub.',
'next_check' => 'Use Open support diagnostics for broader provider, operation, evidence, or audit context.',
'primary_action_label' => null,
'secondary_action_label' => null,
'blockers' => [],
];
}
$primaryBlocker = $blockers[0];
$secondaryBlocker = $blockers[1] ?? null;
return [
'headline' => $blockerCount === 1
? '1 repair diagnostic needs attention'
: $blockerCount.' repair diagnostics need attention',
'body' => 'Repair diagnostics checks supported TenantPilot access and membership defects only. Resolve the highest-impact 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 [
$this->openSupportDiagnosticsAction(),
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),
];
}
private function openSupportDiagnosticsAction(): Action
{
$action = Action::make('openSupportDiagnostics')
->label('Open support diagnostics')
->icon('heroicon-o-lifebuoy')
->color('gray')
->modal()
->slideOver()
->stickyModalHeader()
->modalHeading(__('localization.dashboard.support_diagnostics'))
->modalDescription(__('localization.dashboard.support_diagnostics_description'))
->modalSubmitAction(false)
->modalCancelAction(fn (Action $action): Action => $action->label(__('localization.dashboard.close')))
->mountUsing(function (): void {
$this->auditTenantSupportDiagnosticsOpen();
})
->modalContent(fn (): View => view('filament.modals.support-diagnostic-bundle', [
'bundle' => $this->tenantSupportDiagnosticBundle(),
]));
return UiEnforcement::forScopedAction(
$action,
fn (): UiActionContext => static::tenantUiActionContext(),
)
->requireCapability(Capabilities::SUPPORT_DIAGNOSTICS_VIEW)
->apply();
}
/**
* @return array<string, mixed>
*/
public function tenantSupportDiagnosticBundle(): array
{
$tenant = static::resolveTenantContextForCurrentPanelOrFail();
$user = auth()->user();
if (! $user instanceof User) {
abort(403, 'Not allowed');
}
return app(SupportDiagnosticBundleBuilder::class)->forTenant($tenant, $user);
}
private function auditTenantSupportDiagnosticsOpen(): void
{
$tenant = static::resolveTenantContextForCurrentPanelOrFail();
$user = auth()->user();
if (! $user instanceof User) {
abort(403, 'Not allowed');
}
$this->recordSupportDiagnosticsOpened(
tenant: $tenant,
bundle: $this->tenantSupportDiagnosticBundle(),
user: $user,
);
}
/**
* @param array<string, mixed> $bundle
*/
private function recordSupportDiagnosticsOpened(ManagedEnvironment $tenant, array $bundle, User $user): void
{
$auditKey = 'tenant:'.$tenant->getKey();
if (in_array($auditKey, $this->supportDiagnosticsAuditKeys, true)) {
return;
}
app(WorkspaceAuditLogger::class)->logSupportDiagnosticsOpened(
tenant: $tenant,
contextType: 'tenant',
bundle: $bundle,
actor: $user,
);
app(ProductTelemetryRecorder::class)->record(
eventName: ProductUsageEventCatalog::SUPPORT_DIAGNOSTICS_OPENED,
workspaceId: (int) $tenant->workspace_id,
tenantId: (int) $tenant->getKey(),
userId: (int) $user->getKey(),
subjectType: 'tenant',
subjectId: (int) $tenant->getKey(),
metadata: [
'source_surface' => 'repair_diagnostics',
],
);
$this->supportDiagnosticsAuditKeys[] = $auditKey;
}
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();
}
}