TenantAtlas/apps/platform/app/Support/OpsUx/OperationRunProgressContract.php
Ahmed Darrazi 8fce9f0af1
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m44s
chore: commit workspace changes
2026-05-04 18:29:12 +02:00

223 lines
6.1 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\OpsUx;
use App\Models\OperationRun;
use App\Support\OperationRunStatus;
final class OperationRunProgressContract
{
public const string NONE = 'none';
public const string ACTIVITY = 'activity';
public const string COUNTED = 'counted';
public const string PHASED = 'phased';
public const string COMPOSITE = 'composite';
/**
* @return array{
* capability: string,
* display: string,
* label: ?string,
* processed: ?int,
* total: ?int,
* percent: ?int
* }
*/
public static function forRun(OperationRun $run): array
{
$summaryCounts = SummaryCountsNormalizer::normalize(
is_array($run->summary_counts) ? $run->summary_counts : [],
);
$context = is_array($run->context) ? $run->context : [];
$capability = self::capabilityForRun($run, $summaryCounts, $context);
return match ($capability) {
self::COUNTED => self::countedModel($summaryCounts),
self::PHASED => self::indeterminateModel(self::PHASED, 'Phase progress pending.'),
self::COMPOSITE => self::indeterminateModel(self::COMPOSITE, 'Composite progress pending.'),
self::ACTIVITY => self::indeterminateModel(
self::ACTIVITY,
(string) $run->status === OperationRunStatus::Queued->value
? 'Waiting for worker.'
: 'Progress details pending.',
),
default => self::noneModel(),
};
}
/**
* @param array<string, int> $summaryCounts
* @param array<string, mixed> $context
*/
private static function capabilityForRun(OperationRun $run, array $summaryCounts, array $context): string
{
if (! $run->isCurrentlyActive()) {
return self::NONE;
}
if ((string) $run->status === OperationRunStatus::Queued->value) {
return self::ACTIVITY;
}
if (self::hasPhasedHint($context)) {
return self::PHASED;
}
if (self::hasCompositeHint($summaryCounts, $context)) {
return self::COMPOSITE;
}
if (self::hasCountedHint($summaryCounts)) {
return self::COUNTED;
}
return self::ACTIVITY;
}
/**
* @param array<string, int> $summaryCounts
*/
private static function hasCountedHint(array $summaryCounts): bool
{
$total = $summaryCounts['total'] ?? null;
$processed = $summaryCounts['processed'] ?? null;
return is_int($total)
&& $total > 0
&& is_int($processed);
}
/**
* @param array<string, int> $summaryCounts
* @return array{
* capability: string,
* display: string,
* label: string,
* processed: int,
* total: int,
* percent: int
* }
*/
private static function countedModel(array $summaryCounts): array
{
$total = max(1, (int) ($summaryCounts['total'] ?? 0));
$processed = min(max(0, (int) ($summaryCounts['processed'] ?? 0)), $total);
$percent = max(0, min(100, (int) round(($processed / $total) * 100)));
return [
'capability' => self::COUNTED,
'display' => self::COUNTED,
'label' => sprintf('%d / %d processed (%d%%)', $processed, $total, $percent),
'processed' => $processed,
'total' => $total,
'percent' => $percent,
];
}
/**
* @return array{
* capability: string,
* display: string,
* label: string,
* processed: null,
* total: null,
* percent: null
* }
*/
private static function indeterminateModel(string $capability, string $label): array
{
return [
'capability' => $capability,
'display' => 'indeterminate',
'label' => $label,
'processed' => null,
'total' => null,
'percent' => null,
];
}
/**
* @return array{
* capability: string,
* display: string,
* label: null,
* processed: null,
* total: null,
* percent: null
* }
*/
private static function noneModel(): array
{
return [
'capability' => self::NONE,
'display' => self::NONE,
'label' => null,
'processed' => null,
'total' => null,
'percent' => null,
];
}
/**
* @param array<string, mixed> $context
*/
private static function hasPhasedHint(array $context): bool
{
foreach (['baseline_capture.evidence_capture', 'baseline_compare.evidence_capture'] as $path) {
$phaseStats = data_get($context, $path);
if (is_array($phaseStats) && self::looksLikePhaseStats($phaseStats)) {
return true;
}
}
return false;
}
/**
* @param array<string, mixed> $phaseStats
*/
private static function looksLikePhaseStats(array $phaseStats): bool
{
return count(array_intersect(
array_keys($phaseStats),
['requested', 'succeeded', 'skipped', 'failed', 'throttled'],
)) >= 2;
}
/**
* @param array<string, int> $summaryCounts
* @param array<string, mixed> $context
*/
private static function hasCompositeHint(array $summaryCounts, array $context): bool
{
$operationCount = $summaryCounts['operation_count'] ?? null;
if (is_int($operationCount) && $operationCount > 1) {
return true;
}
foreach (['child_run_ids', 'operation_run_ids'] as $path) {
$runIds = data_get($context, $path);
if (! is_array($runIds)) {
continue;
}
$numericIds = array_filter($runIds, static fn (mixed $runId): bool => is_numeric($runId));
if (count($numericIds) > 1) {
return true;
}
}
return false;
}
}