TenantAtlas/app/Filament/Pages/Monitoring/Operations.php
ahmido 845d21db6d feat: harden operation lifecycle monitoring (#190)
## Summary
- harden operation-run lifecycle handling with explicit reconciliation policy, stale-run healing, failed-job bridging, and monitoring visibility
- refactor audit log event inspection into a Filament slide-over and remove the stale inline detail/header-action coupling
- align panel theme asset resolution and supporting Filament UI updates, including the rounded 2xl theme token regression fix

## Testing
- ran focused Pest coverage for the affected audit-log inspection flow and related visibility tests
- ran formatting with `vendor/bin/sail bin pint --dirty --format agent`
- manually verified the updated audit-log slide-over flow in the integrated browser

## Notes
- branch includes the Spec 160 artifacts under `specs/160-operation-lifecycle-guarantees/`
- the full test suite was not rerun as part of this final commit/PR step

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #190
2026-03-23 21:53:19 +00:00

276 lines
10 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Filament\Pages\Monitoring;
use App\Filament\Resources\OperationRunResource;
use App\Filament\Widgets\Operations\OperationsKpiHeader;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Support\Filament\CanonicalAdminTenantFilterState;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperateHub\OperateHubShell;
use App\Support\OperationRunOutcome;
use App\Support\OperationRunStatus;
use App\Support\Operations\OperationLifecyclePolicy;
use App\Support\Workspaces\WorkspaceContext;
use BackedEnum;
use Filament\Actions\Action;
use Filament\Facades\Filament;
use Filament\Forms\Concerns\InteractsWithForms;
use Filament\Forms\Contracts\HasForms;
use Filament\Pages\Page;
use Filament\Tables\Concerns\InteractsWithTable;
use Filament\Tables\Contracts\HasTable;
use Filament\Tables\Table;
use Illuminate\Database\Eloquent\Builder;
use UnitEnum;
class Operations extends Page implements HasForms, HasTable
{
use InteractsWithForms;
use InteractsWithTable;
public string $activeTab = 'all';
/**
* @var array<string, mixed>|null
*/
public ?array $navigationContextPayload = null;
protected static bool $isDiscovered = false;
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-queue-list';
protected static string|UnitEnum|null $navigationGroup = 'Monitoring';
protected static ?string $title = 'Operations';
// Must be non-static
protected string $view = 'filament.pages.monitoring.operations';
public function mount(): void
{
$this->navigationContextPayload = is_array(request()->query('nav')) ? request()->query('nav') : null;
app(CanonicalAdminTenantFilterState::class)->sync(
$this->getTableFiltersSessionKey(),
['type', 'initiator_name'],
request(),
);
$this->mountInteractsWithTable();
}
protected function getHeaderWidgets(): array
{
return [
OperationsKpiHeader::class,
];
}
/**
* @return array<Action>
*/
protected function getHeaderActions(): array
{
$operateHubShell = app(OperateHubShell::class);
$navigationContext = $this->navigationContext();
$actions = [
Action::make('operate_hub_scope_operations')
->label($operateHubShell->scopeLabel(request()))
->color('gray')
->disabled(),
];
$activeTenant = $operateHubShell->activeEntitledTenant(request());
if ($navigationContext?->backLinkLabel !== null && $navigationContext->backLinkUrl !== null) {
$actions[] = Action::make('operate_hub_back_to_origin_operations')
->label($navigationContext->backLinkLabel)
->icon('heroicon-o-arrow-left')
->color('gray')
->url($navigationContext->backLinkUrl);
} elseif ($activeTenant instanceof Tenant) {
$actions[] = Action::make('operate_hub_back_to_tenant_operations')
->label('Back to '.$activeTenant->name)
->icon('heroicon-o-arrow-left')
->color('gray')
->url(\App\Filament\Pages\TenantDashboard::getUrl(panel: 'tenant', tenant: $activeTenant));
}
if ($activeTenant instanceof Tenant) {
$actions[] = Action::make('operate_hub_show_all_tenants')
->label('Show all tenants')
->color('gray')
->action(function (): void {
Filament::setTenant(null, true);
app(WorkspaceContext::class)->clearLastTenantId(request());
$this->removeTableFilter('tenant_id');
$this->redirect('/admin/operations');
});
}
return $actions;
}
private function navigationContext(): ?CanonicalNavigationContext
{
if (! is_array($this->navigationContextPayload)) {
return CanonicalNavigationContext::fromRequest(request());
}
$request = request()->duplicate(query: ['nav' => $this->navigationContextPayload]);
return CanonicalNavigationContext::fromRequest($request);
}
public function updatedActiveTab(): void
{
$this->resetPage();
}
public function table(Table $table): Table
{
return OperationRunResource::table($table)
->query(function (): Builder {
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
$tenantFilter = data_get($this->tableFilters, 'tenant_id.value');
if (! is_numeric($tenantFilter)) {
$tenantFilter = data_get(session()->get($this->getTableFiltersSessionKey(), []), 'tenant_id.value');
}
$query = OperationRun::query()
->with('user')
->latest('id')
->when(
$workspaceId,
fn (Builder $query): Builder => $query->where('workspace_id', (int) $workspaceId),
)
->when(
! $workspaceId,
fn (Builder $query): Builder => $query->whereRaw('1 = 0'),
)
->when(
is_numeric($tenantFilter),
fn (Builder $query): Builder => $query->where('tenant_id', (int) $tenantFilter),
);
return $this->applyActiveTab($query);
});
}
/**
* @return array{likely_stale:int,reconciled:int}
*/
public function lifecycleVisibilitySummary(): array
{
$baseQuery = $this->scopedSummaryQuery();
if (! $baseQuery instanceof Builder) {
return [
'likely_stale' => 0,
'reconciled' => 0,
];
}
$reconciled = (clone $baseQuery)
->whereNotNull('context->reconciliation->reconciled_at')
->count();
$policy = app(OperationLifecyclePolicy::class);
$likelyStale = (clone $baseQuery)
->whereIn('status', [
OperationRunStatus::Queued->value,
OperationRunStatus::Running->value,
])
->where(function (Builder $query) use ($policy): void {
foreach ($policy->coveredTypeNames() as $type) {
$query->orWhere(function (Builder $typeQuery) use ($policy, $type): void {
$typeQuery
->where('type', $type)
->where(function (Builder $stateQuery) use ($policy, $type): void {
$stateQuery
->where(function (Builder $queuedQuery) use ($policy, $type): void {
$queuedQuery
->where('status', OperationRunStatus::Queued->value)
->whereNull('started_at')
->where('created_at', '<=', now()->subSeconds($policy->queuedStaleAfterSeconds($type)));
})
->orWhere(function (Builder $runningQuery) use ($policy, $type): void {
$runningQuery
->where('status', OperationRunStatus::Running->value)
->where(function (Builder $startedAtQuery) use ($policy, $type): void {
$startedAtQuery
->where('started_at', '<=', now()->subSeconds($policy->runningStaleAfterSeconds($type)))
->orWhere(function (Builder $fallbackQuery) use ($policy, $type): void {
$fallbackQuery
->whereNull('started_at')
->where('created_at', '<=', now()->subSeconds($policy->runningStaleAfterSeconds($type)));
});
});
});
});
});
}
})
->count();
return [
'likely_stale' => $likelyStale,
'reconciled' => $reconciled,
];
}
private function applyActiveTab(Builder $query): Builder
{
return match ($this->activeTab) {
'active' => $query->whereIn('status', [
OperationRunStatus::Queued->value,
OperationRunStatus::Running->value,
]),
'blocked' => $query
->where('status', OperationRunStatus::Completed->value)
->where('outcome', OperationRunOutcome::Blocked->value),
'succeeded' => $query
->where('status', OperationRunStatus::Completed->value)
->where('outcome', OperationRunOutcome::Succeeded->value),
'partial' => $query
->where('status', OperationRunStatus::Completed->value)
->where('outcome', OperationRunOutcome::PartiallySucceeded->value),
'failed' => $query
->where('status', OperationRunStatus::Completed->value)
->where('outcome', OperationRunOutcome::Failed->value),
default => $query,
};
}
private function scopedSummaryQuery(): ?Builder
{
$workspaceId = app(WorkspaceContext::class)->currentWorkspaceId(request());
if (! $workspaceId) {
return null;
}
$tenantFilter = data_get($this->tableFilters, 'tenant_id.value');
if (! is_numeric($tenantFilter)) {
$tenantFilter = data_get(session()->get($this->getTableFiltersSessionKey(), []), 'tenant_id.value');
}
return OperationRun::query()
->where('workspace_id', (int) $workspaceId)
->when(
is_numeric($tenantFilter),
fn (Builder $query): Builder => $query->where('tenant_id', (int) $tenantFilter),
);
}
}