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
321 lines
12 KiB
PHP
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();
|
|
}
|
|
}
|