## Summary - introduce a shared reason-translation contract with envelopes, presenter helpers, fallback handling, and provider translation support - adopt translated operator-facing reason presentation across operation runs, notifications, provider guidance, tenant operability, and RBAC-related surfaces - add Spec 157 design artifacts and targeted regression coverage for translation quality, diagnostics retention, and authorization-safe guidance ## Validation - `vendor/bin/sail bin pint --dirty --format agent` - `vendor/bin/sail artisan test --compact tests/Architecture/ReasonTranslationPrimarySurfaceGuardTest.php tests/Unit/Support/ReasonTranslation/ReasonResolutionEnvelopeTest.php tests/Unit/Support/ReasonTranslation/ExecutionDenialReasonTranslationTest.php tests/Unit/Support/ReasonTranslation/TenantOperabilityReasonTranslationTest.php tests/Unit/Support/ReasonTranslation/RbacReasonTranslationTest.php tests/Unit/Support/ReasonTranslation/ProviderReasonTranslationTest.php tests/Feature/Notifications/OperationRunNotificationTest.php tests/Feature/Operations/OperationRunBlockedExecutionPresentationTest.php tests/Feature/Operations/TenantlessOperationRunViewerTest.php tests/Feature/ReasonTranslation/GovernanceReasonPresentationTest.php tests/Feature/Authorization/ReasonTranslationScopeSafetyTest.php tests/Feature/Monitoring/OperationRunBlockedSpec081Test.php tests/Feature/ProviderConnections/ProviderOperationBlockedGuidanceSpec081Test.php tests/Feature/ProviderConnections/ProviderGatewayRuntimeSmokeSpec081Test.php` ## Notes - Livewire v4.0+ compliance remains unchanged within the existing Filament v5 stack. - No new panel was added; provider registration remains in `bootstrap/providers.php`. - No new globally searchable resource was introduced. - No new destructive action family was introduced. - No new assets were added; the existing `filament:assets` deployment behavior remains unchanged. Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #187
123 lines
3.1 KiB
PHP
123 lines
3.1 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\OpsUx;
|
|
|
|
use App\Support\ReasonTranslation\ReasonTranslator;
|
|
|
|
final class SummaryCountsNormalizer
|
|
{
|
|
/**
|
|
* @param array<string, mixed> $summaryCounts
|
|
* @return array<string, int>
|
|
*/
|
|
public static function normalize(array $summaryCounts): array
|
|
{
|
|
$allowedKeys = array_flip(OperationSummaryKeys::all());
|
|
|
|
$sanitized = [];
|
|
|
|
foreach ($summaryCounts as $key => $value) {
|
|
$key = trim((string) $key);
|
|
|
|
if ($key === '' || ! isset($allowedKeys[$key])) {
|
|
continue;
|
|
}
|
|
|
|
if (is_int($value)) {
|
|
$sanitized[$key] = $value;
|
|
|
|
continue;
|
|
}
|
|
|
|
if (is_float($value) && is_finite($value)) {
|
|
$sanitized[$key] = (int) round($value);
|
|
|
|
continue;
|
|
}
|
|
|
|
if (is_numeric($value)) {
|
|
$sanitized[$key] = (int) $value;
|
|
}
|
|
}
|
|
|
|
$ordered = [];
|
|
|
|
foreach (OperationSummaryKeys::all() as $key) {
|
|
if (array_key_exists($key, $sanitized)) {
|
|
$ordered[$key] = $sanitized[$key];
|
|
}
|
|
}
|
|
|
|
return $ordered;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $summaryCounts
|
|
*/
|
|
public static function renderSummaryLine(array $summaryCounts): ?string
|
|
{
|
|
$normalized = self::normalize($summaryCounts);
|
|
|
|
if ($normalized === []) {
|
|
return null;
|
|
}
|
|
|
|
$parts = [];
|
|
|
|
foreach ($normalized as $key => $value) {
|
|
if ($value === 0) {
|
|
continue;
|
|
}
|
|
|
|
$parts[] = self::label($key).': '.$value;
|
|
}
|
|
|
|
if ($parts === []) {
|
|
return null;
|
|
}
|
|
|
|
return implode(' · ', $parts);
|
|
}
|
|
|
|
/**
|
|
* Convert a snake_case summary key to a human-readable label.
|
|
*/
|
|
public static function label(string $key): string
|
|
{
|
|
$reasonCode = null;
|
|
|
|
if (str_starts_with($key, 'reason_')) {
|
|
$reasonCode = substr($key, strlen('reason_'));
|
|
} elseif (str_starts_with($key, 'blocked_reason_')) {
|
|
$reasonCode = substr($key, strlen('blocked_reason_'));
|
|
}
|
|
|
|
if (is_string($reasonCode) && $reasonCode !== '') {
|
|
$translation = app(ReasonTranslator::class)->translate($reasonCode, surface: 'summary_line');
|
|
|
|
if ($translation !== null) {
|
|
return 'Reason: '.$translation->operatorLabel;
|
|
}
|
|
}
|
|
|
|
return match ($key) {
|
|
'total' => 'Total',
|
|
'processed' => 'Processed',
|
|
'succeeded' => 'Completed successfully',
|
|
'failed' => 'Failed items',
|
|
'skipped' => 'Skipped items',
|
|
'items' => 'Affected items',
|
|
'tenants' => 'Tenants',
|
|
'created' => 'Created',
|
|
'updated' => 'Updated',
|
|
'deleted' => 'Deleted',
|
|
'finding_count' => 'Findings',
|
|
'report_count' => 'Reports',
|
|
'operation_count' => 'Operations',
|
|
default => ucfirst(str_replace('_', ' ', $key)),
|
|
};
|
|
}
|
|
}
|