getId() !== 'admin') { return false; } return parent::shouldRegisterNavigation(); } public static function canViewAny(): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } $workspace = self::resolveWorkspace(); if (! $workspace instanceof Workspace) { return false; } $resolver = app(\App\Services\Auth\WorkspaceCapabilityResolver::class); return $resolver->isMember($user, $workspace) && $resolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_VIEW); } public static function canCreate(): bool { return self::hasManageCapability(); } public static function canEdit(Model $record): bool { return self::hasManageCapability(); } public static function canDelete(Model $record): bool { return self::hasManageCapability(); } public static function canView(Model $record): bool { return self::canViewAny(); } public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration { return ActionSurfaceDeclaration::forResource(ActionSurfaceProfile::CrudListAndView) ->satisfy(ActionSurfaceSlot::ListHeader, 'Header action: Create baseline profile (capability-gated).') ->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ViewAction->value) ->satisfy(ActionSurfaceSlot::ListRowMoreMenu, 'Secondary row actions (edit, archive) under "More".') ->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'No bulk mutations for baseline profiles in v1.') ->satisfy(ActionSurfaceSlot::ListEmptyState, 'List defines empty-state create CTA.') ->satisfy(ActionSurfaceSlot::DetailHeader, 'View page provides capture + edit actions.'); } public static function getEloquentQuery(): Builder { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); return parent::getEloquentQuery() ->with(['activeSnapshot', 'createdByUser']) ->when( $workspaceId !== null, fn (Builder $query): Builder => $query->where('workspace_id', (int) $workspaceId), ) ->when( $workspaceId === null, fn (Builder $query): Builder => $query->whereRaw('1 = 0'), ); } public static function form(Schema $schema): Schema { return $schema ->schema([ Section::make('Profile') ->schema([ TextInput::make('name') ->required() ->maxLength(255) ->rule(fn (?BaselineProfile $record): Unique => Rule::unique('baseline_profiles', 'name') ->where('workspace_id', $record?->workspace_id ?? app(WorkspaceContext::class)->currentWorkspaceId(request())) ->ignore($record)) ->helperText('A descriptive name for this baseline profile.'), Textarea::make('description') ->rows(3) ->maxLength(1000) ->helperText('Explain the purpose and scope of this baseline.'), ]), Section::make('Controls') ->schema([ Select::make('status') ->required() ->options(fn (?BaselineProfile $record): array => self::statusOptionsForRecord($record)) ->default(BaselineProfileStatus::Draft->value) ->native(false) ->disabled(fn (?BaselineProfile $record): bool => $record?->status === BaselineProfileStatus::Archived) ->helperText(fn (?BaselineProfile $record): string => match ($record?->status) { BaselineProfileStatus::Archived => 'Archived baselines cannot be reactivated.', BaselineProfileStatus::Active => 'Changing status to Archived is permanent.', default => 'Only active baselines are enforced during compliance checks.', }), Select::make('capture_mode') ->label('Capture mode') ->required() ->options(BaselineCaptureMode::selectOptions()) ->default(BaselineCaptureMode::Opportunistic->value) ->native(false) ->disabled(fn (?BaselineProfile $record): bool => $record?->status === BaselineProfileStatus::Archived) ->disableOptionWhen(function (string $value): bool { if ($value !== BaselineCaptureMode::FullContent->value) { return false; } return ! app(BaselineFullContentRolloutGate::class)->enabled(); }) ->helperText(fn (): string => app(BaselineFullContentRolloutGate::class)->enabled() ? 'Full content capture enables deep drift detection by capturing policy evidence on demand.' : 'Full content capture is currently disabled by rollout configuration.'), TextInput::make('version_label') ->label('Version label') ->maxLength(50) ->placeholder('e.g. v2.1 — February rollout') ->helperText('Optional label to identify this version.'), Select::make('scope_jsonb.policy_types') ->label('Policy types') ->multiple() ->options(self::policyTypeOptions()) ->helperText('Leave empty to include all supported policy types (excluding foundations).') ->native(false), Select::make('scope_jsonb.foundation_types') ->label('Foundations') ->multiple() ->options(self::foundationTypeOptions()) ->helperText('Leave empty to exclude foundations. Select foundations to include them.') ->native(false), Placeholder::make('metadata') ->label('Last modified') ->content(fn (?BaselineProfile $record): string => $record?->updated_at ? $record->updated_at->diffForHumans() : '—') ->visible(fn (?BaselineProfile $record): bool => $record !== null), ]) ->columns(2), ]); } public static function infolist(Schema $schema): Schema { return $schema ->schema([ Section::make('Profile') ->schema([ TextEntry::make('name'), TextEntry::make('status') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::BaselineProfileStatus)) ->color(BadgeRenderer::color(BadgeDomain::BaselineProfileStatus)) ->icon(BadgeRenderer::icon(BadgeDomain::BaselineProfileStatus)), TextEntry::make('capture_mode') ->label('Capture mode') ->badge() ->formatStateUsing(function (mixed $state): string { if ($state instanceof BaselineCaptureMode) { return $state->label(); } $parsed = is_string($state) ? BaselineCaptureMode::tryFrom($state) : null; return $parsed?->label() ?? (is_string($state) ? $state : '—'); }) ->color(function (mixed $state): string { $mode = $state instanceof BaselineCaptureMode ? $state : (is_string($state) ? BaselineCaptureMode::tryFrom($state) : null); return match ($mode) { BaselineCaptureMode::FullContent => 'success', BaselineCaptureMode::Opportunistic => 'warning', BaselineCaptureMode::MetaOnly => 'gray', default => 'gray', }; }), TextEntry::make('version_label') ->label('Version') ->placeholder('—'), TextEntry::make('description') ->placeholder('No description') ->columnSpanFull(), ]) ->columns(2) ->columnSpanFull(), Section::make('Scope') ->schema([ TextEntry::make('scope_jsonb.policy_types') ->label('Policy types') ->badge() ->formatStateUsing(function (string $state): string { $options = self::policyTypeOptions(); return $options[$state] ?? $state; }) ->placeholder('All supported policy types (excluding foundations)'), TextEntry::make('scope_jsonb.foundation_types') ->label('Foundations') ->badge() ->formatStateUsing(function (string $state): string { $options = self::foundationTypeOptions(); return $options[$state] ?? $state; }) ->placeholder('None'), ]) ->columnSpanFull(), Section::make('Baseline truth') ->schema([ TextEntry::make('current_snapshot_truth') ->label('Current snapshot') ->state(fn (BaselineProfile $record): string => self::currentSnapshotLabel($record)), TextEntry::make('latest_attempted_snapshot_truth') ->label('Latest attempt') ->state(fn (BaselineProfile $record): string => self::latestAttemptedSnapshotLabel($record)), TextEntry::make('compare_readiness') ->label('Compare readiness') ->badge() ->state(fn (BaselineProfile $record): string => self::compareReadinessLabel($record)) ->color(fn (BaselineProfile $record): string => self::compareReadinessColor($record)) ->icon(fn (BaselineProfile $record): ?string => self::compareReadinessIcon($record)), TextEntry::make('baseline_next_step') ->label('Next step') ->state(fn (BaselineProfile $record): string => self::profileNextStep($record)) ->columnSpanFull(), ]) ->columns(2) ->columnSpanFull(), Section::make('Metadata') ->schema([ TextEntry::make('createdByUser.name') ->label('Created by') ->placeholder('—'), TextEntry::make('created_at') ->dateTime(), TextEntry::make('updated_at') ->dateTime(), ]) ->columns(2) ->columnSpanFull(), ]); } public static function table(Table $table): Table { $workspace = self::resolveWorkspace(); return $table ->defaultSort('name') ->paginated(\App\Support\Filament\TablePaginationProfiles::resource()) ->columns([ TextColumn::make('name') ->searchable() ->sortable(), TextColumn::make('status') ->badge() ->formatStateUsing(BadgeRenderer::label(BadgeDomain::BaselineProfileStatus)) ->color(BadgeRenderer::color(BadgeDomain::BaselineProfileStatus)) ->icon(BadgeRenderer::icon(BadgeDomain::BaselineProfileStatus)) ->sortable(), TextColumn::make('capture_mode') ->label('Capture mode') ->badge() ->formatStateUsing(function (mixed $state): string { if ($state instanceof BaselineCaptureMode) { return $state->label(); } $parsed = is_string($state) ? BaselineCaptureMode::tryFrom($state) : null; return $parsed?->label() ?? (is_string($state) ? $state : '—'); }) ->color(function (mixed $state): string { $mode = $state instanceof BaselineCaptureMode ? $state : (is_string($state) ? BaselineCaptureMode::tryFrom($state) : null); return match ($mode) { BaselineCaptureMode::FullContent => 'success', BaselineCaptureMode::Opportunistic => 'warning', BaselineCaptureMode::MetaOnly => 'gray', default => 'gray', }; }) ->toggleable(isToggledHiddenByDefault: true), TextColumn::make('version_label') ->label('Version') ->placeholder('—'), TextColumn::make('tenant_assignments_count') ->label('Assigned tenants') ->counts('tenantAssignments'), TextColumn::make('current_snapshot_truth') ->label('Current snapshot') ->getStateUsing(fn (BaselineProfile $record): string => self::currentSnapshotLabel($record)) ->description(fn (BaselineProfile $record): ?string => self::currentSnapshotDescription($record)) ->wrap(), TextColumn::make('latest_attempted_snapshot_truth') ->label('Latest attempt') ->getStateUsing(fn (BaselineProfile $record): string => self::latestAttemptedSnapshotLabel($record)) ->description(fn (BaselineProfile $record): ?string => self::latestAttemptedSnapshotDescription($record)) ->wrap(), TextColumn::make('compare_readiness') ->label('Compare readiness') ->badge() ->getStateUsing(fn (BaselineProfile $record): string => self::compareReadinessLabel($record)) ->color(fn (BaselineProfile $record): string => self::compareReadinessColor($record)) ->icon(fn (BaselineProfile $record): ?string => self::compareReadinessIcon($record)) ->wrap(), TextColumn::make('baseline_next_step') ->label('Next step') ->getStateUsing(fn (BaselineProfile $record): string => self::profileNextStep($record)) ->wrap(), TextColumn::make('created_at') ->dateTime() ->sortable() ->toggleable(isToggledHiddenByDefault: true), ]) ->filters([ \Filament\Tables\Filters\SelectFilter::make('status') ->options(FilterOptionCatalog::baselineProfileStatuses()), ]) ->actions([ Action::make('view') ->label('View') ->url(fn (BaselineProfile $record): string => static::getUrl('view', ['record' => $record])) ->icon('heroicon-o-eye'), ActionGroup::make([ Action::make('edit') ->label('Edit') ->url(fn (BaselineProfile $record): string => static::getUrl('edit', ['record' => $record])) ->icon('heroicon-o-pencil-square') ->visible(fn (): bool => self::hasManageCapability()), self::archiveTableAction($workspace), ])->label('More'), ]) ->bulkActions([ BulkActionGroup::make([])->label('More'), ]) ->emptyStateHeading('No baseline profiles') ->emptyStateDescription('Create a baseline profile to define what "good" looks like for your tenants.') ->emptyStateActions([ Action::make('create') ->label('Create baseline profile') ->url(fn (): string => static::getUrl('create')) ->icon('heroicon-o-plus') ->visible(fn (): bool => self::hasManageCapability()), ]); } public static function getRelations(): array { return [ BaselineProfileResource\RelationManagers\BaselineTenantAssignmentsRelationManager::class, ]; } public static function getPages(): array { return [ 'index' => Pages\ListBaselineProfiles::route('/'), 'create' => Pages\CreateBaselineProfile::route('/create'), 'view' => Pages\ViewBaselineProfile::route('/{record}'), 'edit' => Pages\EditBaselineProfile::route('/{record}/edit'), ]; } /** * @return array */ public static function policyTypeOptions(): array { return collect(InventoryPolicyTypeMeta::supported()) ->filter(fn (array $row): bool => filled($row['type'] ?? null)) ->mapWithKeys(fn (array $row): array => [ (string) $row['type'] => (string) ($row['label'] ?? $row['type']), ]) ->sort() ->all(); } /** * @return array */ public static function foundationTypeOptions(): array { return collect(InventoryPolicyTypeMeta::baselineSupportedFoundations()) ->filter(fn (array $row): bool => filled($row['type'] ?? null)) ->mapWithKeys(fn (array $row): array => [ (string) $row['type'] => InventoryPolicyTypeMeta::baselineCompareLabel((string) $row['type']) ?? (string) ($row['label'] ?? $row['type']), ]) ->sort() ->all(); } /** * @param array $metadata */ public static function audit(BaselineProfile $record, AuditActionId $actionId, array $metadata): void { $workspace = $record->workspace; if ($workspace === null) { return; } $actor = auth()->user(); app(WorkspaceAuditLogger::class)->log( workspace: $workspace, action: $actionId->value, context: ['metadata' => $metadata], actor: $actor instanceof User ? $actor : null, resourceType: 'baseline_profile', resourceId: (string) $record->getKey(), ); } /** * Status options scoped to valid transitions from the current record state. * * @return array */ private static function statusOptionsForRecord(?BaselineProfile $record): array { if ($record === null) { return [BaselineProfileStatus::Draft->value => BaselineProfileStatus::Draft->label()]; } $currentStatus = $record->status instanceof BaselineProfileStatus ? $record->status : (BaselineProfileStatus::tryFrom((string) $record->status) ?? BaselineProfileStatus::Draft); return $currentStatus->selectOptions(); } private static function resolveWorkspace(): ?Workspace { $workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request()); if ($workspaceId === null) { return null; } return Workspace::query()->whereKey($workspaceId)->first(); } private static function hasManageCapability(): bool { $user = auth()->user(); $workspace = self::resolveWorkspace(); if (! $user instanceof User || ! $workspace instanceof Workspace) { return false; } $resolver = app(\App\Services\Auth\WorkspaceCapabilityResolver::class); return $resolver->isMember($user, $workspace) && $resolver->can($user, $workspace, Capabilities::WORKSPACE_BASELINES_MANAGE); } private static function archiveTableAction(?Workspace $workspace): Action { $action = Action::make('archive') ->label('Archive') ->icon('heroicon-o-archive-box') ->color('warning') ->requiresConfirmation() ->modalHeading('Archive baseline profile') ->modalDescription('Archiving is permanent in v1. This profile can no longer be used for captures or compares.') ->hidden(fn (BaselineProfile $record): bool => $record->status === BaselineProfileStatus::Archived) ->action(function (BaselineProfile $record): void { if (! self::hasManageCapability()) { throw new AuthorizationException; } $record->forceFill(['status' => BaselineProfileStatus::Archived->value])->save(); self::audit($record, AuditActionId::BaselineProfileArchived, [ 'baseline_profile_id' => (int) $record->getKey(), 'name' => (string) $record->name, ]); Notification::make() ->title('Baseline profile archived') ->success() ->send(); }); if ($workspace instanceof Workspace) { $action = WorkspaceUiEnforcement::forTableAction($action, $workspace) ->requireCapability(Capabilities::WORKSPACE_BASELINES_MANAGE) ->destructive() ->apply(); } return $action; } private static function currentSnapshotLabel(BaselineProfile $profile): string { $snapshot = self::effectiveSnapshot($profile); if (! $snapshot instanceof BaselineSnapshot) { return 'No complete snapshot'; } return self::snapshotReference($snapshot); } private static function currentSnapshotDescription(BaselineProfile $profile): ?string { $snapshot = self::effectiveSnapshot($profile); if (! $snapshot instanceof BaselineSnapshot) { return self::compareAvailabilityEnvelope($profile)?->shortExplanation; } return $snapshot->captured_at?->toDayDateTimeString(); } private static function latestAttemptedSnapshotLabel(BaselineProfile $profile): string { $latestAttempt = self::latestAttemptedSnapshot($profile); if (! $latestAttempt instanceof BaselineSnapshot) { return 'No capture attempts yet'; } $effectiveSnapshot = self::effectiveSnapshot($profile); if ($effectiveSnapshot instanceof BaselineSnapshot && (int) $effectiveSnapshot->getKey() === (int) $latestAttempt->getKey()) { return 'Matches current snapshot'; } return self::snapshotReference($latestAttempt); } private static function latestAttemptedSnapshotDescription(BaselineProfile $profile): ?string { $latestAttempt = self::latestAttemptedSnapshot($profile); if (! $latestAttempt instanceof BaselineSnapshot) { return null; } $effectiveSnapshot = self::effectiveSnapshot($profile); if ($effectiveSnapshot instanceof BaselineSnapshot && (int) $effectiveSnapshot->getKey() === (int) $latestAttempt->getKey()) { return 'No newer attempt is pending.'; } return $latestAttempt->captured_at?->toDayDateTimeString(); } private static function compareReadinessLabel(BaselineProfile $profile): string { return self::compareAvailabilityEnvelope($profile)?->operatorLabel ?? 'Ready'; } private static function compareReadinessColor(BaselineProfile $profile): string { return match (self::compareAvailabilityReason($profile)) { null => 'success', BaselineReasonCodes::COMPARE_PROFILE_NOT_ACTIVE => 'gray', default => 'warning', }; } private static function compareReadinessIcon(BaselineProfile $profile): ?string { return match (self::compareAvailabilityReason($profile)) { null => 'heroicon-m-check-badge', BaselineReasonCodes::COMPARE_PROFILE_NOT_ACTIVE => 'heroicon-m-pause-circle', default => 'heroicon-m-exclamation-triangle', }; } private static function profileNextStep(BaselineProfile $profile): string { return self::compareAvailabilityEnvelope($profile)?->guidanceText() ?? 'No action needed.'; } private static function effectiveSnapshot(BaselineProfile $profile): ?BaselineSnapshot { return app(BaselineSnapshotTruthResolver::class)->resolveEffectiveSnapshot($profile); } private static function latestAttemptedSnapshot(BaselineProfile $profile): ?BaselineSnapshot { return app(BaselineSnapshotTruthResolver::class)->resolveLatestAttemptedSnapshot($profile); } private static function compareAvailabilityReason(BaselineProfile $profile): ?string { $status = $profile->status instanceof BaselineProfileStatus ? $profile->status : BaselineProfileStatus::tryFrom((string) $profile->status); if ($status !== BaselineProfileStatus::Active) { return BaselineReasonCodes::COMPARE_PROFILE_NOT_ACTIVE; } $resolution = app(BaselineSnapshotTruthResolver::class)->resolveCompareSnapshot($profile); $reasonCode = $resolution['reason_code'] ?? null; if (is_string($reasonCode) && trim($reasonCode) !== '') { return trim($reasonCode); } if (! self::hasEligibleCompareTarget($profile)) { return BaselineReasonCodes::COMPARE_NO_ELIGIBLE_TARGET; } return null; } private static function compareAvailabilityEnvelope(BaselineProfile $profile): ?ReasonResolutionEnvelope { $reasonCode = self::compareAvailabilityReason($profile); if (! is_string($reasonCode)) { return null; } return app(ReasonPresenter::class)->forArtifactTruth($reasonCode, 'artifact_truth'); } private static function snapshotReference(BaselineSnapshot $snapshot): string { $lifecycleLabel = BadgeCatalog::spec(BadgeDomain::BaselineSnapshotLifecycle, $snapshot->lifecycleState()->value)->label; return sprintf('Snapshot #%d (%s)', (int) $snapshot->getKey(), $lifecycleLabel); } private static function hasEligibleCompareTarget(BaselineProfile $profile): bool { $user = auth()->user(); if (! $user instanceof User) { return false; } $tenantIds = BaselineTenantAssignment::query() ->where('workspace_id', (int) $profile->workspace_id) ->where('baseline_profile_id', (int) $profile->getKey()) ->pluck('tenant_id') ->all(); if ($tenantIds === []) { return false; } $resolver = app(CapabilityResolver::class); return Tenant::query() ->where('workspace_id', (int) $profile->workspace_id) ->whereIn('id', $tenantIds) ->get(['id']) ->contains(fn (Tenant $tenant): bool => $resolver->can($user, $tenant, Capabilities::TENANT_SYNC)); } }