TenantAtlas/apps/platform/app/Support/SystemConsole/StuckRunClassifier.php
ahmido ce0615a9c1 Spec 182: relocate Laravel platform to apps/platform (#213)
## Summary
- move the Laravel application into `apps/platform` and keep the repository root for orchestration, docs, and tooling
- update the local command model, Sail/Docker wiring, runtime paths, and ignore rules around the new platform location
- add relocation quickstart/contracts plus focused smoke coverage for bootstrap, command model, routes, and runtime behavior

## Validation
- `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/PlatformRelocation`
- integrated browser smoke validated `/up`, `/`, `/admin`, `/admin/choose-workspace`, and tenant route semantics for `200`, `403`, and `404`

## Remaining Rollout Checks
- validate Dokploy build context and working-directory assumptions against the new `apps/platform` layout
- confirm web, queue, and scheduler processes all start from the expected working directory in staging/production
- verify no legacy volume mounts or asset-publish paths still point at the old root-level `public/` or `storage/` locations

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #213
2026-04-08 08:40:47 +00:00

87 lines
2.8 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\SystemConsole;
use App\Models\OperationRun;
use App\Support\OperationRunStatus;
use Carbon\CarbonImmutable;
use Illuminate\Database\Eloquent\Builder;
final class StuckRunClassifier
{
public function __construct(
private readonly int $queuedThresholdMinutes = 0,
private readonly int $runningThresholdMinutes = 0,
) {}
public function queuedThresholdMinutes(): int
{
if ($this->queuedThresholdMinutes > 0) {
return $this->queuedThresholdMinutes;
}
return max(1, (int) config('tenantpilot.system_console.stuck_thresholds.queued_minutes', 15));
}
public function runningThresholdMinutes(): int
{
if ($this->runningThresholdMinutes > 0) {
return $this->runningThresholdMinutes;
}
return max(1, (int) config('tenantpilot.system_console.stuck_thresholds.running_minutes', 30));
}
public function apply(Builder $query, ?CarbonImmutable $now = null): Builder
{
$now ??= CarbonImmutable::now();
$queuedCutoff = $now->subMinutes($this->queuedThresholdMinutes());
$runningCutoff = $now->subMinutes($this->runningThresholdMinutes());
return $query->where(function (Builder $stuckQuery) use ($queuedCutoff, $runningCutoff): void {
$stuckQuery
->where(function (Builder $queuedQuery) use ($queuedCutoff): void {
$queuedQuery
->where('status', OperationRunStatus::Queued->value)
->whereNull('started_at')
->where('created_at', '<=', $queuedCutoff);
})
->orWhere(function (Builder $runningQuery) use ($runningCutoff): void {
$runningQuery
->where('status', OperationRunStatus::Running->value)
->where('started_at', '<=', $runningCutoff);
});
});
}
public function classify(OperationRun $run, ?CarbonImmutable $now = null): ?string
{
$now ??= CarbonImmutable::now();
if ($run->status === OperationRunStatus::Queued->value) {
if ($run->started_at !== null) {
return null;
}
if ($run->created_at === null) {
return null;
}
return $run->created_at->lte($now->subMinutes($this->queuedThresholdMinutes()))
? OperationRunStatus::Queued->value
: null;
}
if ($run->status === OperationRunStatus::Running->value && $run->started_at !== null) {
return $run->started_at->lte($now->subMinutes($this->runningThresholdMinutes()))
? OperationRunStatus::Running->value
: null;
}
return null;
}
}