Some checks failed
Main Confidence / confidence (push) Failing after 1m23s
Removes the Findings lifecycle backfill from the Operational Controls UI and OperationalControlCatalog. This patch is a safe, controls-only change; runbooks, jobs and other runtime artifacts are NOT removed yet. Follow-up work will delete the runbook service/scope, jobs, commands, and update tests. Files changed: - apps/platform/app/Filament/System/Pages/Ops/Controls.php - apps/platform/app/Support/OperationalControls/OperationalControlCatalog.php - apps/platform/tests/Feature/System/OpsControls/OperationalControlManagementTest.php - apps/platform/tests/Unit/Support/OperationalControls/OperationalControlCatalogTest.php - apps/platform/tests/Unit/Support/OperationalControls/OperationalControlScopeResolutionTest.php Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #280
660 lines
24 KiB
PHP
660 lines
24 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\System\Pages\Ops;
|
|
|
|
use App\Models\AuditLog;
|
|
use App\Models\OperationalControlActivation;
|
|
use App\Models\PlatformUser;
|
|
use App\Models\Tenant;
|
|
use App\Models\Workspace;
|
|
use App\Services\Audit\AuditRecorder;
|
|
use App\Services\Audit\WorkspaceAuditLogger;
|
|
use App\Support\Audit\AuditActionId;
|
|
use App\Support\Audit\AuditActorSnapshot;
|
|
use App\Support\Audit\AuditTargetSnapshot;
|
|
use App\Support\Auth\PlatformCapabilities;
|
|
use App\Support\OperationalControls\OperationalControlCatalog;
|
|
use Carbon\Carbon;
|
|
use Carbon\CarbonInterface;
|
|
use Filament\Actions\Action;
|
|
use Filament\Forms\Components\DateTimePicker;
|
|
use Filament\Forms\Components\Placeholder;
|
|
use Filament\Forms\Components\Radio;
|
|
use Filament\Forms\Components\Select;
|
|
use Filament\Forms\Components\Textarea;
|
|
use Filament\Notifications\Notification;
|
|
use Filament\Pages\Page;
|
|
use Illuminate\Database\Eloquent\Collection;
|
|
use Illuminate\Contracts\View\View;
|
|
use Illuminate\Support\Arr;
|
|
use Illuminate\Validation\ValidationException;
|
|
|
|
class Controls extends Page
|
|
{
|
|
protected static ?string $navigationLabel = 'Controls';
|
|
|
|
protected static ?string $title = 'Operational Controls';
|
|
|
|
protected static string|\BackedEnum|null $navigationIcon = 'heroicon-o-pause-circle';
|
|
|
|
protected static string|\UnitEnum|null $navigationGroup = 'Ops';
|
|
|
|
protected static ?string $slug = 'ops/controls';
|
|
|
|
protected string $view = 'filament.system.pages.ops.controls';
|
|
|
|
public static function canAccess(): bool
|
|
{
|
|
$user = auth('platform')->user();
|
|
|
|
if (! $user instanceof PlatformUser) {
|
|
return false;
|
|
}
|
|
|
|
return $user->hasCapability(PlatformCapabilities::ACCESS_SYSTEM_PANEL)
|
|
&& $user->hasCapability(PlatformCapabilities::OPS_CONTROLS_MANAGE);
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
abort_unless(static::canAccess(), 403);
|
|
}
|
|
|
|
public function getHeader(): ?View
|
|
{
|
|
return view('filament.system.pages.ops.partials.controls-header', [
|
|
'breadcrumbs' => filament()->hasBreadcrumbs() ? $this->getBreadcrumbs() : [],
|
|
'heading' => $this->getHeading(),
|
|
'subheading' => $this->getSubheading(),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
$this->pauseRestoreExecuteAction(),
|
|
$this->resumeRestoreExecuteAction(),
|
|
$this->viewHistoryRestoreExecuteAction(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, array<string, mixed>>
|
|
*/
|
|
public function controlCards(): array
|
|
{
|
|
$catalog = app(OperationalControlCatalog::class);
|
|
|
|
return array_map(
|
|
fn (string $controlKey): array => $this->controlSummary($controlKey),
|
|
$catalog->keys(),
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function controlSummary(string $controlKey): array
|
|
{
|
|
$definition = app(OperationalControlCatalog::class)->definition($controlKey);
|
|
$activations = $this->activeActivationsForControl($controlKey);
|
|
|
|
$effectiveState = $activations->isEmpty() ? 'enabled' : 'paused';
|
|
$stateLabel = match (true) {
|
|
$activations->contains(fn (OperationalControlActivation $activation): bool => $activation->scope_type === 'global') => 'Paused globally',
|
|
$activations->isNotEmpty() => sprintf('Workspace pauses active (%d)', $activations->where('scope_type', 'workspace')->count()),
|
|
default => 'Enabled',
|
|
};
|
|
|
|
return [
|
|
'control_key' => $controlKey,
|
|
'action_slug' => $this->actionSlug($controlKey),
|
|
'label' => (string) $definition['label'],
|
|
'effective_state' => $effectiveState,
|
|
'state_label' => $stateLabel,
|
|
'supported_scopes' => $definition['supported_scopes'],
|
|
'affected_surfaces' => $definition['affected_surfaces'],
|
|
'active_activations' => $activations
|
|
->map(fn (OperationalControlActivation $activation): array => $this->activationSummary($activation))
|
|
->values()
|
|
->all(),
|
|
'history_count' => $this->recentAuditEventsForControl($controlKey)->count(),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{control_key: string, scope_type: string, workspace_id: ?int, workspace_count: int, tenant_count: int, summary: string}
|
|
*/
|
|
public function scopeImpactPreview(string $controlKey, string $scopeType, ?int $workspaceId): array
|
|
{
|
|
$label = app(OperationalControlCatalog::class)->label($controlKey);
|
|
|
|
if ($scopeType === 'workspace') {
|
|
$workspace = is_int($workspaceId)
|
|
? Workspace::query()->whereKey($workspaceId)->first()
|
|
: null;
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return [
|
|
'control_key' => $controlKey,
|
|
'scope_type' => $scopeType,
|
|
'workspace_id' => null,
|
|
'workspace_count' => 0,
|
|
'tenant_count' => 0,
|
|
'summary' => 'Select a workspace to preview the scope impact.',
|
|
];
|
|
}
|
|
|
|
$tenantCount = Tenant::query()
|
|
->where('workspace_id', (int) $workspace->getKey())
|
|
->where('external_id', '!=', 'platform')
|
|
->count();
|
|
|
|
return [
|
|
'control_key' => $controlKey,
|
|
'scope_type' => $scopeType,
|
|
'workspace_id' => (int) $workspace->getKey(),
|
|
'workspace_count' => 1,
|
|
'tenant_count' => $tenantCount,
|
|
'summary' => sprintf('%s will affect workspace %s and %d %s.', $label, $workspace->name, $tenantCount, $tenantCount === 1 ? 'tenant' : 'tenants'),
|
|
];
|
|
}
|
|
|
|
$tenantCount = Tenant::query()
|
|
->where('external_id', '!=', 'platform')
|
|
->count();
|
|
|
|
$workspaceCount = Tenant::query()
|
|
->where('external_id', '!=', 'platform')
|
|
->distinct('workspace_id')
|
|
->count('workspace_id');
|
|
|
|
return [
|
|
'control_key' => $controlKey,
|
|
'scope_type' => 'global',
|
|
'workspace_id' => null,
|
|
'workspace_count' => $workspaceCount,
|
|
'tenant_count' => $tenantCount,
|
|
'summary' => sprintf('%s will affect %d %s across %d %s.', $label, $workspaceCount, $workspaceCount === 1 ? 'workspace' : 'workspaces', $tenantCount, $tenantCount === 1 ? 'tenant' : 'tenants'),
|
|
];
|
|
}
|
|
|
|
public function pauseRestoreExecuteAction(): Action
|
|
{
|
|
return $this->pauseActionFor('restore.execute');
|
|
}
|
|
|
|
public function resumeRestoreExecuteAction(): Action
|
|
{
|
|
return $this->resumeActionFor('restore.execute');
|
|
}
|
|
|
|
public function viewHistoryRestoreExecuteAction(): Action
|
|
{
|
|
return $this->historyActionFor('restore.execute');
|
|
}
|
|
|
|
private function pauseActionFor(string $controlKey): Action
|
|
{
|
|
$label = app(OperationalControlCatalog::class)->label($controlKey);
|
|
|
|
return Action::make('pause_'.$this->actionSlug($controlKey))
|
|
->label('Pause '.$label)
|
|
->icon('heroicon-o-pause')
|
|
->color('danger')
|
|
->requiresConfirmation()
|
|
->modalHeading('Pause '.$label)
|
|
->modalDescription('Review the scope impact, reason, and optional expiry before confirming this control change.')
|
|
->form($this->pauseFormSchema($controlKey))
|
|
->action(function (array $data, AuditRecorder $auditRecorder, WorkspaceAuditLogger $workspaceAuditLogger) use ($controlKey, $label): void {
|
|
$actor = $this->controlsActor();
|
|
[$scopeType, $workspace, $reasonText, $expiresAt] = $this->normalizePauseInput($data);
|
|
|
|
$scopeQuery = $this->activationScopeQuery($controlKey, $scopeType, $workspace);
|
|
|
|
(clone $scopeQuery)
|
|
->whereNotNull('expires_at')
|
|
->where('expires_at', '<=', now())
|
|
->delete();
|
|
|
|
$activation = (clone $scopeQuery)->notExpired()->first();
|
|
$auditAction = $activation instanceof OperationalControlActivation
|
|
? AuditActionId::OperationalControlUpdated
|
|
: AuditActionId::OperationalControlPaused;
|
|
|
|
if ($activation instanceof OperationalControlActivation) {
|
|
$activation->fill([
|
|
'reason_text' => $reasonText,
|
|
'expires_at' => $expiresAt,
|
|
'updated_by_platform_user_id' => (int) $actor->getKey(),
|
|
])->save();
|
|
} else {
|
|
$activation = OperationalControlActivation::query()->create([
|
|
'control_key' => $controlKey,
|
|
'scope_type' => $scopeType,
|
|
'workspace_id' => $workspace instanceof Workspace ? (int) $workspace->getKey() : null,
|
|
'reason_text' => $reasonText,
|
|
'expires_at' => $expiresAt,
|
|
'created_by_platform_user_id' => (int) $actor->getKey(),
|
|
]);
|
|
}
|
|
|
|
$this->recordControlMutation(
|
|
auditAction: $auditAction,
|
|
activation: $activation,
|
|
actor: $actor,
|
|
auditRecorder: $auditRecorder,
|
|
workspaceAuditLogger: $workspaceAuditLogger,
|
|
);
|
|
|
|
Notification::make()
|
|
->title(sprintf('%s %s', $label, $auditAction === AuditActionId::OperationalControlPaused ? 'paused' : 'updated'))
|
|
->success()
|
|
->send();
|
|
});
|
|
}
|
|
|
|
private function resumeActionFor(string $controlKey): Action
|
|
{
|
|
$label = app(OperationalControlCatalog::class)->label($controlKey);
|
|
|
|
return Action::make('resume_'.$this->actionSlug($controlKey))
|
|
->label('Resume '.$label)
|
|
->icon('heroicon-o-play')
|
|
->color('gray')
|
|
->requiresConfirmation()
|
|
->modalHeading('Resume '.$label)
|
|
->modalDescription('Remove the selected pause so new starts can proceed again.')
|
|
->form($this->resumeFormSchema($controlKey))
|
|
->action(function (array $data, AuditRecorder $auditRecorder, WorkspaceAuditLogger $workspaceAuditLogger) use ($controlKey, $label): void {
|
|
$actor = $this->controlsActor();
|
|
[$scopeType, $workspace] = $this->normalizeResumeInput($data);
|
|
|
|
$activation = $this->activationScopeQuery($controlKey, $scopeType, $workspace)
|
|
->notExpired()
|
|
->first();
|
|
|
|
if (! $activation instanceof OperationalControlActivation) {
|
|
Notification::make()
|
|
->title(sprintf('%s already enabled', $label))
|
|
->warning()
|
|
->send();
|
|
|
|
return;
|
|
}
|
|
|
|
$activationSnapshot = $activation->replicate();
|
|
$activationSnapshot->forceFill($activation->getAttributes());
|
|
$activation->delete();
|
|
|
|
$this->recordControlMutation(
|
|
auditAction: AuditActionId::OperationalControlResumed,
|
|
activation: $activationSnapshot,
|
|
actor: $actor,
|
|
auditRecorder: $auditRecorder,
|
|
workspaceAuditLogger: $workspaceAuditLogger,
|
|
);
|
|
|
|
Notification::make()
|
|
->title($label.' resumed')
|
|
->success()
|
|
->send();
|
|
});
|
|
}
|
|
|
|
private function historyActionFor(string $controlKey): Action
|
|
{
|
|
$label = app(OperationalControlCatalog::class)->label($controlKey);
|
|
|
|
return Action::make('view_history_'.$this->actionSlug($controlKey))
|
|
->label('View '.$label.' history')
|
|
->link()
|
|
->modalHeading($label.' history')
|
|
->modalSubmitAction(false)
|
|
->modalCancelActionLabel('Close')
|
|
->modalContent(fn () => view('filament.system.pages.ops.partials.operational-control-history', [
|
|
'events' => $this->recentAuditEventsForControl($controlKey),
|
|
'label' => $label,
|
|
]));
|
|
}
|
|
|
|
/**
|
|
* @return array<int, \Filament\Schemas\Components\Component>
|
|
*/
|
|
private function pauseFormSchema(string $controlKey): array
|
|
{
|
|
return [
|
|
Radio::make('scope_type')
|
|
->label('Scope')
|
|
->options([
|
|
'global' => 'Global',
|
|
'workspace' => 'One workspace',
|
|
])
|
|
->default('global')
|
|
->live()
|
|
->required(),
|
|
|
|
Select::make('workspace_id')
|
|
->label('Workspace')
|
|
->searchable()
|
|
->visible(fn (callable $get): bool => $get('scope_type') === 'workspace')
|
|
->required(fn (callable $get): bool => $get('scope_type') === 'workspace')
|
|
->live()
|
|
->getSearchResultsUsing(function (string $search): array {
|
|
return Workspace::query()
|
|
->where('name', 'like', "%{$search}%")
|
|
->orderBy('name')
|
|
->limit(25)
|
|
->pluck('name', 'id')
|
|
->all();
|
|
})
|
|
->getOptionLabelUsing(function ($value): ?string {
|
|
if (! is_numeric($value)) {
|
|
return null;
|
|
}
|
|
|
|
return Workspace::query()->whereKey((int) $value)->value('name');
|
|
}),
|
|
|
|
Textarea::make('reason_text')
|
|
->label('Reason')
|
|
->required()
|
|
->minLength(5)
|
|
->maxLength(500)
|
|
->rows(4),
|
|
|
|
DateTimePicker::make('expires_at')
|
|
->label('Expires at')
|
|
->seconds(false)
|
|
->nullable(),
|
|
|
|
Placeholder::make('scope_preview')
|
|
->label('Scope impact preview')
|
|
->content(function (callable $get) use ($controlKey): string {
|
|
$preview = $this->scopeImpactPreview(
|
|
$controlKey,
|
|
(string) ($get('scope_type') ?? 'global'),
|
|
is_numeric($get('workspace_id')) ? (int) $get('workspace_id') : null,
|
|
);
|
|
|
|
return (string) $preview['summary'];
|
|
}),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<int, \Filament\Schemas\Components\Component>
|
|
*/
|
|
private function resumeFormSchema(string $controlKey): array
|
|
{
|
|
return [
|
|
Radio::make('scope_type')
|
|
->label('Scope')
|
|
->options([
|
|
'global' => 'Global',
|
|
'workspace' => 'One workspace',
|
|
])
|
|
->default('global')
|
|
->live()
|
|
->required(),
|
|
|
|
Select::make('workspace_id')
|
|
->label('Workspace')
|
|
->searchable()
|
|
->visible(fn (callable $get): bool => $get('scope_type') === 'workspace')
|
|
->required(fn (callable $get): bool => $get('scope_type') === 'workspace')
|
|
->getSearchResultsUsing(function (string $search): array {
|
|
return Workspace::query()
|
|
->where('name', 'like', "%{$search}%")
|
|
->orderBy('name')
|
|
->limit(25)
|
|
->pluck('name', 'id')
|
|
->all();
|
|
})
|
|
->getOptionLabelUsing(function ($value): ?string {
|
|
if (! is_numeric($value)) {
|
|
return null;
|
|
}
|
|
|
|
return Workspace::query()->whereKey((int) $value)->value('name');
|
|
}),
|
|
|
|
Placeholder::make('scope_preview')
|
|
->label('Resume impact preview')
|
|
->content(function (callable $get) use ($controlKey): string {
|
|
$preview = $this->scopeImpactPreview(
|
|
$controlKey,
|
|
(string) ($get('scope_type') ?? 'global'),
|
|
is_numeric($get('workspace_id')) ? (int) $get('workspace_id') : null,
|
|
);
|
|
|
|
return (string) $preview['summary'];
|
|
}),
|
|
];
|
|
}
|
|
|
|
private function controlsActor(): PlatformUser
|
|
{
|
|
$actor = auth('platform')->user();
|
|
|
|
if (! $actor instanceof PlatformUser) {
|
|
abort(403);
|
|
}
|
|
|
|
if (! $actor->hasCapability(PlatformCapabilities::OPS_CONTROLS_MANAGE)) {
|
|
abort(403);
|
|
}
|
|
|
|
return $actor;
|
|
}
|
|
|
|
/**
|
|
* @return array{0: string, 1: ?Workspace, 2: string, 3: ?CarbonInterface}
|
|
*/
|
|
private function normalizePauseInput(array $data): array
|
|
{
|
|
[$scopeType, $workspace] = $this->resolveScopeInput($data);
|
|
$reasonText = trim((string) ($data['reason_text'] ?? ''));
|
|
|
|
if ($reasonText === '') {
|
|
throw ValidationException::withMessages([
|
|
'reason_text' => 'A reason is required.',
|
|
]);
|
|
}
|
|
|
|
$expiresAt = null;
|
|
|
|
if (filled($data['expires_at'] ?? null)) {
|
|
$expiresAt = Carbon::parse((string) $data['expires_at']);
|
|
|
|
if ($expiresAt->lessThanOrEqualTo(now())) {
|
|
throw ValidationException::withMessages([
|
|
'expires_at' => 'Expiry must be in the future.',
|
|
]);
|
|
}
|
|
}
|
|
|
|
return [$scopeType, $workspace, $reasonText, $expiresAt];
|
|
}
|
|
|
|
/**
|
|
* @return array{0: string, 1: ?Workspace}
|
|
*/
|
|
private function normalizeResumeInput(array $data): array
|
|
{
|
|
return $this->resolveScopeInput($data);
|
|
}
|
|
|
|
/**
|
|
* @return array{0: string, 1: ?Workspace}
|
|
*/
|
|
private function resolveScopeInput(array $data): array
|
|
{
|
|
$scopeType = (string) ($data['scope_type'] ?? 'global');
|
|
|
|
if (! in_array($scopeType, ['global', 'workspace'], true)) {
|
|
throw ValidationException::withMessages([
|
|
'scope_type' => 'Invalid scope selected.',
|
|
]);
|
|
}
|
|
|
|
if ($scopeType === 'global') {
|
|
return [$scopeType, null];
|
|
}
|
|
|
|
$workspaceId = $data['workspace_id'] ?? null;
|
|
|
|
if (! is_numeric($workspaceId)) {
|
|
throw ValidationException::withMessages([
|
|
'workspace_id' => 'A workspace is required for workspace scope.',
|
|
]);
|
|
}
|
|
|
|
$workspace = Workspace::query()->whereKey((int) $workspaceId)->first();
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
throw ValidationException::withMessages([
|
|
'workspace_id' => 'The selected workspace could not be found.',
|
|
]);
|
|
}
|
|
|
|
return [$scopeType, $workspace];
|
|
}
|
|
|
|
private function activationScopeQuery(string $controlKey, string $scopeType, ?Workspace $workspace): \Illuminate\Database\Eloquent\Builder
|
|
{
|
|
$query = OperationalControlActivation::query()
|
|
->forControl($controlKey)
|
|
->where('scope_type', $scopeType);
|
|
|
|
if ($scopeType === 'workspace') {
|
|
$query->where('workspace_id', (int) $workspace?->getKey());
|
|
} else {
|
|
$query->whereNull('workspace_id');
|
|
}
|
|
|
|
return $query;
|
|
}
|
|
|
|
private function recordControlMutation(
|
|
AuditActionId $auditAction,
|
|
OperationalControlActivation $activation,
|
|
PlatformUser $actor,
|
|
AuditRecorder $auditRecorder,
|
|
WorkspaceAuditLogger $workspaceAuditLogger,
|
|
): void {
|
|
$label = app(OperationalControlCatalog::class)->label((string) $activation->control_key);
|
|
$summary = sprintf('%s %s', $label, match ($auditAction) {
|
|
AuditActionId::OperationalControlPaused => 'paused',
|
|
AuditActionId::OperationalControlUpdated => 'updated',
|
|
AuditActionId::OperationalControlResumed => 'resumed',
|
|
default => 'changed',
|
|
});
|
|
|
|
$metadata = array_filter([
|
|
'control_key' => (string) $activation->control_key,
|
|
'scope_type' => (string) $activation->scope_type,
|
|
'workspace_id' => is_numeric($activation->workspace_id) ? (int) $activation->workspace_id : null,
|
|
'reason_text' => $activation->reason_text,
|
|
'expires_at' => $activation->expires_at?->toIso8601String(),
|
|
'actor_id' => (int) $actor->getKey(),
|
|
], static fn (mixed $value): bool => $value !== null && $value !== '');
|
|
|
|
if ((string) $activation->scope_type === 'global') {
|
|
$auditRecorder->record(
|
|
action: $auditAction,
|
|
context: ['metadata' => $metadata],
|
|
actor: AuditActorSnapshot::platform($actor),
|
|
target: new AuditTargetSnapshot(
|
|
type: 'operational_control',
|
|
id: (string) $activation->getKey(),
|
|
label: $label,
|
|
),
|
|
outcome: 'success',
|
|
summary: $summary,
|
|
);
|
|
|
|
return;
|
|
}
|
|
|
|
$workspace = Workspace::query()->whereKey((int) $activation->workspace_id)->firstOrFail();
|
|
|
|
$workspaceAuditLogger->log(
|
|
workspace: $workspace,
|
|
action: $auditAction,
|
|
context: ['metadata' => $metadata],
|
|
actor: $actor,
|
|
status: 'success',
|
|
resourceType: 'operational_control',
|
|
resourceId: (string) $activation->getKey(),
|
|
targetLabel: $label,
|
|
summary: $summary,
|
|
);
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, OperationalControlActivation>
|
|
*/
|
|
private function activeActivationsForControl(string $controlKey): Collection
|
|
{
|
|
return OperationalControlActivation::query()
|
|
->forControl($controlKey)
|
|
->notExpired()
|
|
->with(['workspace', 'createdBy', 'updatedBy'])
|
|
->orderByRaw("CASE WHEN scope_type = 'global' THEN 0 ELSE 1 END")
|
|
->orderBy('workspace_id')
|
|
->orderBy('id')
|
|
->get();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function activationSummary(OperationalControlActivation $activation): array
|
|
{
|
|
$owner = $activation->updatedBy ?? $activation->createdBy;
|
|
$workspaceName = $activation->workspace?->name;
|
|
|
|
return [
|
|
'id' => (int) $activation->getKey(),
|
|
'scope_type' => (string) $activation->scope_type,
|
|
'scope_label' => (string) $activation->scope_type === 'global'
|
|
? 'Global'
|
|
: sprintf('Workspace: %s', $workspaceName ?? '#'.(int) $activation->workspace_id),
|
|
'workspace_id' => is_numeric($activation->workspace_id) ? (int) $activation->workspace_id : null,
|
|
'workspace_name' => $workspaceName,
|
|
'reason_text' => (string) $activation->reason_text,
|
|
'expires_at' => $activation->expires_at?->toIso8601String(),
|
|
'expires_label' => $activation->expires_at?->diffForHumans() ?? 'No expiry',
|
|
'owner_name' => $owner?->name ?: $owner?->email ?: 'Unknown operator',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, AuditLog>
|
|
*/
|
|
private function recentAuditEventsForControl(string $controlKey): Collection
|
|
{
|
|
return AuditLog::query()
|
|
->where('metadata->control_key', $controlKey)
|
|
->whereIn('action', [
|
|
AuditActionId::OperationalControlPaused->value,
|
|
AuditActionId::OperationalControlUpdated->value,
|
|
AuditActionId::OperationalControlResumed->value,
|
|
AuditActionId::OperationalControlExecutionBlocked->value,
|
|
])
|
|
->latestFirst()
|
|
->limit(10)
|
|
->get();
|
|
}
|
|
|
|
private function actionSlug(string $controlKey): string
|
|
{
|
|
return str_replace('.', '_', $controlKey);
|
|
}
|
|
} |