TenantAtlas/app/Console/Commands/TenantpilotDispatchAlerts.php
ahmido 3ed275cef3 feat(alerts): Monitoring cluster + v1 resources (spec 099) (#121)
Implements spec `099-alerts-v1-teams-email`.

- Monitoring navigation: Alerts as a cluster under Monitoring; default landing is Alert deliveries.
- Tenant panel: Alerts points to `/admin/alerts` and the cluster navigation is hidden in tenant panel.
- Guard compliance: removes direct `Gate::` usage from Alert resources so `NoAdHocFilamentAuthPatternsTest` passes.

Verification:
- Full suite: `1348 passed, 7 skipped` (EXIT=0).

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #121
2026-02-18 15:20:43 +00:00

107 lines
3.3 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Console\Commands;
use App\Jobs\Alerts\DeliverAlertsJob;
use App\Jobs\Alerts\EvaluateAlertsJob;
use App\Models\Workspace;
use App\Services\OperationRunService;
use Carbon\CarbonImmutable;
use Illuminate\Console\Command;
use Illuminate\Support\Collection;
class TenantpilotDispatchAlerts extends Command
{
protected $signature = 'tenantpilot:alerts:dispatch {--workspace=* : Limit dispatch to one or more workspace IDs}';
protected $description = 'Queue workspace-scoped alert evaluation and delivery jobs idempotently.';
public function handle(OperationRunService $operationRuns): int
{
if (! (bool) config('tenantpilot.alerts.enabled', true)) {
return self::SUCCESS;
}
$workspaceFilter = array_values(array_filter(array_map(
static fn (mixed $value): int => (int) $value,
(array) $this->option('workspace'),
)));
$workspaces = $this->resolveWorkspaces($workspaceFilter);
$slotKey = CarbonImmutable::now('UTC')->format('YmdHi').'Z';
$queuedEvaluate = 0;
$queuedDeliver = 0;
$skippedEvaluate = 0;
$skippedDeliver = 0;
foreach ($workspaces as $workspace) {
$evaluateRun = $operationRuns->ensureWorkspaceRunWithIdentity(
workspace: $workspace,
type: 'alerts.evaluate',
identityInputs: ['slot_key' => $slotKey],
context: [
'trigger' => 'scheduled_dispatch',
'slot_key' => $slotKey,
],
initiator: null,
);
if ($evaluateRun->wasRecentlyCreated) {
EvaluateAlertsJob::dispatch((int) $workspace->getKey(), (int) $evaluateRun->getKey());
$queuedEvaluate++;
} else {
$skippedEvaluate++;
}
$deliverRun = $operationRuns->ensureWorkspaceRunWithIdentity(
workspace: $workspace,
type: 'alerts.deliver',
identityInputs: ['slot_key' => $slotKey],
context: [
'trigger' => 'scheduled_dispatch',
'slot_key' => $slotKey,
],
initiator: null,
);
if ($deliverRun->wasRecentlyCreated) {
DeliverAlertsJob::dispatch((int) $workspace->getKey(), (int) $deliverRun->getKey());
$queuedDeliver++;
} else {
$skippedDeliver++;
}
}
$this->info(sprintf(
'Alert dispatch scanned %d workspace(s): evaluate queued=%d skipped=%d, deliver queued=%d skipped=%d.',
$workspaces->count(),
$queuedEvaluate,
$skippedEvaluate,
$queuedDeliver,
$skippedDeliver,
));
return self::SUCCESS;
}
/**
* @param array<int, int> $workspaceIds
* @return Collection<int, Workspace>
*/
private function resolveWorkspaces(array $workspaceIds): Collection
{
return Workspace::query()
->when(
$workspaceIds !== [],
fn ($query) => $query->whereIn('id', $workspaceIds),
fn ($query) => $query->whereHas('tenants'),
)
->orderBy('id')
->get();
}
}