TenantAtlas/apps/platform/app/Support/GovernanceInbox/GovernanceInboxSectionBuilder.php
ahmido 72bfb37ba7
Some checks failed
Main Confidence / confidence (push) Failing after 57s
feat: add decision-based governance inbox (#291)
## Summary
- add a read-first governance inbox page at `/admin/governance/inbox`
- aggregate assigned findings, intake, stale operations, alert-delivery failures, and review follow-up into one canonical routing surface
- add focused coverage for inbox authorization, navigation context, page behavior, and section builder logic
- include the Spec Kit artifacts for spec 250

## Notes
- branch is synced with `dev`
- this PR supersedes #290 for the governance inbox work

Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de>
Reviewed-on: #291
2026-04-28 10:13:09 +00:00

888 lines
35 KiB
PHP

<?php
declare(strict_types=1);
namespace App\Support\GovernanceInbox;
use App\Filament\Pages\Findings\FindingsIntakeQueue;
use App\Filament\Pages\Findings\MyFindingsInbox;
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
use App\Filament\Resources\AlertDeliveryResource;
use App\Filament\Resources\FindingExceptionResource;
use App\Filament\Resources\FindingResource;
use App\Filament\Resources\TenantResource;
use App\Filament\Resources\TenantReviewResource;
use App\Models\AlertDelivery;
use App\Models\Finding;
use App\Models\OperationRun;
use App\Models\Tenant;
use App\Models\TenantTriageReview;
use App\Models\User;
use App\Models\Workspace;
use App\Services\TenantReviews\TenantReviewRegisterService;
use App\Support\Auth\Capabilities;
use App\Support\BackupHealth\TenantBackupHealthAssessment;
use App\Support\BackupHealth\TenantBackupHealthResolver;
use App\Support\Navigation\CanonicalNavigationContext;
use App\Support\OperationRunLinks;
use App\Support\PortfolioTriage\PortfolioArrivalContextToken;
use App\Support\PortfolioTriage\TenantTriageReviewStateResolver;
use App\Support\RestoreSafety\RestoreSafetyResolver;
use App\Support\Tenants\TenantRecoveryTriagePresentation;
use Illuminate\Support\Str;
final readonly class GovernanceInboxSectionBuilder
{
private const PREVIEW_LIMIT = 3;
/**
* @var list<string>
*/
private const FAMILY_ORDER = [
'assigned_findings',
'intake_findings',
'stale_operations',
'alert_delivery_failures',
'review_follow_up',
];
public function __construct(
private TenantBackupHealthResolver $backupHealthResolver,
private RestoreSafetyResolver $restoreSafetyResolver,
private TenantTriageReviewStateResolver $tenantTriageReviewStateResolver,
private TenantReviewRegisterService $tenantReviewRegisterService,
) {}
/**
* @param array<int, Tenant> $authorizedTenants
* @param array<int, Tenant> $visibleFindingTenants
* @param array<int, Tenant> $reviewTenants
* @return array{
* sections: list<array<string, mixed>>,
* available_families: list<array{key: string, label: string, count: int}>,
* family_counts: array<string, int>,
* total_count: int,
* }
*/
public function build(
User $user,
Workspace $workspace,
array $authorizedTenants,
array $visibleFindingTenants,
array $reviewTenants,
bool $canViewAlerts,
?Tenant $selectedTenant = null,
?string $selectedFamily = null,
?CanonicalNavigationContext $navigationContext = null,
): array {
$authorizedTenantsById = $this->indexTenants($authorizedTenants);
$visibleFindingTenantsById = $this->indexTenants($visibleFindingTenants);
$reviewTenantsById = $this->indexTenants($reviewTenants);
$allSections = [];
$availableFamilies = [];
$familyCounts = [];
if ($visibleFindingTenantsById !== []) {
$assignedSection = $this->assignedFindingsSection(
user: $user,
visibleFindingTenants: $visibleFindingTenantsById,
selectedTenant: $selectedTenant,
navigationContext: $navigationContext,
);
$allSections[$assignedSection['key']] = $assignedSection;
$availableFamilies[] = [
'key' => $assignedSection['key'],
'label' => $assignedSection['label'],
'count' => $assignedSection['count'],
];
$familyCounts[$assignedSection['key']] = $assignedSection['count'];
$intakeSection = $this->intakeFindingsSection(
visibleFindingTenants: $visibleFindingTenantsById,
selectedTenant: $selectedTenant,
navigationContext: $navigationContext,
);
$allSections[$intakeSection['key']] = $intakeSection;
$availableFamilies[] = [
'key' => $intakeSection['key'],
'label' => $intakeSection['label'],
'count' => $intakeSection['count'],
];
$familyCounts[$intakeSection['key']] = $intakeSection['count'];
}
if ($authorizedTenantsById !== []) {
$operationsSection = $this->operationsSection(
workspace: $workspace,
authorizedTenants: $authorizedTenantsById,
selectedTenant: $selectedTenant,
navigationContext: $navigationContext,
);
$allSections[$operationsSection['key']] = $operationsSection;
$availableFamilies[] = [
'key' => $operationsSection['key'],
'label' => $operationsSection['label'],
'count' => $operationsSection['count'],
];
$familyCounts[$operationsSection['key']] = $operationsSection['count'];
}
if ($canViewAlerts) {
$alertsSection = $this->alertsSection(
workspace: $workspace,
authorizedTenants: $authorizedTenantsById,
selectedTenant: $selectedTenant,
navigationContext: $navigationContext,
);
$allSections[$alertsSection['key']] = $alertsSection;
$availableFamilies[] = [
'key' => $alertsSection['key'],
'label' => $alertsSection['label'],
'count' => $alertsSection['count'],
];
$familyCounts[$alertsSection['key']] = $alertsSection['count'];
}
if ($reviewTenantsById !== []) {
$reviewSection = $this->reviewFollowUpSection(
user: $user,
workspace: $workspace,
reviewTenants: $reviewTenantsById,
selectedTenant: $selectedTenant,
navigationContext: $navigationContext,
);
$allSections[$reviewSection['key']] = $reviewSection;
$availableFamilies[] = [
'key' => $reviewSection['key'],
'label' => $reviewSection['label'],
'count' => $reviewSection['count'],
];
$familyCounts[$reviewSection['key']] = $reviewSection['count'];
}
$sections = [];
foreach (self::FAMILY_ORDER as $familyKey) {
$section = $allSections[$familyKey] ?? null;
if (! is_array($section)) {
continue;
}
if ($selectedFamily !== null) {
if ($familyKey === $selectedFamily) {
$sections[] = $section;
}
continue;
}
if ((int) ($section['count'] ?? 0) > 0) {
$sections[] = $section;
}
}
return [
'sections' => $sections,
'available_families' => $availableFamilies,
'family_counts' => $familyCounts,
'total_count' => array_sum($familyCounts),
];
}
/**
* @param array<int, Tenant> $tenants
* @return array<int, Tenant>
*/
private function indexTenants(array $tenants): array
{
$indexed = [];
foreach ($tenants as $tenant) {
$indexed[(int) $tenant->getKey()] = $tenant;
}
return $indexed;
}
/**
* @param array<int, Tenant> $visibleFindingTenants
* @return array<string, mixed>
*/
private function assignedFindingsSection(
User $user,
array $visibleFindingTenants,
?Tenant $selectedTenant,
?CanonicalNavigationContext $navigationContext,
): array {
$baseQuery = $this->assignedFindingsQuery($user, $visibleFindingTenants, $selectedTenant);
$count = (clone $baseQuery)->count();
$overdueCount = (clone $baseQuery)
->whereNotNull('due_at')
->where('due_at', '<', now())
->count();
$entries = $this->orderedAssignedFindingsQuery(clone $baseQuery)
->limit(self::PREVIEW_LIMIT)
->get()
->map(fn (Finding $finding): array => $this->findingEntry($finding, 'assigned_findings', $navigationContext, 10))
->all();
return [
'key' => 'assigned_findings',
'label' => 'Assigned findings',
'count' => $count,
'summary' => $this->assignedFindingsSummary($count, $overdueCount),
'dominant_action_label' => 'Open my findings',
'dominant_action_url' => $this->appendQuery(
MyFindingsInbox::getUrl(
panel: 'admin',
parameters: array_filter([
'tenant' => $selectedTenant?->external_id,
], static fn (mixed $value): bool => is_string($value) && $value !== ''),
),
$navigationContext?->toQuery() ?? [],
),
'entries' => $entries,
'empty_state' => $selectedTenant instanceof Tenant
? 'No assigned findings match this tenant filter right now.'
: 'No assigned findings are visible right now.',
];
}
/**
* @param array<int, Tenant> $visibleFindingTenants
* @return array<string, mixed>
*/
private function intakeFindingsSection(
array $visibleFindingTenants,
?Tenant $selectedTenant,
?CanonicalNavigationContext $navigationContext,
): array {
$baseQuery = $this->intakeFindingsQuery($visibleFindingTenants, $selectedTenant);
$count = (clone $baseQuery)->count();
$needsTriageCount = (clone $baseQuery)
->whereIn('status', [Finding::STATUS_NEW, Finding::STATUS_REOPENED])
->count();
$entries = $this->orderedIntakeFindingsQuery(clone $baseQuery)
->limit(self::PREVIEW_LIMIT)
->get()
->map(fn (Finding $finding): array => $this->findingEntry($finding, 'intake_findings', $navigationContext, 20))
->all();
return [
'key' => 'intake_findings',
'label' => 'Findings intake',
'count' => $count,
'summary' => $this->intakeFindingsSummary($count, $needsTriageCount),
'dominant_action_label' => 'Open findings intake',
'dominant_action_url' => $this->appendQuery(
FindingsIntakeQueue::getUrl(
panel: 'admin',
parameters: array_filter([
'tenant' => $selectedTenant?->external_id,
'view' => $needsTriageCount > 0 ? 'needs_triage' : null,
], static fn (mixed $value): bool => $value !== null && $value !== ''),
),
$navigationContext?->toQuery() ?? [],
),
'entries' => $entries,
'empty_state' => $selectedTenant instanceof Tenant
? 'No intake findings match this tenant filter right now.'
: 'No intake findings are visible right now.',
];
}
/**
* @param array<int, Tenant> $authorizedTenants
* @return array<string, mixed>
*/
private function operationsSection(
Workspace $workspace,
array $authorizedTenants,
?Tenant $selectedTenant,
?CanonicalNavigationContext $navigationContext,
): array {
$terminalQuery = $this->terminalOperationsQuery($workspace, $authorizedTenants, $selectedTenant);
$staleQuery = $this->staleOperationsQuery($workspace, $authorizedTenants, $selectedTenant);
$terminalCount = (clone $terminalQuery)->count();
$staleCount = (clone $staleQuery)->count();
$entries = array_merge(
(clone $terminalQuery)->latest('completed_at')->latest('id')->limit(self::PREVIEW_LIMIT)->get()->all(),
(clone $staleQuery)->latest('created_at')->latest('id')->limit(self::PREVIEW_LIMIT)->get()->all(),
);
$entries = collect($entries)
->unique(fn (OperationRun $run): int => (int) $run->getKey())
->sortBy([
fn (OperationRun $run): int => $run->problemClass() === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP ? 0 : 1,
fn (OperationRun $run): int => -1 * (int) $run->getKey(),
])
->take(self::PREVIEW_LIMIT)
->map(fn (OperationRun $run): array => $this->operationEntry($run, $navigationContext))
->values()
->all();
$dominantProblemClass = $terminalCount > 0
? OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP
: OperationRun::PROBLEM_CLASS_ACTIVE_STALE_ATTENTION;
return [
'key' => 'stale_operations',
'label' => 'Operations follow-up',
'count' => $terminalCount + $staleCount,
'summary' => $this->operationsSummary($terminalCount, $staleCount),
'dominant_action_label' => $terminalCount > 0 ? 'Open terminal follow-up' : 'Open stale operations',
'dominant_action_url' => OperationRunLinks::index(
tenant: $selectedTenant,
context: $navigationContext,
problemClass: $dominantProblemClass,
),
'entries' => $entries,
'empty_state' => $selectedTenant instanceof Tenant
? 'No stale or terminal follow-up operations match this tenant filter right now.'
: 'No stale or terminal follow-up operations are visible right now.',
];
}
/**
* @param array<int, Tenant> $authorizedTenants
* @return array<string, mixed>
*/
private function alertsSection(
Workspace $workspace,
array $authorizedTenants,
?Tenant $selectedTenant,
?CanonicalNavigationContext $navigationContext,
): array {
$baseQuery = $this->alertsQuery($workspace, $authorizedTenants, $selectedTenant);
$count = (clone $baseQuery)->count();
$entries = (clone $baseQuery)
->latest('created_at')
->latest('id')
->limit(self::PREVIEW_LIMIT)
->get()
->map(fn (AlertDelivery $delivery): array => $this->alertEntry($delivery, $navigationContext))
->all();
return [
'key' => 'alert_delivery_failures',
'label' => 'Alert delivery failures',
'count' => $count,
'summary' => $this->alertsSummary($count),
'dominant_action_label' => 'Open alert deliveries',
'dominant_action_url' => $this->appendQuery(
AlertDeliveryResource::getUrl(panel: 'admin'),
array_replace_recursive(
$navigationContext?->toQuery() ?? [],
[
'tableFilters' => array_filter([
'status' => ['value' => AlertDelivery::STATUS_FAILED],
'tenant_id' => $selectedTenant instanceof Tenant
? ['value' => (string) $selectedTenant->getKey()]
: null,
], static fn (mixed $value): bool => $value !== null),
],
),
),
'entries' => $entries,
'empty_state' => $selectedTenant instanceof Tenant
? 'No failed alert deliveries match this tenant filter right now.'
: 'No failed alert deliveries are visible right now.',
];
}
/**
* @param array<int, Tenant> $reviewTenants
* @return array<string, mixed>
*/
private function reviewFollowUpSection(
User $user,
Workspace $workspace,
array $reviewTenants,
?Tenant $selectedTenant,
?CanonicalNavigationContext $navigationContext,
): array {
$tenantIds = $selectedTenant instanceof Tenant
? [(int) $selectedTenant->getKey()]
: array_keys($reviewTenants);
$backupHealthByTenant = $this->backupHealthResolver->assessMany($tenantIds);
$recoveryEvidenceByTenant = $this->restoreSafetyResolver->dashboardRecoveryEvidenceForTenants($tenantIds, $backupHealthByTenant);
$resolved = $this->tenantTriageReviewStateResolver->resolveMany(
workspaceId: (int) $workspace->getKey(),
tenantIds: $tenantIds,
backupHealthByTenant: $backupHealthByTenant,
recoveryEvidenceByTenant: $recoveryEvidenceByTenant,
);
$latestPublishedReviews = $this->tenantReviewRegisterService
->latestPublishedQuery($user, $workspace)
->get()
->keyBy('tenant_id')
->all();
$rawEntries = [];
foreach ($tenantIds as $tenantId) {
$tenant = $reviewTenants[$tenantId] ?? null;
$rows = $resolved['rows'][$tenantId] ?? null;
if (! $tenant instanceof Tenant || ! is_array($rows)) {
continue;
}
foreach ([PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH, PortfolioArrivalContextToken::FAMILY_RECOVERY_EVIDENCE] as $family) {
$row = $rows[$family] ?? null;
if (! is_array($row) || ($row['current_concern_present'] ?? false) !== true) {
continue;
}
$derivedState = $row['derived_state'] ?? null;
if (! in_array($derivedState, [
TenantTriageReview::STATE_FOLLOW_UP_NEEDED,
TenantTriageReview::DERIVED_STATE_CHANGED_SINCE_REVIEW,
], true)) {
continue;
}
$rawEntries[] = $this->reviewEntry(
tenant: $tenant,
family: $family,
row: $row,
latestPublishedReview: $latestPublishedReviews[$tenantId] ?? null,
navigationContext: $navigationContext,
);
}
}
usort($rawEntries, function (array $left, array $right): int {
$leftRank = (int) ($left['urgency_rank'] ?? 0);
$rightRank = (int) ($right['urgency_rank'] ?? 0);
if ($leftRank !== $rightRank) {
return $leftRank <=> $rightRank;
}
return strcmp((string) ($left['headline'] ?? ''), (string) ($right['headline'] ?? ''));
});
$followUpCount = collect($rawEntries)
->where('status_label', 'Follow-up needed')
->count();
$changedCount = collect($rawEntries)
->where('status_label', 'Changed since review')
->count();
return [
'key' => 'review_follow_up',
'label' => 'Review follow-up',
'count' => count($rawEntries),
'summary' => $this->reviewSummary($followUpCount, $changedCount),
'dominant_action_label' => 'Open review follow-up',
'dominant_action_url' => $selectedTenant instanceof Tenant
? $this->appendQuery(CustomerReviewWorkspace::tenantPrefilterUrl($selectedTenant), $navigationContext?->toQuery() ?? [])
: $this->appendQuery(TenantResource::getUrl(panel: 'admin'), array_replace_recursive(
$navigationContext?->toQuery() ?? [],
[
'backup_posture' => [
TenantBackupHealthAssessment::POSTURE_ABSENT,
TenantBackupHealthAssessment::POSTURE_STALE,
TenantBackupHealthAssessment::POSTURE_DEGRADED,
],
'recovery_evidence' => [
TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_WEAKENED,
TenantRecoveryTriagePresentation::RECOVERY_EVIDENCE_UNVALIDATED,
],
'review_state' => [
TenantTriageReview::STATE_FOLLOW_UP_NEEDED,
TenantTriageReview::DERIVED_STATE_CHANGED_SINCE_REVIEW,
],
'triage_sort' => TenantRecoveryTriagePresentation::TRIAGE_SORT_WORST_FIRST,
],
)),
'entries' => array_slice($rawEntries, 0, self::PREVIEW_LIMIT),
'empty_state' => $selectedTenant instanceof Tenant
? 'No review follow-up is visible for this tenant filter right now.'
: 'No review follow-up is visible right now.',
];
}
/**
* @param array<int, Tenant> $visibleFindingTenants
*/
private function assignedFindingsQuery(User $user, array $visibleFindingTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder
{
$tenantIds = $selectedTenant instanceof Tenant
? [(int) $selectedTenant->getKey()]
: array_keys($visibleFindingTenants);
return Finding::query()
->with(['tenant', 'ownerUser:id,name', 'assigneeUser:id,name'])
->withSubjectDisplayName()
->whereIn('tenant_id', $tenantIds === [] ? [-1] : $tenantIds)
->where('assignee_user_id', (int) $user->getKey())
->whereIn('status', Finding::openStatusesForQuery());
}
private function orderedAssignedFindingsQuery(\Illuminate\Database\Eloquent\Builder $query): \Illuminate\Database\Eloquent\Builder
{
return $query
->orderByRaw(
'case when due_at is not null and due_at < ? then 0 when reopened_at is not null then 1 else 2 end asc',
[now()],
)
->orderByRaw('case when due_at is null then 1 else 0 end asc')
->orderBy('due_at')
->orderByDesc('id');
}
/**
* @param array<int, Tenant> $visibleFindingTenants
*/
private function intakeFindingsQuery(array $visibleFindingTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder
{
$tenantIds = $selectedTenant instanceof Tenant
? [(int) $selectedTenant->getKey()]
: array_keys($visibleFindingTenants);
return Finding::query()
->with(['tenant', 'ownerUser:id,name', 'assigneeUser:id,name'])
->withSubjectDisplayName()
->whereIn('tenant_id', $tenantIds === [] ? [-1] : $tenantIds)
->whereNull('assignee_user_id')
->whereIn('status', Finding::openStatusesForQuery());
}
private function orderedIntakeFindingsQuery(\Illuminate\Database\Eloquent\Builder $query): \Illuminate\Database\Eloquent\Builder
{
return $query
->orderByRaw(
"case
when due_at is not null and due_at < ? then 0
when status = ? then 1
when status = ? then 2
else 3
end asc",
[now(), Finding::STATUS_REOPENED, Finding::STATUS_NEW],
)
->orderByRaw('case when due_at is null then 1 else 0 end asc')
->orderBy('due_at')
->orderByDesc('id');
}
/**
* @param array<int, Tenant> $authorizedTenants
*/
private function terminalOperationsQuery(Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder
{
return $this->operationsBaseQuery($workspace, $authorizedTenants, $selectedTenant)
->terminalFollowUp();
}
/**
* @param array<int, Tenant> $authorizedTenants
*/
private function staleOperationsQuery(Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder
{
return $this->operationsBaseQuery($workspace, $authorizedTenants, $selectedTenant)
->activeStaleAttention();
}
/**
* @param array<int, Tenant> $authorizedTenants
*/
private function operationsBaseQuery(Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder
{
$tenantIds = array_keys($authorizedTenants);
return OperationRun::query()
->with('tenant')
->where('workspace_id', (int) $workspace->getKey())
->where(function ($query) use ($selectedTenant, $tenantIds): void {
if ($selectedTenant instanceof Tenant) {
$query->where('tenant_id', (int) $selectedTenant->getKey());
return;
}
$query
->whereIn('tenant_id', $tenantIds === [] ? [-1] : $tenantIds)
->orWhereNull('tenant_id');
});
}
/**
* @param array<int, Tenant> $authorizedTenants
*/
private function alertsQuery(Workspace $workspace, array $authorizedTenants, ?Tenant $selectedTenant): \Illuminate\Database\Eloquent\Builder
{
$tenantIds = array_keys($authorizedTenants);
return AlertDelivery::query()
->with('tenant')
->where('workspace_id', (int) $workspace->getKey())
->where('status', AlertDelivery::STATUS_FAILED)
->where(function ($query) use ($selectedTenant, $tenantIds): void {
if ($selectedTenant instanceof Tenant) {
$query->where('tenant_id', (int) $selectedTenant->getKey());
return;
}
$query
->whereIn('tenant_id', $tenantIds === [] ? [-1] : $tenantIds)
->orWhereNull('tenant_id');
});
}
/**
* @return array<string, mixed>
*/
private function findingEntry(Finding $finding, string $familyKey, ?CanonicalNavigationContext $navigationContext, int $baseUrgencyRank): array
{
$sublineParts = array_values(array_filter([
$finding->owner_user_id !== null ? 'Owner: '.FindingResource::accountableOwnerDisplayFor($finding) : null,
FindingExceptionResource::relativeTimeDescription($finding->due_at) ?? FindingResource::dueAttentionLabelFor($finding),
$finding->reopened_at !== null ? 'Reopened' : null,
]));
return [
'family_key' => $familyKey,
'source_model' => Finding::class,
'source_key' => (string) $finding->getKey(),
'tenant_id' => $finding->tenant ? (int) $finding->tenant->getKey() : null,
'tenant_label' => $finding->tenant?->name,
'headline' => $finding->resolvedSubjectDisplayName() ?? 'Finding #'.$finding->getKey(),
'subline' => $sublineParts === [] ? null : implode(' • ', $sublineParts),
'urgency_rank' => $baseUrgencyRank
+ ($finding->due_at?->isPast() === true ? 0 : 1)
+ ($finding->reopened_at !== null ? 0 : 1),
'status_label' => Str::of((string) $finding->status)->replace('_', ' ')->title()->value(),
'destination_url' => $this->appendQuery(
FindingResource::getUrl('view', ['record' => $finding], panel: 'tenant', tenant: $finding->tenant),
$navigationContext?->toQuery() ?? [],
),
'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox',
];
}
/**
* @return array<string, mixed>
*/
private function operationEntry(OperationRun $run, ?CanonicalNavigationContext $navigationContext): array
{
$problemClass = $run->problemClass();
return [
'family_key' => 'stale_operations',
'source_model' => OperationRun::class,
'source_key' => (string) $run->getKey(),
'tenant_id' => $run->tenant ? (int) $run->tenant->getKey() : null,
'tenant_label' => $run->tenant?->name,
'headline' => $problemClass === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP
? 'Terminal follow-up operation'
: 'Stale active operation',
'subline' => OperationRunLinks::identifier($run),
'urgency_rank' => $problemClass === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP ? 0 : 1,
'status_label' => $problemClass === OperationRun::PROBLEM_CLASS_TERMINAL_FOLLOW_UP
? 'Terminal follow-up'
: 'Stale',
'destination_url' => OperationRunLinks::tenantlessView($run, $navigationContext),
'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox',
];
}
/**
* @return array<string, mixed>
*/
private function alertEntry(AlertDelivery $delivery, ?CanonicalNavigationContext $navigationContext): array
{
$payload = is_array($delivery->payload) ? $delivery->payload : [];
$headline = is_string($payload['title'] ?? null) && $payload['title'] !== ''
? (string) $payload['title']
: 'Failed alert delivery';
$sublineParts = array_values(array_filter([
is_string($delivery->last_error_message) && $delivery->last_error_message !== ''
? $delivery->last_error_message
: null,
is_string($delivery->event_type) && $delivery->event_type !== ''
? $delivery->event_type
: null,
]));
return [
'family_key' => 'alert_delivery_failures',
'source_model' => AlertDelivery::class,
'source_key' => (string) $delivery->getKey(),
'tenant_id' => $delivery->tenant ? (int) $delivery->tenant->getKey() : null,
'tenant_label' => $delivery->tenant?->name,
'headline' => $headline,
'subline' => $sublineParts === [] ? null : implode(' • ', $sublineParts),
'urgency_rank' => 0,
'status_label' => 'Failed',
'destination_url' => $this->appendQuery(
AlertDeliveryResource::getUrl('view', ['record' => $delivery], panel: 'admin'),
$navigationContext?->toQuery() ?? [],
),
'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox',
];
}
/**
* @param array<string, mixed> $row
* @return array<string, mixed>
*/
private function reviewEntry(
Tenant $tenant,
string $family,
array $row,
mixed $latestPublishedReview,
?CanonicalNavigationContext $navigationContext,
): array {
$state = (string) ($row['derived_state'] ?? TenantTriageReview::DERIVED_STATE_NOT_REVIEWED);
$familyLabel = $family === PortfolioArrivalContextToken::FAMILY_BACKUP_HEALTH
? 'Backup health'
: 'Recovery evidence';
$headline = $state === TenantTriageReview::STATE_FOLLOW_UP_NEEDED
? $familyLabel.' needs review follow-up'
: $familyLabel.' changed since review';
$sublineParts = array_values(array_filter([
is_string($row['reviewed_by_user_name'] ?? null) && $row['reviewed_by_user_name'] !== ''
? 'Last review: '.$row['reviewed_by_user_name']
: null,
isset($row['reviewed_at']) && $row['reviewed_at'] !== null
? 'Reviewed '.optional($row['reviewed_at'])->toDateTimeString()
: null,
]));
$destinationUrl = $latestPublishedReview !== null
? TenantReviewResource::tenantScopedUrl('view', ['record' => $latestPublishedReview], $tenant, 'tenant')
: CustomerReviewWorkspace::tenantPrefilterUrl($tenant);
return [
'family_key' => 'review_follow_up',
'source_model' => TenantTriageReview::class,
'source_key' => (string) $tenant->getKey().':'.$family,
'tenant_id' => (int) $tenant->getKey(),
'tenant_label' => $tenant->name,
'headline' => $headline,
'subline' => $sublineParts === [] ? null : implode(' • ', $sublineParts),
'urgency_rank' => $state === TenantTriageReview::STATE_FOLLOW_UP_NEEDED ? 0 : 1,
'status_label' => $state === TenantTriageReview::STATE_FOLLOW_UP_NEEDED
? 'Follow-up needed'
: 'Changed since review',
'destination_url' => $this->appendQuery($destinationUrl, $navigationContext?->toQuery() ?? []),
'back_label' => $navigationContext?->backLinkLabel ?? 'Back to governance inbox',
];
}
private function assignedFindingsSummary(int $count, int $overdueCount): string
{
if ($count === 0) {
return 'No assigned findings are visible in the current scope.';
}
if ($overdueCount > 0) {
return sprintf(
'%d assigned finding%s remain open. %d %s overdue.',
$count,
$count === 1 ? '' : 's',
$overdueCount,
$overdueCount === 1 ? 'is' : 'are',
);
}
return sprintf(
'%d assigned finding%s remain open in the visible scope.',
$count,
$count === 1 ? '' : 's',
);
}
private function intakeFindingsSummary(int $count, int $needsTriageCount): string
{
if ($count === 0) {
return 'No intake findings are visible in the current scope.';
}
return sprintf(
'%d unassigned finding%s remain in intake. %d still need first triage.',
$count,
$count === 1 ? '' : 's',
$needsTriageCount,
);
}
private function operationsSummary(int $terminalCount, int $staleCount): string
{
if ($terminalCount + $staleCount === 0) {
return 'No stale or terminal follow-up operations are visible in the current scope.';
}
if ($terminalCount > 0 && $staleCount > 0) {
return sprintf(
'%d terminal follow-up operation%s and %d stale active run%s need monitoring attention.',
$terminalCount,
$terminalCount === 1 ? '' : 's',
$staleCount,
$staleCount === 1 ? '' : 's',
);
}
if ($terminalCount > 0) {
return sprintf(
'%d terminal follow-up operation%s need monitoring attention.',
$terminalCount,
$terminalCount === 1 ? '' : 's',
);
}
return sprintf(
'%d stale active run%s need monitoring attention.',
$staleCount,
$staleCount === 1 ? '' : 's',
);
}
private function alertsSummary(int $count): string
{
if ($count === 0) {
return 'No failed alert deliveries are visible in the current scope.';
}
return sprintf(
'%d failed alert delivery attempt%s remain visible in this workspace.',
$count,
$count === 1 ? '' : 's',
);
}
private function reviewSummary(int $followUpCount, int $changedCount): string
{
$total = $followUpCount + $changedCount;
if ($total === 0) {
return 'No review follow-up is visible in the current scope.';
}
return sprintf(
'%d review concern%s need attention. %d marked follow-up needed and %d changed since review.',
$total,
$total === 1 ? '' : 's',
$followUpCount,
$changedCount,
);
}
/**
* @param array<string, mixed> $query
*/
private function appendQuery(string $url, array $query): string
{
if ($query === []) {
return $url;
}
$separator = str_contains($url, '?') ? '&' : '?';
return $url.$separator.http_build_query($query);
}
}