## Summary - turn the tenant registry into a workspace-scoped recovery triage surface with backup posture and recovery evidence columns - preserve workspace overview backup and recovery drilldown intent by routing multi-tenant cases into filtered tenant registry slices - add the Spec 186 planning artifacts, focused regression coverage, and shared triage presentation helpers ## Testing - `cd apps/platform && ./vendor/bin/sail bin pint --dirty --format agent` - `cd apps/platform && ./vendor/bin/sail artisan test --compact tests/Feature/Filament/TenantRegistryRecoveryTriageTest.php tests/Feature/Filament/WorkspaceOverviewSummaryMetricsTest.php tests/Feature/Filament/WorkspaceOverviewDrilldownContinuityTest.php tests/Feature/Filament/TenantResourceIndexIsWorkspaceScopedTest.php tests/Feature/Filament/WorkspaceOverviewAuthorizationTest.php tests/Feature/Guards/ActionSurfaceContractTest.php tests/Feature/Guards/FilamentTableStandardsGuardTest.php` ## Notes - no schema change - no new persisted recovery truth - branch includes the full Spec 186 spec, plan, research, data model, contract, quickstart, and tasks artifacts Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #217
184 lines
5.9 KiB
PHP
184 lines
5.9 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Support\Tenants;
|
|
|
|
use App\Support\BackupHealth\TenantBackupHealthAssessment;
|
|
use App\Support\RestoreSafety\RestoreResultAttention;
|
|
use Illuminate\Support\Str;
|
|
|
|
final class TenantRecoveryTriagePresentation
|
|
{
|
|
public const RECOVERY_EVIDENCE_WEAKENED = 'weakened';
|
|
|
|
public const RECOVERY_EVIDENCE_UNVALIDATED = 'unvalidated';
|
|
|
|
public const RECOVERY_EVIDENCE_NO_RECENT_ISSUES_VISIBLE = 'no_recent_issues_visible';
|
|
|
|
public const TRIAGE_SORT_DEFAULT = 'default';
|
|
|
|
public const TRIAGE_SORT_WORST_FIRST = 'worst_first';
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public static function backupPostureOptions(): array
|
|
{
|
|
return [
|
|
TenantBackupHealthAssessment::POSTURE_ABSENT => 'Absent',
|
|
TenantBackupHealthAssessment::POSTURE_STALE => 'Stale',
|
|
TenantBackupHealthAssessment::POSTURE_DEGRADED => 'Degraded',
|
|
TenantBackupHealthAssessment::POSTURE_HEALTHY => 'Healthy',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public static function recoveryEvidenceOptions(): array
|
|
{
|
|
return [
|
|
self::RECOVERY_EVIDENCE_WEAKENED => 'Weakened',
|
|
self::RECOVERY_EVIDENCE_UNVALIDATED => 'Unvalidated',
|
|
self::RECOVERY_EVIDENCE_NO_RECENT_ISSUES_VISIBLE => 'No recent issues visible',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
public static function triageSortOptions(): array
|
|
{
|
|
return [
|
|
self::TRIAGE_SORT_DEFAULT => 'Default order',
|
|
self::TRIAGE_SORT_WORST_FIRST => 'Worst first',
|
|
];
|
|
}
|
|
|
|
public static function backupPostureState(?TenantBackupHealthAssessment $assessment): ?string
|
|
{
|
|
return $assessment?->posture;
|
|
}
|
|
|
|
public static function backupPostureLabel(?TenantBackupHealthAssessment $assessment): string
|
|
{
|
|
$state = self::backupPostureState($assessment);
|
|
|
|
if ($state === null) {
|
|
return 'Unknown';
|
|
}
|
|
|
|
return self::backupPostureOptions()[$state] ?? Str::headline($state);
|
|
}
|
|
|
|
public static function backupPostureTone(?TenantBackupHealthAssessment $assessment): string
|
|
{
|
|
return $assessment?->tone() ?? 'gray';
|
|
}
|
|
|
|
public static function backupPostureDescription(?TenantBackupHealthAssessment $assessment, ?string $helperText = null): ?string
|
|
{
|
|
if (! $assessment instanceof TenantBackupHealthAssessment) {
|
|
return $helperText;
|
|
}
|
|
|
|
$parts = [
|
|
$assessment->supportingMessage ?? $assessment->headline,
|
|
$assessment->posture === TenantBackupHealthAssessment::POSTURE_HEALTHY
|
|
? $assessment->positiveClaimBoundary
|
|
: null,
|
|
$helperText,
|
|
];
|
|
|
|
$description = trim(implode(' ', array_filter($parts, static fn (?string $part): bool => filled($part))));
|
|
|
|
return $description !== '' ? $description : null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $recoveryEvidence
|
|
*/
|
|
public static function recoveryEvidenceState(?array $recoveryEvidence): ?string
|
|
{
|
|
$state = $recoveryEvidence['overview_state'] ?? null;
|
|
|
|
return is_string($state) && $state !== '' ? $state : null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $recoveryEvidence
|
|
*/
|
|
public static function recoveryEvidenceLabel(?array $recoveryEvidence): string
|
|
{
|
|
$state = self::recoveryEvidenceState($recoveryEvidence);
|
|
|
|
if ($state === null) {
|
|
return 'Unknown';
|
|
}
|
|
|
|
return self::recoveryEvidenceOptions()[$state] ?? Str::headline($state);
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $recoveryEvidence
|
|
*/
|
|
public static function recoveryEvidenceDescription(?array $recoveryEvidence, ?string $helperText = null): ?string
|
|
{
|
|
if (! is_array($recoveryEvidence)) {
|
|
return $helperText;
|
|
}
|
|
|
|
$parts = [
|
|
is_string($recoveryEvidence['summary'] ?? null) ? $recoveryEvidence['summary'] : null,
|
|
is_string($recoveryEvidence['claim_boundary'] ?? null) ? $recoveryEvidence['claim_boundary'] : null,
|
|
$helperText,
|
|
];
|
|
|
|
$description = trim(implode(' ', array_filter($parts, static fn (?string $part): bool => filled($part))));
|
|
|
|
return $description !== '' ? $description : null;
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $recoveryEvidence
|
|
*/
|
|
public static function recoveryEvidenceTone(?array $recoveryEvidence): string
|
|
{
|
|
if (! is_array($recoveryEvidence)) {
|
|
return 'gray';
|
|
}
|
|
|
|
$attentionState = is_string($recoveryEvidence['latest_relevant_attention_state'] ?? null)
|
|
? $recoveryEvidence['latest_relevant_attention_state']
|
|
: null;
|
|
|
|
return match (self::recoveryEvidenceState($recoveryEvidence)) {
|
|
self::RECOVERY_EVIDENCE_UNVALIDATED => 'warning',
|
|
self::RECOVERY_EVIDENCE_WEAKENED => $attentionState === RestoreResultAttention::STATE_FAILED ? 'danger' : 'warning',
|
|
self::RECOVERY_EVIDENCE_NO_RECENT_ISSUES_VISIBLE => 'success',
|
|
default => 'gray',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed>|null $recoveryEvidence
|
|
*/
|
|
public static function triageTier(
|
|
?TenantBackupHealthAssessment $backupHealth,
|
|
?array $recoveryEvidence,
|
|
): int {
|
|
$backupPosture = self::backupPostureState($backupHealth);
|
|
$recoveryState = self::recoveryEvidenceState($recoveryEvidence);
|
|
|
|
return match (true) {
|
|
$backupPosture === TenantBackupHealthAssessment::POSTURE_ABSENT => 1,
|
|
$recoveryState === self::RECOVERY_EVIDENCE_WEAKENED => 2,
|
|
$backupPosture === TenantBackupHealthAssessment::POSTURE_STALE => 3,
|
|
$recoveryState === self::RECOVERY_EVIDENCE_UNVALIDATED => 4,
|
|
$backupPosture === TenantBackupHealthAssessment::POSTURE_DEGRADED => 5,
|
|
default => 6,
|
|
};
|
|
}
|
|
}
|