Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 3m40s
Productize the evidence overview review-pack process flow: snapshot -> stored report -> review pack -> customer-safe export states with clear gating and copy. Adds feature and browser smoke coverage plus Spec 337 artifacts.
1885 lines
76 KiB
PHP
1885 lines
76 KiB
PHP
<?php
|
|
|
|
declare(strict_types=1);
|
|
|
|
namespace App\Filament\Pages\Monitoring;
|
|
|
|
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
|
use App\Filament\Pages\Reviews\CustomerReviewWorkspace;
|
|
use App\Filament\Resources\EvidenceSnapshotResource;
|
|
use App\Filament\Resources\ReviewPackResource;
|
|
use App\Filament\Resources\StoredReportResource;
|
|
use App\Models\EnvironmentReview;
|
|
use App\Models\EvidenceSnapshot;
|
|
use App\Models\ManagedEnvironment;
|
|
use App\Models\OperationRun;
|
|
use App\Models\ReviewPack;
|
|
use App\Models\StoredReport;
|
|
use App\Models\User;
|
|
use App\Models\Workspace;
|
|
use App\Services\ReviewPackService;
|
|
use App\Support\Auth\Capabilities;
|
|
use App\Support\Badges\BadgeDomain;
|
|
use App\Support\Badges\BadgeRenderer;
|
|
use App\Support\EnvironmentReviewStatus;
|
|
use App\Support\Evidence\EvidenceCompletenessState;
|
|
use App\Support\Evidence\EvidenceSnapshotStatus;
|
|
use App\Support\Filament\TablePaginationProfiles;
|
|
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
|
use App\Support\OperationRunLinks;
|
|
use App\Support\OperationRunOutcome;
|
|
use App\Support\OperationRunStatus;
|
|
use App\Support\ReviewPackStatus;
|
|
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceSlot;
|
|
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceType;
|
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthEnvelope;
|
|
use App\Support\Ui\GovernanceArtifactTruth\ArtifactTruthPresenter;
|
|
use App\Support\Ui\GovernanceArtifactTruth\CompressedGovernanceOutcome;
|
|
use App\Support\Ui\GovernanceArtifactTruth\SurfaceCompressionContext;
|
|
use App\Support\Workspaces\WorkspaceContext;
|
|
use BackedEnum;
|
|
use Filament\Actions\Action;
|
|
use Filament\Pages\Page;
|
|
use Filament\Tables\Columns\TextColumn;
|
|
use Filament\Tables\Concerns\InteractsWithTable;
|
|
use Filament\Tables\Contracts\HasTable;
|
|
use Filament\Tables\Filters\SelectFilter;
|
|
use Filament\Tables\Table;
|
|
use Illuminate\Auth\AuthenticationException;
|
|
use Illuminate\Pagination\LengthAwarePaginator;
|
|
use Illuminate\Support\Collection;
|
|
use Illuminate\Support\Facades\Gate;
|
|
use Illuminate\Support\Str;
|
|
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
|
use UnitEnum;
|
|
|
|
class EvidenceOverview extends Page implements HasTable
|
|
{
|
|
use ClearsWorkspaceHubEnvironmentFilterState;
|
|
use InteractsWithTable;
|
|
|
|
protected const MONITORING_PAGE_STATE_CONTRACT = [
|
|
'surfaceKey' => 'evidence_overview',
|
|
'surfaceType' => 'simple_monitoring',
|
|
'stateFields' => [
|
|
[
|
|
'stateKey' => 'environment_id',
|
|
'stateClass' => 'contextual_prefilter',
|
|
'carrier' => 'query_param',
|
|
'queryRole' => 'durable_restorable',
|
|
'shareable' => true,
|
|
'restorableOnRefresh' => true,
|
|
'tenantSensitive' => true,
|
|
'invalidFallback' => 'discard_and_continue',
|
|
],
|
|
[
|
|
'stateKey' => 'search',
|
|
'stateClass' => 'contextual_prefilter',
|
|
'carrier' => 'query_param',
|
|
'queryRole' => 'durable_restorable',
|
|
'shareable' => true,
|
|
'restorableOnRefresh' => true,
|
|
'tenantSensitive' => false,
|
|
'invalidFallback' => 'discard_and_continue',
|
|
],
|
|
[
|
|
'stateKey' => 'tableFilters',
|
|
'stateClass' => 'shareable_restorable',
|
|
'carrier' => 'session',
|
|
'queryRole' => 'unsupported',
|
|
'shareable' => false,
|
|
'restorableOnRefresh' => true,
|
|
'tenantSensitive' => true,
|
|
'invalidFallback' => 'discard_and_continue',
|
|
],
|
|
[
|
|
'stateKey' => 'tableSort',
|
|
'stateClass' => 'shareable_restorable',
|
|
'carrier' => 'session',
|
|
'queryRole' => 'unsupported',
|
|
'shareable' => false,
|
|
'restorableOnRefresh' => true,
|
|
'tenantSensitive' => false,
|
|
'invalidFallback' => 'discard_and_continue',
|
|
],
|
|
],
|
|
'hydrationRule' => [
|
|
'precedenceOrder' => ['query', 'session', 'default'],
|
|
'appliesOnInitialMountOnly' => true,
|
|
'activeStateBecomesAuthoritativeAfterMount' => true,
|
|
'clearsOnTenantSwitch' => ['environment_id', 'managed_environment_id'],
|
|
'invalidRequestedStateFallback' => 'discard_and_continue',
|
|
],
|
|
'inspectContract' => [
|
|
'primaryModel' => 'none',
|
|
'selectedStateKey' => null,
|
|
'openedBy' => ['row_navigation'],
|
|
'presentation' => 'navigate_to_canonical_detail',
|
|
'shareable' => false,
|
|
'invalidSelectionFallback' => 'discard_and_continue',
|
|
],
|
|
'shareableStateKeys' => ['environment_id', 'search'],
|
|
'localOnlyStateKeys' => [],
|
|
];
|
|
|
|
protected static bool $isDiscovered = false;
|
|
|
|
protected static bool $shouldRegisterNavigation = false;
|
|
|
|
protected static string|BackedEnum|null $navigationIcon = 'heroicon-o-shield-check';
|
|
|
|
protected static string|UnitEnum|null $navigationGroup = 'Monitoring';
|
|
|
|
protected static ?string $title = 'Evidence Overview';
|
|
|
|
protected string $view = 'filament.pages.monitoring.evidence-overview';
|
|
|
|
/**
|
|
* @var list<array<string, mixed>>
|
|
*/
|
|
public array $rows = [];
|
|
|
|
/**
|
|
* @var array<int, ManagedEnvironment>|null
|
|
*/
|
|
private ?array $accessibleTenants = null;
|
|
|
|
private ?Collection $cachedSnapshots = null;
|
|
|
|
public static function actionSurfaceDeclaration(): ActionSurfaceDeclaration
|
|
{
|
|
return ActionSurfaceDeclaration::forPage(ActionSurfaceProfile::ListOnlyReadOnly, ActionSurfaceType::ReadOnlyRegistryReport)
|
|
->satisfy(ActionSurfaceSlot::ListHeader, 'The overview header exposes a clear-filters action when a tenant prefilter is active.')
|
|
->satisfy(ActionSurfaceSlot::InspectAffordance, ActionSurfaceInspectAffordance::ClickableRow->value)
|
|
->exempt(ActionSurfaceSlot::ListRowMoreMenu, 'The overview exposes a single drill-down link per row without a More menu.')
|
|
->exempt(ActionSurfaceSlot::ListBulkMoreGroup, 'The overview does not expose bulk actions.')
|
|
->satisfy(ActionSurfaceSlot::ListEmptyState, 'The empty state explains the current scope and offers a clear-filters CTA.');
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public static function monitoringPageStateContract(): array
|
|
{
|
|
return self::MONITORING_PAGE_STATE_CONTRACT;
|
|
}
|
|
|
|
public function mount(): void
|
|
{
|
|
$this->authorizeWorkspaceAccess();
|
|
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
|
$this->seedTableStateFromQuery();
|
|
|
|
$this->mountInteractsWithTable();
|
|
$this->resetWorkspaceHubEnvironmentFilterStateForCleanEntry(request());
|
|
$this->rows = $this->rowsForState($this->tableFilters ?? [], $this->tableSearch)->values()->all();
|
|
}
|
|
|
|
public function table(Table $table): Table
|
|
{
|
|
return $table
|
|
->defaultSort('tenant_name')
|
|
->defaultPaginationPageOption(25)
|
|
->paginated(TablePaginationProfiles::customPage())
|
|
->persistFiltersInSession()
|
|
->persistSearchInSession()
|
|
->persistSortInSession()
|
|
->searchable()
|
|
->searchPlaceholder('Search evidence or next step')
|
|
->records(function (
|
|
?string $sortColumn,
|
|
?string $sortDirection,
|
|
?string $search,
|
|
array $filters,
|
|
int $page,
|
|
int $recordsPerPage
|
|
): LengthAwarePaginator {
|
|
$rows = $this->rowsForState($filters, $search);
|
|
$rows = $this->sortRows($rows, $sortColumn, $sortDirection);
|
|
|
|
return $this->paginateRows($rows, $page, $recordsPerPage);
|
|
})
|
|
->filters([
|
|
SelectFilter::make('managed_environment_id')
|
|
->label('Environment')
|
|
->options(fn (): array => $this->tenantFilterOptions())
|
|
->searchable(),
|
|
])
|
|
->columns([
|
|
TextColumn::make('tenant_name')
|
|
->label('Environment')
|
|
->sortable(),
|
|
TextColumn::make('artifact_truth_label')
|
|
->label('Outcome')
|
|
->badge()
|
|
->color(fn (array $record): string => (string) ($record['artifact_truth_color'] ?? 'gray'))
|
|
->icon(fn (array $record): ?string => is_string($record['artifact_truth_icon'] ?? null) ? $record['artifact_truth_icon'] : null)
|
|
->description(fn (array $record): ?string => is_string($record['artifact_truth_explanation'] ?? null) ? $record['artifact_truth_explanation'] : null)
|
|
->sortable()
|
|
->wrap(),
|
|
TextColumn::make('generated_at')
|
|
->label('Generated')
|
|
->placeholder('—')
|
|
->sortable(),
|
|
TextColumn::make('next_step')
|
|
->label('Next step')
|
|
->wrap(),
|
|
])
|
|
->recordUrl(fn ($record): ?string => is_array($record) ? (is_string($record['view_url'] ?? null) ? $record['view_url'] : null) : null)
|
|
->actions([])
|
|
->bulkActions([])
|
|
->emptyStateHeading('No evidence snapshots in this scope')
|
|
->emptyStateDescription(fn (): string => $this->hasActiveOverviewFilters()
|
|
? 'Clear the current filters to return to the full workspace evidence overview.'
|
|
: 'Adjust filters or create an environment snapshot to populate the workspace overview.')
|
|
->emptyStateActions([
|
|
Action::make('clear_filters')
|
|
->label('Clear filters')
|
|
->icon('heroicon-o-x-mark')
|
|
->color('gray')
|
|
->visible(fn (): bool => $this->hasActiveOverviewFilters())
|
|
->action(fn (): mixed => $this->clearOverviewFilters()),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* @return array<Action>
|
|
*/
|
|
protected function getHeaderActions(): array
|
|
{
|
|
return [
|
|
Action::make('clear_filters')
|
|
->label('Clear filters')
|
|
->color('gray')
|
|
->visible(fn (): bool => $this->hasActiveOverviewFilters())
|
|
->action(fn (): mixed => $this->clearOverviewFilters()),
|
|
];
|
|
}
|
|
|
|
public function clearOverviewFilters(): void
|
|
{
|
|
$this->tableFilters = [
|
|
'managed_environment_id' => ['value' => null],
|
|
];
|
|
$this->tableDeferredFilters = $this->tableFilters;
|
|
$this->tableSearch = '';
|
|
$this->rows = $this->rowsForState($this->tableFilters, $this->tableSearch)->values()->all();
|
|
|
|
session()->forget($this->getTableFiltersSessionKey());
|
|
session()->put($this->getTableSearchSessionKey(), $this->tableSearch);
|
|
$this->clearWorkspaceHubEnvironmentFilterState(request());
|
|
|
|
$this->redirectToCleanWorkspaceHubUrl($this->overviewUrl(), request());
|
|
}
|
|
|
|
/**
|
|
* @return array{label: string, clear_url: string}|null
|
|
*/
|
|
public function environmentFilterChip(): ?array
|
|
{
|
|
$tenant = $this->filteredTenant();
|
|
|
|
if (! $tenant instanceof ManagedEnvironment) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'label' => (string) $tenant->name,
|
|
'clear_url' => $this->cleanWorkspaceHubUrl($this->overviewUrl()),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
public function evidenceDisclosurePayload(): array
|
|
{
|
|
$snapshots = $this->scopedSnapshots();
|
|
$filteredTenant = $this->filteredTenant();
|
|
$primarySnapshot = $this->primarySnapshotForScope($snapshots, $filteredTenant);
|
|
$primaryTenant = $filteredTenant instanceof ManagedEnvironment
|
|
? $filteredTenant
|
|
: $primarySnapshot?->tenant;
|
|
$primaryReviewPack = $primarySnapshot instanceof EvidenceSnapshot
|
|
? $this->latestReviewPackForSnapshot($primarySnapshot)
|
|
: null;
|
|
$primaryStoredReport = $primaryTenant instanceof ManagedEnvironment
|
|
? $this->latestStoredReportForTenant($primaryTenant)
|
|
: null;
|
|
$primaryReview = $primarySnapshot instanceof EvidenceSnapshot
|
|
? $this->latestEnvironmentReviewForSnapshot($primarySnapshot)
|
|
: null;
|
|
$primaryOperationRun = $this->primaryOperationRun($primarySnapshot, $primaryReviewPack);
|
|
$decisionState = $this->evidenceReviewPackDecisionState(
|
|
$primarySnapshot,
|
|
$primaryReviewPack,
|
|
$primaryStoredReport,
|
|
$primaryReview,
|
|
$primaryTenant,
|
|
$primaryOperationRun,
|
|
);
|
|
$primaryAction = $this->primaryEvidenceAction(
|
|
$decisionState,
|
|
$primarySnapshot,
|
|
$primaryReviewPack,
|
|
$primaryStoredReport,
|
|
$primaryReview,
|
|
$primaryTenant,
|
|
$primaryOperationRun,
|
|
);
|
|
$decisionCard = $this->evidenceReviewPackDecisionCard(
|
|
$decisionState,
|
|
$primarySnapshot,
|
|
$primaryReviewPack,
|
|
$primaryStoredReport,
|
|
$primaryReview,
|
|
$primaryTenant,
|
|
$primaryOperationRun,
|
|
$primaryAction,
|
|
);
|
|
|
|
return [
|
|
'scope_label' => $this->evidenceScopeLabel(),
|
|
'scope_description' => $this->evidenceScopeDescription($snapshots),
|
|
'snapshot_count' => $snapshots->count(),
|
|
'primary_title' => $primaryTenant instanceof ManagedEnvironment
|
|
? $primaryTenant->name
|
|
: 'Select an environment to evaluate evidence readiness',
|
|
'primary_summary' => $primarySnapshot instanceof EvidenceSnapshot
|
|
? $this->productSafeEvidenceReason($this->snapshotOutcome($primarySnapshot)->primaryReason)
|
|
: ($primaryTenant instanceof ManagedEnvironment
|
|
? 'No evidence snapshot has been generated for the active workspace scope.'
|
|
: 'Choose an authorized environment from the scope control before building a customer-safe proof path.'),
|
|
'primary_proof_state' => $this->primaryProofState(
|
|
$primarySnapshot,
|
|
$primaryReviewPack,
|
|
$primaryStoredReport,
|
|
$primaryOperationRun,
|
|
),
|
|
'decision_card' => $decisionCard,
|
|
'readiness_flow' => $this->evidenceReviewPackReadinessFlow(
|
|
$decisionState,
|
|
$primarySnapshot,
|
|
$primaryReviewPack,
|
|
$primaryStoredReport,
|
|
$primaryReview,
|
|
$primaryTenant,
|
|
),
|
|
'primary_action' => $primaryAction,
|
|
'review_pack_coverage' => $this->reviewPackCoverageSummary($primaryReviewPack),
|
|
'proof_items' => $this->evidenceReviewPackProofItems(
|
|
$primarySnapshot,
|
|
$primaryReviewPack,
|
|
$primaryStoredReport,
|
|
$primaryReview,
|
|
$primaryTenant,
|
|
$primaryOperationRun,
|
|
),
|
|
'cards' => [
|
|
$this->snapshotProofCard($primarySnapshot),
|
|
$this->reviewPackProofCard($primaryReviewPack, $primarySnapshot),
|
|
$this->storedReportProofCard($primaryStoredReport, $primaryTenant),
|
|
$this->operationProofCard($primaryOperationRun),
|
|
],
|
|
'path_items' => [
|
|
$this->snapshotPathItem($primarySnapshot),
|
|
$this->reviewPackPathItem($primaryReviewPack, $primarySnapshot),
|
|
$this->storedReportPathItem($primaryStoredReport, $primaryTenant),
|
|
$this->operationPathItem($primaryOperationRun),
|
|
],
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, EvidenceSnapshot> $snapshots
|
|
*/
|
|
private function primarySnapshotForScope(Collection $snapshots, ?ManagedEnvironment $filteredTenant): ?EvidenceSnapshot
|
|
{
|
|
if ($filteredTenant instanceof ManagedEnvironment) {
|
|
return $this->latestEvidenceSnapshotForTenant($filteredTenant) ?? $snapshots->first();
|
|
}
|
|
|
|
return $snapshots->first();
|
|
}
|
|
|
|
private function latestEvidenceSnapshotForTenant(ManagedEnvironment $tenant): ?EvidenceSnapshot
|
|
{
|
|
return EvidenceSnapshot::query()
|
|
->with([
|
|
'tenant',
|
|
'operationRun',
|
|
'reviewPacks.operationRun',
|
|
'reviewPacks.environmentReview.currentExportReviewPack',
|
|
'items',
|
|
])
|
|
->where('workspace_id', (int) $tenant->workspace_id)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->whereIn('status', [
|
|
EvidenceSnapshotStatus::Queued->value,
|
|
EvidenceSnapshotStatus::Generating->value,
|
|
EvidenceSnapshotStatus::Active->value,
|
|
EvidenceSnapshotStatus::Failed->value,
|
|
EvidenceSnapshotStatus::Expired->value,
|
|
])
|
|
->orderByRaw('COALESCE(generated_at, created_at) DESC')
|
|
->first();
|
|
}
|
|
|
|
private function latestEnvironmentReviewForSnapshot(EvidenceSnapshot $snapshot): ?EnvironmentReview
|
|
{
|
|
return EnvironmentReview::query()
|
|
->with(['currentExportReviewPack.operationRun', 'operationRun'])
|
|
->where('workspace_id', (int) $snapshot->workspace_id)
|
|
->where('managed_environment_id', (int) $snapshot->managed_environment_id)
|
|
->where('evidence_snapshot_id', (int) $snapshot->getKey())
|
|
->orderByRaw('COALESCE(generated_at, created_at) DESC')
|
|
->first();
|
|
}
|
|
|
|
private function evidenceReviewPackDecisionState(
|
|
?EvidenceSnapshot $snapshot,
|
|
?ReviewPack $reviewPack,
|
|
?StoredReport $storedReport,
|
|
?EnvironmentReview $review,
|
|
?ManagedEnvironment $tenant,
|
|
?OperationRun $operationRun,
|
|
): string {
|
|
if (! $tenant instanceof ManagedEnvironment && ! $snapshot instanceof EvidenceSnapshot) {
|
|
return 'source_unavailable';
|
|
}
|
|
|
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
|
return 'no_snapshot';
|
|
}
|
|
|
|
$snapshotStatus = (string) $snapshot->status;
|
|
|
|
if (in_array($snapshotStatus, [EvidenceSnapshotStatus::Queued->value, EvidenceSnapshotStatus::Generating->value], true)) {
|
|
return 'snapshot_generating';
|
|
}
|
|
|
|
if ($snapshotStatus === EvidenceSnapshotStatus::Failed->value) {
|
|
return 'snapshot_failed';
|
|
}
|
|
|
|
if ($this->snapshotIsStale($snapshot)) {
|
|
return 'snapshot_stale';
|
|
}
|
|
|
|
if ($reviewPack instanceof ReviewPack) {
|
|
$packStatus = (string) $reviewPack->status;
|
|
|
|
if (in_array($packStatus, [ReviewPackStatus::Queued->value, ReviewPackStatus::Generating->value], true)) {
|
|
return 'pack_generating';
|
|
}
|
|
|
|
if ($packStatus === ReviewPackStatus::Failed->value) {
|
|
return 'pack_failed';
|
|
}
|
|
}
|
|
|
|
if (! $storedReport instanceof StoredReport) {
|
|
return 'report_missing';
|
|
}
|
|
|
|
if (! $reviewPack instanceof ReviewPack) {
|
|
return 'pack_required';
|
|
}
|
|
|
|
if (! $this->reviewPackHasExportArtifact($reviewPack)) {
|
|
return 'export_unavailable';
|
|
}
|
|
|
|
if ($this->customerSafeOutputReady($review, $reviewPack)) {
|
|
return $this->canDownloadReviewPack($reviewPack, $tenant) ? 'export_available' : 'customer_safe_ready';
|
|
}
|
|
|
|
return $operationRun instanceof OperationRun && (string) $operationRun->outcome === OperationRunOutcome::Failed->value
|
|
? 'pack_failed'
|
|
: 'customer_review_required';
|
|
}
|
|
|
|
/**
|
|
* @param array{label:string,url:string}|null $primaryAction
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function evidenceReviewPackDecisionCard(
|
|
string $state,
|
|
?EvidenceSnapshot $snapshot,
|
|
?ReviewPack $reviewPack,
|
|
?StoredReport $storedReport,
|
|
?EnvironmentReview $review,
|
|
?ManagedEnvironment $tenant,
|
|
?OperationRun $operationRun,
|
|
?array $primaryAction,
|
|
): array {
|
|
$base = match ($state) {
|
|
'no_snapshot' => [
|
|
'status' => 'Evidence snapshot required',
|
|
'tone' => 'warning',
|
|
'reason' => 'No evidence snapshot is available for the selected review scope.',
|
|
'impact' => 'Review pack output cannot be trusted or exported yet.',
|
|
],
|
|
'snapshot_generating' => [
|
|
'status' => 'Evidence generation in progress',
|
|
'tone' => 'info',
|
|
'reason' => 'Evidence snapshot generation is currently running.',
|
|
'impact' => 'Review pack output is not final yet.',
|
|
],
|
|
'snapshot_failed' => [
|
|
'status' => 'Evidence generation failed',
|
|
'tone' => 'danger',
|
|
'reason' => 'Evidence snapshot generation ended with errors.',
|
|
'impact' => 'Review pack output cannot be generated from this evidence yet.',
|
|
],
|
|
'snapshot_stale' => [
|
|
'status' => 'Evidence refresh required',
|
|
'tone' => 'warning',
|
|
'reason' => 'Evidence exists, but its freshness is outside the acceptable window or the snapshot is expired or stale.',
|
|
'impact' => 'Review pack output should not be treated as current until evidence is refreshed.',
|
|
],
|
|
'report_missing' => [
|
|
'status' => 'Stored report required',
|
|
'tone' => 'warning',
|
|
'reason' => 'Evidence snapshot exists, but no stored report is available for this review output.',
|
|
'impact' => 'Evidence is present but not yet packaged for consumption.',
|
|
],
|
|
'pack_required' => [
|
|
'status' => 'Review pack required',
|
|
'tone' => 'warning',
|
|
'reason' => 'Stored report exists, but a review pack has not been generated.',
|
|
'impact' => 'Customer-safe delivery is not ready yet.',
|
|
],
|
|
'pack_generating' => [
|
|
'status' => 'Review pack generation in progress',
|
|
'tone' => 'info',
|
|
'reason' => 'Review pack generation is currently running.',
|
|
'impact' => 'Customer output is not final yet.',
|
|
],
|
|
'pack_failed' => [
|
|
'status' => 'Review pack generation failed',
|
|
'tone' => 'danger',
|
|
'reason' => 'Review pack generation ended with errors.',
|
|
'impact' => 'Customer-safe output cannot be generated from this pack yet.',
|
|
],
|
|
'customer_review_required' => [
|
|
'status' => 'Customer-safe review required',
|
|
'tone' => 'warning',
|
|
'reason' => 'A review pack exists, but customer-safe output has not been confirmed by repo-backed review/package readiness.',
|
|
'impact' => 'Do not share the pack externally until it has been reviewed.',
|
|
],
|
|
'customer_safe_ready' => [
|
|
'status' => 'Customer-safe output ready',
|
|
'tone' => 'success',
|
|
'reason' => 'Review pack output is backed by a published review and its current export pack.',
|
|
'impact' => 'Authorized users can use the pack; external sharing remains governed by workspace policy.',
|
|
],
|
|
'export_available' => [
|
|
'status' => 'Review pack export available',
|
|
'tone' => 'success',
|
|
'reason' => 'A generated export artifact is available for authorized download.',
|
|
'impact' => 'Download is available from this surface; external sharing remains governed by workspace policy.',
|
|
],
|
|
'export_unavailable' => [
|
|
'status' => 'Export unavailable',
|
|
'tone' => 'warning',
|
|
'reason' => 'No generated export artifact is available, the pack is not ready, the file is missing or expired, or external delivery is not configured.',
|
|
'impact' => 'Evidence package cannot be downloaded or delivered from this surface yet.',
|
|
],
|
|
default => [
|
|
'status' => 'Evidence source unavailable',
|
|
'tone' => 'gray',
|
|
'reason' => 'No repo-backed source data is selected for this proof scope.',
|
|
'impact' => 'No customer or auditor consumption decision should rely on this surface yet.',
|
|
],
|
|
};
|
|
|
|
return $base + [
|
|
'question' => 'Is this evidence package ready for customer or auditor consumption?',
|
|
'statusLabel' => 'Status',
|
|
'reasonLabel' => 'Reason',
|
|
'impactLabel' => 'Impact',
|
|
'nextActionLabel' => 'Primary next action',
|
|
'evidenceLabel' => 'Evidence path',
|
|
'evidence' => $this->decisionEvidenceSummary($snapshot, $reviewPack, $storedReport, $review, $tenant, $operationRun),
|
|
'actionLabel' => $primaryAction['label'] ?? ($state === 'source_unavailable' ? 'Select environment scope' : 'No authorized action available'),
|
|
'actionUrl' => $primaryAction['url'] ?? null,
|
|
'helperText' => $this->decisionActionHelper($state, $tenant, $primaryAction),
|
|
'actionDescription' => $this->decisionActionDescription($state, $primaryAction),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function evidenceReviewPackReadinessFlow(
|
|
string $state,
|
|
?EvidenceSnapshot $snapshot,
|
|
?ReviewPack $reviewPack,
|
|
?StoredReport $storedReport,
|
|
?EnvironmentReview $review,
|
|
?ManagedEnvironment $tenant,
|
|
): array {
|
|
$sourceState = $tenant instanceof ManagedEnvironment ? 'Available' : 'Unavailable';
|
|
$snapshotState = $this->snapshotFlowState($snapshot);
|
|
$storedReportState = $this->storedReportFlowState($snapshot, $storedReport);
|
|
$reviewPackState = $this->reviewPackFlowState($snapshot, $storedReport, $reviewPack);
|
|
$customerSafeState = $this->customerSafeFlowState($snapshot, $reviewPack, $review);
|
|
$exportState = $this->exportFlowState($snapshot, $reviewPack, $tenant);
|
|
|
|
return [
|
|
$this->flowStep('Source data selected', $sourceState, $sourceState === 'Available' ? 'Environment scope is established from the workspace context.' : 'Select an authorized environment before building customer-safe evidence.', $this->flowTone($sourceState), $state === 'source_unavailable'),
|
|
$this->flowStep('Evidence snapshot', $snapshotState, $this->snapshotFlowDescription($snapshotState), $this->flowTone($snapshotState), in_array($state, ['no_snapshot', 'snapshot_generating', 'snapshot_failed', 'snapshot_stale'], true)),
|
|
$this->flowStep('Stored report', $storedReportState, $this->storedReportFlowDescription($storedReportState), $this->flowTone($storedReportState), $state === 'report_missing'),
|
|
$this->flowStep('Review pack', $reviewPackState, $this->reviewPackFlowDescription($reviewPackState), $this->flowTone($reviewPackState), in_array($state, ['pack_required', 'pack_generating', 'pack_failed'], true)),
|
|
$this->flowStep('Customer-safe output', $customerSafeState, $this->customerSafeFlowDescription($customerSafeState), $this->flowTone($customerSafeState), $state === 'customer_review_required'),
|
|
$this->flowStep('Export / delivery', $exportState, $this->exportFlowDescription($exportState), $this->flowTone($exportState), in_array($state, ['export_available', 'export_unavailable'], true)),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{description:string,items:list<array{label:string,value:string}>}
|
|
*/
|
|
private function reviewPackCoverageSummary(?ReviewPack $reviewPack): array
|
|
{
|
|
if (! $reviewPack instanceof ReviewPack) {
|
|
return [
|
|
'description' => 'Review pack coverage details are not available for this artifact.',
|
|
'items' => [],
|
|
];
|
|
}
|
|
|
|
$summary = is_array($reviewPack->summary) ? $reviewPack->summary : [];
|
|
$items = [];
|
|
|
|
foreach ([
|
|
'finding_count' => 'Findings included',
|
|
'report_count' => 'Reports included',
|
|
'operation_count' => 'Operations included',
|
|
'section_count' => 'Review sections',
|
|
] as $key => $label) {
|
|
if (array_key_exists($key, $summary) && is_numeric($summary[$key])) {
|
|
$items[] = ['label' => $label, 'value' => (string) ((int) $summary[$key])];
|
|
}
|
|
}
|
|
|
|
$requiredDimensions = data_get($summary, 'evidence_resolution.required_dimensions');
|
|
|
|
if (is_array($requiredDimensions)) {
|
|
$items[] = ['label' => 'Evidence dimensions', 'value' => (string) count($requiredDimensions)];
|
|
}
|
|
|
|
if (filled($reviewPack->file_path)) {
|
|
$items[] = ['label' => 'Generated files', 'value' => '1'];
|
|
}
|
|
|
|
return [
|
|
'description' => $items === []
|
|
? 'Review pack coverage details are not available for this artifact.'
|
|
: 'Coverage values are derived from the generated review-pack summary and file metadata.',
|
|
'items' => $items,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return list<array<string, mixed>>
|
|
*/
|
|
private function evidenceReviewPackProofItems(
|
|
?EvidenceSnapshot $snapshot,
|
|
?ReviewPack $reviewPack,
|
|
?StoredReport $storedReport,
|
|
?EnvironmentReview $review,
|
|
?ManagedEnvironment $tenant,
|
|
?OperationRun $operationRun,
|
|
): array {
|
|
$operationDescription = $operationRun instanceof OperationRun
|
|
? sprintf(
|
|
'%s · %s · Started %s · Completed %s · Requested by %s',
|
|
(string) $operationRun->type,
|
|
Str::headline((string) $operationRun->outcome),
|
|
$operationRun->started_at?->toDateTimeString() ?? '—',
|
|
$operationRun->completed_at?->toDateTimeString() ?? '—',
|
|
(string) ($operationRun->initiator_name ?: 'Unknown'),
|
|
)
|
|
: 'Operation proof unavailable. No generation operation is linked to this artifact.';
|
|
|
|
return [
|
|
$this->proofItem('Source data', $tenant instanceof ManagedEnvironment ? 'Available' : 'Unavailable', $tenant instanceof ManagedEnvironment ? 'Workspace and environment scope are established.' : 'No environment scope is selected.', $tenant instanceof ManagedEnvironment ? 'success' : 'gray'),
|
|
$this->proofItemFromCard($this->snapshotProofCard($snapshot)),
|
|
$this->proofItemFromCard($this->storedReportProofCard($storedReport, $tenant)),
|
|
$this->proofItemFromCard($this->reviewPackProofCard($reviewPack, $snapshot)),
|
|
$this->proofItem('Operation proof', $operationRun instanceof OperationRun ? $this->operationProofState($operationRun) : 'Unavailable', $operationDescription, $operationRun instanceof OperationRun ? $this->operationProofTone($operationRun) : 'gray', $operationRun instanceof OperationRun ? OperationRunLinks::tenantlessView($operationRun) : null, $operationRun instanceof OperationRun ? OperationRunLinks::openLabel() : null),
|
|
$this->proofItem('Export artifact', $this->exportFlowState($snapshot, $reviewPack, $tenant), $this->exportProofDescription($reviewPack, $tenant), $this->flowTone($this->exportFlowState($snapshot, $reviewPack, $tenant)), $this->canDownloadReviewPack($reviewPack, $tenant) ? app(ReviewPackService::class)->generateDownloadUrl($reviewPack) : null, $this->canDownloadReviewPack($reviewPack, $tenant) ? 'Download export' : null),
|
|
$this->proofItem('Customer-safe state', $this->customerSafeFlowState($snapshot, $reviewPack, $review), $this->customerSafeFlowDescription($this->customerSafeFlowState($snapshot, $reviewPack, $review)), $this->flowTone($this->customerSafeFlowState($snapshot, $reviewPack, $review)), $tenant instanceof ManagedEnvironment ? CustomerReviewWorkspace::environmentFilterUrl($tenant) : null, $tenant instanceof ManagedEnvironment ? 'Open customer workspace' : null),
|
|
$this->proofItem('Diagnostics', 'Collapsed', 'Raw report metadata, raw evidence payloads, generation diagnostics, export diagnostics, provider diagnostics, stack traces, and internal exceptions stay collapsed by default.', 'gray'),
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{label:string,state:string,description:string,tone:string,currentBlocker:bool}
|
|
*/
|
|
private function flowStep(string $label, string $state, string $description, string $tone, bool $currentBlocker = false): array
|
|
{
|
|
return [
|
|
'label' => $label,
|
|
'state' => $state,
|
|
'description' => $description,
|
|
'tone' => $tone,
|
|
'currentBlocker' => $currentBlocker,
|
|
];
|
|
}
|
|
|
|
private function snapshotFlowState(?EvidenceSnapshot $snapshot): string
|
|
{
|
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
|
return 'Missing';
|
|
}
|
|
|
|
return match ((string) $snapshot->status) {
|
|
EvidenceSnapshotStatus::Queued->value, EvidenceSnapshotStatus::Generating->value => 'Generating',
|
|
EvidenceSnapshotStatus::Failed->value => 'Failed',
|
|
default => $this->snapshotIsStale($snapshot) ? 'Stale' : 'Available',
|
|
};
|
|
}
|
|
|
|
private function storedReportFlowState(?EvidenceSnapshot $snapshot, ?StoredReport $storedReport): string
|
|
{
|
|
if (! $snapshot instanceof EvidenceSnapshot || in_array($this->snapshotFlowState($snapshot), ['Generating', 'Failed'], true)) {
|
|
return 'Unavailable';
|
|
}
|
|
|
|
return $storedReport instanceof StoredReport ? 'Available' : 'Missing';
|
|
}
|
|
|
|
private function reviewPackFlowState(?EvidenceSnapshot $snapshot, ?StoredReport $storedReport, ?ReviewPack $reviewPack): string
|
|
{
|
|
if ($reviewPack instanceof ReviewPack) {
|
|
return match ((string) $reviewPack->status) {
|
|
ReviewPackStatus::Queued->value, ReviewPackStatus::Generating->value => 'Generating',
|
|
ReviewPackStatus::Failed->value => 'Failed',
|
|
ReviewPackStatus::Ready->value => 'Available',
|
|
default => 'Unavailable',
|
|
};
|
|
}
|
|
|
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
|
return 'Unavailable';
|
|
}
|
|
|
|
return $storedReport instanceof StoredReport ? 'Required' : 'Unavailable';
|
|
}
|
|
|
|
private function customerSafeFlowState(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack, ?EnvironmentReview $review): string
|
|
{
|
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
|
return 'Not ready';
|
|
}
|
|
|
|
if (! $reviewPack instanceof ReviewPack || ! $reviewPack->isReady()) {
|
|
return 'Not ready';
|
|
}
|
|
|
|
return $this->customerSafeOutputReady($review, $reviewPack) ? 'Ready' : 'Needs review';
|
|
}
|
|
|
|
private function exportFlowState(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack, ?ManagedEnvironment $tenant): string
|
|
{
|
|
if (! $snapshot instanceof EvidenceSnapshot || ! $reviewPack instanceof ReviewPack) {
|
|
return 'Unavailable';
|
|
}
|
|
|
|
if ((string) $reviewPack->status === ReviewPackStatus::Failed->value) {
|
|
return 'Failed';
|
|
}
|
|
|
|
if (! $reviewPack->isReady()) {
|
|
return 'Unavailable';
|
|
}
|
|
|
|
return $this->reviewPackHasExportArtifact($reviewPack) && $this->canDownloadReviewPack($reviewPack, $tenant)
|
|
? 'Available'
|
|
: 'Required';
|
|
}
|
|
|
|
private function snapshotFlowDescription(string $state): string
|
|
{
|
|
return match ($state) {
|
|
'Available' => 'Snapshot proof exists.',
|
|
'Missing' => 'No snapshot in scope.',
|
|
'Generating' => 'Generation is running.',
|
|
'Failed' => 'Generation failed.',
|
|
'Stale' => 'Evidence is stale or expired.',
|
|
default => 'Evidence snapshot state is unavailable.',
|
|
};
|
|
}
|
|
|
|
private function storedReportFlowDescription(string $state): string
|
|
{
|
|
return match ($state) {
|
|
'Available' => 'Stored report exists.',
|
|
'Missing' => 'No report for this output.',
|
|
default => 'Depends on snapshot availability.',
|
|
};
|
|
}
|
|
|
|
private function reviewPackFlowDescription(string $state): string
|
|
{
|
|
return match ($state) {
|
|
'Available' => 'Review pack exists.',
|
|
'Required' => 'Generate a review pack.',
|
|
'Generating' => 'Generation is running.',
|
|
'Failed' => 'Generation failed.',
|
|
default => 'Blocked by earlier proof.',
|
|
};
|
|
}
|
|
|
|
private function customerSafeFlowDescription(string $state): string
|
|
{
|
|
return match ($state) {
|
|
'Ready' => 'Published review backs current export pack.',
|
|
'Needs review' => 'Readiness is not confirmed.',
|
|
'Not ready' => 'Customer-safe output is not ready.',
|
|
default => 'Customer-safe output readiness is unavailable.',
|
|
};
|
|
}
|
|
|
|
private function exportFlowDescription(string $state): string
|
|
{
|
|
return match ($state) {
|
|
'Available' => 'Authorized download is available.',
|
|
'Required' => 'Export file is missing or unauthorized.',
|
|
'Failed' => 'The linked review-pack export failed.',
|
|
default => 'No generated export is available.',
|
|
};
|
|
}
|
|
|
|
private function flowTone(string $state): string
|
|
{
|
|
return match ($state) {
|
|
'Available', 'Ready', 'Generated' => 'success',
|
|
'Generating' => 'info',
|
|
'Missing', 'Required', 'Stale', 'Needs review', 'Not ready' => 'warning',
|
|
'Failed' => 'danger',
|
|
default => 'gray',
|
|
};
|
|
}
|
|
|
|
private function snapshotIsStale(EvidenceSnapshot $snapshot): bool
|
|
{
|
|
if ((string) $snapshot->status === EvidenceSnapshotStatus::Expired->value) {
|
|
return true;
|
|
}
|
|
|
|
if ($snapshot->expires_at instanceof \DateTimeInterface && $snapshot->expires_at->isPast()) {
|
|
return true;
|
|
}
|
|
|
|
return $snapshot->completenessState() === EvidenceCompletenessState::Stale
|
|
|| (int) data_get($snapshot->summary ?? [], 'stale_dimensions', 0) > 0;
|
|
}
|
|
|
|
private function reviewPackHasExportArtifact(?ReviewPack $reviewPack): bool
|
|
{
|
|
if (! $reviewPack instanceof ReviewPack || ! $reviewPack->isReady()) {
|
|
return false;
|
|
}
|
|
|
|
if ($reviewPack->expires_at instanceof \DateTimeInterface && $reviewPack->expires_at->isPast()) {
|
|
return false;
|
|
}
|
|
|
|
return filled($reviewPack->file_disk) && filled($reviewPack->file_path);
|
|
}
|
|
|
|
private function customerSafeOutputReady(?EnvironmentReview $review, ?ReviewPack $reviewPack): bool
|
|
{
|
|
if (! $review instanceof EnvironmentReview || ! $reviewPack instanceof ReviewPack) {
|
|
return false;
|
|
}
|
|
|
|
return (string) $review->status === EnvironmentReviewStatus::Published->value
|
|
&& (int) $review->current_export_review_pack_id === (int) $reviewPack->getKey()
|
|
&& $this->reviewPackHasExportArtifact($reviewPack);
|
|
}
|
|
|
|
private function canDownloadReviewPack(?ReviewPack $reviewPack, ?ManagedEnvironment $tenant): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
return $reviewPack instanceof ReviewPack
|
|
&& $tenant instanceof ManagedEnvironment
|
|
&& $user instanceof User
|
|
&& $this->reviewPackHasExportArtifact($reviewPack)
|
|
&& (int) $reviewPack->managed_environment_id === (int) $tenant->getKey()
|
|
&& $user->can(Capabilities::REVIEW_PACK_VIEW, $tenant);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function proofItem(
|
|
string $label,
|
|
string $state,
|
|
string $description,
|
|
string $color,
|
|
?string $url = null,
|
|
?string $actionLabel = null,
|
|
): array {
|
|
return [
|
|
'label' => $label,
|
|
'state' => $state,
|
|
'description' => $description,
|
|
'color' => $color,
|
|
'url' => $url,
|
|
'actionLabel' => $actionLabel,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $card
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function proofItemFromCard(array $card): array
|
|
{
|
|
return $this->proofItem(
|
|
(string) $card['label'],
|
|
(string) ($card['path_state'] ?? $card['value']),
|
|
(string) $card['description'],
|
|
(string) $card['color'],
|
|
is_string($card['url'] ?? null) ? $card['url'] : null,
|
|
is_string($card['url'] ?? null) ? 'Open proof' : null,
|
|
);
|
|
}
|
|
|
|
private function operationProofState(OperationRun $operationRun): string
|
|
{
|
|
if ((string) $operationRun->outcome === OperationRunOutcome::Failed->value) {
|
|
return 'Failed';
|
|
}
|
|
|
|
return in_array((string) $operationRun->status, [OperationRunStatus::Queued->value, OperationRunStatus::Running->value], true)
|
|
? 'Generating'
|
|
: 'Available';
|
|
}
|
|
|
|
private function operationProofTone(OperationRun $operationRun): string
|
|
{
|
|
return $this->flowTone($this->operationProofState($operationRun));
|
|
}
|
|
|
|
private function exportProofDescription(?ReviewPack $reviewPack, ?ManagedEnvironment $tenant): string
|
|
{
|
|
if (! $reviewPack instanceof ReviewPack) {
|
|
return 'No review pack is linked, so no export artifact is available.';
|
|
}
|
|
|
|
if (! $this->reviewPackHasExportArtifact($reviewPack)) {
|
|
return 'External delivery is not configured or the review pack file metadata is missing, expired, or not ready.';
|
|
}
|
|
|
|
return $this->canDownloadReviewPack($reviewPack, $tenant)
|
|
? 'Signed download is available for authorized users.'
|
|
: 'A file-backed export exists, but this user cannot download it from the current scope.';
|
|
}
|
|
|
|
private function decisionEvidenceSummary(
|
|
?EvidenceSnapshot $snapshot,
|
|
?ReviewPack $reviewPack,
|
|
?StoredReport $storedReport,
|
|
?EnvironmentReview $review,
|
|
?ManagedEnvironment $tenant,
|
|
?OperationRun $operationRun,
|
|
): string {
|
|
$parts = [];
|
|
$parts[] = $tenant instanceof ManagedEnvironment ? 'Environment scope selected' : 'No environment selected';
|
|
$parts[] = 'Snapshot: '.$this->snapshotFlowState($snapshot);
|
|
$parts[] = 'Stored report: '.$this->storedReportFlowState($snapshot, $storedReport);
|
|
$parts[] = 'Review pack: '.$this->reviewPackFlowState($snapshot, $storedReport, $reviewPack);
|
|
$parts[] = 'Customer-safe output: '.$this->customerSafeFlowState($snapshot, $reviewPack, $review);
|
|
$parts[] = 'Export: '.$this->exportFlowState($snapshot, $reviewPack, $tenant);
|
|
|
|
if ($operationRun instanceof OperationRun) {
|
|
$parts[] = OperationRunLinks::identifier($operationRun);
|
|
}
|
|
|
|
return implode(' · ', $parts);
|
|
}
|
|
|
|
/**
|
|
* @param array{label:string,url:string}|null $primaryAction
|
|
*/
|
|
private function decisionActionHelper(string $state, ?ManagedEnvironment $tenant, ?array $primaryAction): ?string
|
|
{
|
|
if ($primaryAction !== null) {
|
|
return null;
|
|
}
|
|
|
|
if (! $tenant instanceof ManagedEnvironment) {
|
|
return 'Use the Environment scope control in the top bar to choose an authorized environment.';
|
|
}
|
|
|
|
return match ($state) {
|
|
'no_snapshot' => 'Evidence generation requires evidence management capability.',
|
|
'pack_required' => 'Review pack generation requires review-pack management capability.',
|
|
'export_available' => 'Download requires review-pack view capability.',
|
|
default => 'No authorized repo-backed primary action is available for this state.',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @param array{label:string,url:string}|null $primaryAction
|
|
*/
|
|
private function decisionActionDescription(string $state, ?array $primaryAction): string
|
|
{
|
|
if ($primaryAction === null) {
|
|
return $state === 'source_unavailable'
|
|
? 'Choose an environment before evaluating evidence, reports, review packs, or export readiness.'
|
|
: 'No capability-backed action is available for the current state.';
|
|
}
|
|
|
|
return match ($state) {
|
|
'no_snapshot' => 'Creates the evidence snapshot required before reports or review packs can be trusted.',
|
|
'snapshot_generating', 'pack_generating' => 'Opens the linked operation so progress can be verified before using the output.',
|
|
'snapshot_failed', 'pack_failed' => 'Opens failed operation proof before retrying or sharing any output.',
|
|
'snapshot_stale' => 'Opens the stale snapshot so evidence can be refreshed from the correct scope.',
|
|
'report_missing' => 'Opens snapshot proof; report generation remains on the supported report surface.',
|
|
'pack_required' => 'Opens the environment review-pack surface where generation stays capability-gated.',
|
|
'customer_review_required' => 'Opens the customer review workspace before any external sharing decision.',
|
|
'customer_safe_ready' => 'Opens the customer review workspace that backs the readiness claim.',
|
|
'export_available' => 'Downloads the signed export for authorized users.',
|
|
'export_unavailable' => 'Opens review-pack proof to inspect missing, expired, or unauthorized export state.',
|
|
default => 'Opens the most relevant repo-backed proof surface for this state.',
|
|
};
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, EvidenceSnapshot>
|
|
*/
|
|
private function scopedSnapshots(): Collection
|
|
{
|
|
$snapshotIds = $this->rowsForState($this->tableFilters ?? [], $this->tableSearch)
|
|
->pluck('snapshot_id')
|
|
->map(static fn (mixed $snapshotId): int => (int) $snapshotId)
|
|
->all();
|
|
|
|
if ($snapshotIds === []) {
|
|
return collect();
|
|
}
|
|
|
|
return $this->latestAccessibleSnapshots()
|
|
->filter(static fn (EvidenceSnapshot $snapshot): bool => in_array((int) $snapshot->getKey(), $snapshotIds, true))
|
|
->values();
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, EvidenceSnapshot> $snapshots
|
|
*/
|
|
private function evidenceScopeDescription(Collection $snapshots): string
|
|
{
|
|
$tenant = $this->filteredTenant();
|
|
|
|
if ($tenant instanceof ManagedEnvironment) {
|
|
return 'Filtered to '.$tenant->name.'. Proof states below are derived from records directly attributed to this environment.';
|
|
}
|
|
|
|
if ($snapshots->isEmpty()) {
|
|
return 'Workspace-wide proof view. No accessible evidence snapshot currently matches the active search or filters.';
|
|
}
|
|
|
|
return sprintf(
|
|
'Workspace-wide proof view across %d accessible environment%s.',
|
|
$snapshots->count(),
|
|
$snapshots->count() === 1 ? '' : 's',
|
|
);
|
|
}
|
|
|
|
private function evidenceScopeLabel(): string
|
|
{
|
|
$tenant = $this->filteredTenant();
|
|
|
|
return $tenant instanceof ManagedEnvironment
|
|
? 'Environment proof scope'
|
|
: 'Workspace proof scope';
|
|
}
|
|
|
|
private function latestReviewPackForSnapshot(EvidenceSnapshot $snapshot): ?ReviewPack
|
|
{
|
|
$reviewPack = $snapshot->reviewPacks
|
|
->sortByDesc(fn (ReviewPack $pack): int => $pack->generated_at?->getTimestamp() ?? $pack->created_at?->getTimestamp() ?? 0)
|
|
->first();
|
|
|
|
return $reviewPack instanceof ReviewPack ? $reviewPack : null;
|
|
}
|
|
|
|
private function latestStoredReportForTenant(ManagedEnvironment $tenant): ?StoredReport
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return null;
|
|
}
|
|
|
|
$visibleReportTypes = collect(StoredReportResource::supportedReportTypes())
|
|
->filter(function (string $reportType) use ($tenant, $user): bool {
|
|
$capability = StoredReportResource::capabilityForReportType($reportType);
|
|
|
|
return is_string($capability) && $user->can($capability, $tenant);
|
|
})
|
|
->values()
|
|
->all();
|
|
|
|
if ($visibleReportTypes === []) {
|
|
return null;
|
|
}
|
|
|
|
return StoredReport::query()
|
|
->where('workspace_id', (int) $tenant->workspace_id)
|
|
->where('managed_environment_id', (int) $tenant->getKey())
|
|
->whereIn('report_type', $visibleReportTypes)
|
|
->latest('created_at')
|
|
->first();
|
|
}
|
|
|
|
private function primaryOperationRun(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack): ?OperationRun
|
|
{
|
|
$run = $reviewPack?->operationRun;
|
|
|
|
if (
|
|
$reviewPack instanceof ReviewPack
|
|
&& $run instanceof OperationRun
|
|
&& in_array((string) $reviewPack->status, [ReviewPackStatus::Queued->value, ReviewPackStatus::Generating->value, ReviewPackStatus::Failed->value], true)
|
|
&& $this->canViewOperationRun($run)
|
|
) {
|
|
return $run;
|
|
}
|
|
|
|
$run = $snapshot?->operationRun;
|
|
|
|
if ($run instanceof OperationRun && $this->canViewOperationRun($run)) {
|
|
return $run;
|
|
}
|
|
|
|
$run = $reviewPack?->operationRun;
|
|
|
|
return $run instanceof OperationRun && $this->canViewOperationRun($run)
|
|
? $run
|
|
: null;
|
|
}
|
|
|
|
private function canViewOperationRun(OperationRun $run): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
return $user instanceof User && Gate::forUser($user)->allows('view', $run);
|
|
}
|
|
|
|
/**
|
|
* @return array{label:string,url:string}|null
|
|
*/
|
|
private function primaryEvidenceAction(
|
|
string $state,
|
|
?EvidenceSnapshot $snapshot,
|
|
?ReviewPack $reviewPack,
|
|
?StoredReport $storedReport,
|
|
?EnvironmentReview $review,
|
|
?ManagedEnvironment $tenant,
|
|
?OperationRun $operationRun,
|
|
): ?array {
|
|
if (in_array($state, ['snapshot_generating', 'snapshot_failed', 'pack_generating', 'pack_failed'], true) && $operationRun instanceof OperationRun) {
|
|
return [
|
|
'label' => $state === 'snapshot_generating' || $state === 'pack_generating'
|
|
? 'View operation progress'
|
|
: 'Review operation',
|
|
'url' => OperationRunLinks::tenantlessView($operationRun),
|
|
];
|
|
}
|
|
|
|
if ($state === 'no_snapshot' && $tenant instanceof ManagedEnvironment && $this->canManageEvidence($tenant)) {
|
|
return [
|
|
'label' => 'Generate evidence snapshot',
|
|
'url' => EvidenceSnapshotResource::getUrl('index', tenant: $tenant, panel: 'admin'),
|
|
];
|
|
}
|
|
|
|
if ($state === 'snapshot_stale' && $snapshot instanceof EvidenceSnapshot && $snapshot->tenant instanceof ManagedEnvironment && $this->canManageEvidence($snapshot->tenant)) {
|
|
return [
|
|
'label' => 'Refresh evidence snapshot',
|
|
'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin'),
|
|
];
|
|
}
|
|
|
|
if ($state === 'pack_required' && $tenant instanceof ManagedEnvironment && $this->canManageReviewPacks($tenant)) {
|
|
return [
|
|
'label' => 'Generate review pack',
|
|
'url' => ReviewPackResource::getUrl('index', tenant: $tenant, panel: 'admin'),
|
|
];
|
|
}
|
|
|
|
if ($snapshot instanceof EvidenceSnapshot && $this->isEmptyEvidenceSnapshot($snapshot) && $snapshot->tenant instanceof ManagedEnvironment) {
|
|
return [
|
|
'label' => 'Open evidence snapshot',
|
|
'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin'),
|
|
];
|
|
}
|
|
|
|
if ($state === 'customer_review_required' && $tenant instanceof ManagedEnvironment) {
|
|
return [
|
|
'label' => 'Review customer output',
|
|
'url' => CustomerReviewWorkspace::environmentFilterUrl($tenant),
|
|
];
|
|
}
|
|
|
|
if ($state === 'export_available' && $this->canDownloadReviewPack($reviewPack, $tenant)) {
|
|
return [
|
|
'label' => 'Download export',
|
|
'url' => app(ReviewPackService::class)->generateDownloadUrl($reviewPack),
|
|
];
|
|
}
|
|
|
|
if ($state === 'customer_safe_ready' && $tenant instanceof ManagedEnvironment) {
|
|
return [
|
|
'label' => 'Open customer workspace',
|
|
'url' => CustomerReviewWorkspace::environmentFilterUrl($tenant),
|
|
];
|
|
}
|
|
|
|
if ($state === 'report_missing' && $snapshot instanceof EvidenceSnapshot && $snapshot->tenant instanceof ManagedEnvironment) {
|
|
return [
|
|
'label' => 'Open evidence snapshot',
|
|
'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin'),
|
|
];
|
|
}
|
|
|
|
if ($state === 'export_unavailable' && $reviewPack instanceof ReviewPack && $reviewPack->tenant instanceof ManagedEnvironment) {
|
|
return [
|
|
'label' => 'Open review pack',
|
|
'url' => ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $reviewPack->tenant, panel: 'admin'),
|
|
];
|
|
}
|
|
|
|
if ($review instanceof EnvironmentReview && $tenant instanceof ManagedEnvironment) {
|
|
return [
|
|
'label' => 'Open customer workspace',
|
|
'url' => CustomerReviewWorkspace::environmentFilterUrl($tenant),
|
|
];
|
|
}
|
|
|
|
if ($snapshot instanceof EvidenceSnapshot && $snapshot->tenant instanceof ManagedEnvironment) {
|
|
return [
|
|
'label' => 'Open evidence snapshot',
|
|
'url' => EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin'),
|
|
];
|
|
}
|
|
|
|
if ($reviewPack instanceof ReviewPack && $reviewPack->tenant instanceof ManagedEnvironment) {
|
|
return [
|
|
'label' => 'Open review pack',
|
|
'url' => ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $reviewPack->tenant, panel: 'admin'),
|
|
];
|
|
}
|
|
|
|
if ($storedReport instanceof StoredReport && $storedReport->tenant instanceof ManagedEnvironment) {
|
|
return [
|
|
'label' => 'Open stored report',
|
|
'url' => StoredReportResource::getUrl('view', ['record' => $storedReport], tenant: $storedReport->tenant, panel: 'admin'),
|
|
];
|
|
}
|
|
|
|
if ($operationRun instanceof OperationRun) {
|
|
return [
|
|
'label' => OperationRunLinks::openLabel(),
|
|
'url' => OperationRunLinks::tenantlessView($operationRun),
|
|
];
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
private function canManageEvidence(ManagedEnvironment $tenant): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
return $user instanceof User && $user->can(Capabilities::EVIDENCE_MANAGE, $tenant);
|
|
}
|
|
|
|
private function canManageReviewPacks(ManagedEnvironment $tenant): bool
|
|
{
|
|
$user = auth()->user();
|
|
|
|
return $user instanceof User && $user->can(Capabilities::REVIEW_PACK_MANAGE, $tenant);
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function snapshotProofCard(?EvidenceSnapshot $snapshot): array
|
|
{
|
|
if (! $snapshot instanceof EvidenceSnapshot) {
|
|
return $this->unavailableProofCard(
|
|
'Evidence snapshot',
|
|
'Not generated',
|
|
'No active evidence snapshot is available in this scope.',
|
|
'gray',
|
|
);
|
|
}
|
|
|
|
$outcome = $this->snapshotOutcome($snapshot);
|
|
$isEmptySnapshot = $this->isEmptyEvidenceSnapshot($snapshot);
|
|
|
|
return [
|
|
'label' => 'Evidence snapshot',
|
|
'value' => $isEmptySnapshot ? 'Proof incomplete' : $outcome->primaryLabel,
|
|
'path_state' => $isEmptySnapshot ? 'Empty' : $outcome->primaryLabel,
|
|
'description' => $isEmptySnapshot
|
|
? 'A proof record exists, but no usable captured evidence is available yet.'
|
|
: $this->productSafeEvidenceReason($outcome->primaryReason),
|
|
'color' => $outcome->primaryBadge->color,
|
|
'url' => $snapshot->tenant instanceof ManagedEnvironment
|
|
? EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant, panel: 'admin')
|
|
: null,
|
|
'meta' => $snapshot->generated_at?->diffForHumans() ?? 'Freshness unavailable',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function reviewPackProofCard(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): array
|
|
{
|
|
if (! $reviewPack instanceof ReviewPack) {
|
|
return $this->unavailableProofCard(
|
|
'Review pack',
|
|
$snapshot instanceof EvidenceSnapshot ? 'Not generated' : 'Not applicable',
|
|
$snapshot instanceof EvidenceSnapshot
|
|
? 'No review pack has been generated from the current evidence snapshot.'
|
|
: 'A review pack requires an evidence snapshot first.',
|
|
'gray',
|
|
);
|
|
}
|
|
|
|
return [
|
|
'label' => 'Review pack',
|
|
'value' => BadgeRenderer::label(BadgeDomain::ReviewPackStatus)((string) $reviewPack->status),
|
|
'description' => $reviewPack->isReady()
|
|
? 'Customer-review artifact exists for this evidence path.'
|
|
: 'Review pack exists but is not ready for sharing.',
|
|
'color' => BadgeRenderer::color(BadgeDomain::ReviewPackStatus)((string) $reviewPack->status),
|
|
'url' => $reviewPack->tenant instanceof ManagedEnvironment
|
|
? ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $reviewPack->tenant, panel: 'admin')
|
|
: null,
|
|
'meta' => $reviewPack->generated_at?->diffForHumans() ?? 'Generated time unavailable',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function storedReportProofCard(?StoredReport $storedReport, ?ManagedEnvironment $tenant): array
|
|
{
|
|
if (! $tenant instanceof ManagedEnvironment) {
|
|
return $this->unavailableProofCard(
|
|
'Stored report / export',
|
|
'Not applicable',
|
|
'Stored report availability is evaluated after an evidence scope exists.',
|
|
'gray',
|
|
);
|
|
}
|
|
|
|
if (! $storedReport instanceof StoredReport) {
|
|
return $this->unavailableProofCard(
|
|
'Stored report / export',
|
|
'Unavailable',
|
|
'No repo-supported stored report is available for this environment scope.',
|
|
'gray',
|
|
);
|
|
}
|
|
|
|
return [
|
|
'label' => 'Stored report / export',
|
|
'value' => 'Available',
|
|
'description' => StoredReportResource::reportFamilyReportLabel((string) $storedReport->report_type),
|
|
'color' => 'success',
|
|
'url' => StoredReportResource::getUrl('view', ['record' => $storedReport], tenant: $tenant, panel: 'admin'),
|
|
'meta' => $storedReport->created_at?->diffForHumans() ?? 'Report time unavailable',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function operationProofCard(?OperationRun $operationRun): array
|
|
{
|
|
if (! $operationRun instanceof OperationRun) {
|
|
return $this->unavailableProofCard(
|
|
'Operation proof',
|
|
'Unavailable',
|
|
'No authorized operation run is linked to the current proof path.',
|
|
'gray',
|
|
);
|
|
}
|
|
|
|
return [
|
|
'label' => 'Operation proof',
|
|
'value' => 'Available',
|
|
'description' => OperationRunLinks::identifier($operationRun),
|
|
'color' => 'info',
|
|
'url' => OperationRunLinks::tenantlessView($operationRun),
|
|
'meta' => $operationRun->completed_at?->diffForHumans() ?? $operationRun->created_at?->diffForHumans() ?? 'Run time unavailable',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{label:string,value:string,description:string,color:string,url:null,meta:string}
|
|
*/
|
|
private function unavailableProofCard(string $label, string $value, string $description, string $color): array
|
|
{
|
|
return [
|
|
'label' => $label,
|
|
'value' => $value,
|
|
'description' => $description,
|
|
'color' => $color,
|
|
'url' => null,
|
|
'meta' => 'Derived from current repo truth',
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function snapshotPathItem(?EvidenceSnapshot $snapshot): array
|
|
{
|
|
return $this->pathItemFromCard($this->snapshotProofCard($snapshot));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function reviewPackPathItem(?ReviewPack $reviewPack, ?EvidenceSnapshot $snapshot): array
|
|
{
|
|
return $this->pathItemFromCard($this->reviewPackProofCard($reviewPack, $snapshot));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function storedReportPathItem(?StoredReport $storedReport, ?ManagedEnvironment $tenant): array
|
|
{
|
|
return $this->pathItemFromCard($this->storedReportProofCard($storedReport, $tenant));
|
|
}
|
|
|
|
/**
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function operationPathItem(?OperationRun $operationRun): array
|
|
{
|
|
return $this->pathItemFromCard($this->operationProofCard($operationRun));
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $card
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function pathItemFromCard(array $card): array
|
|
{
|
|
return [
|
|
'label' => (string) $card['label'],
|
|
'state' => (string) ($card['path_state'] ?? $card['value']),
|
|
'description' => (string) $card['description'],
|
|
'color' => (string) $card['color'],
|
|
'url' => is_string($card['url'] ?? null) ? $card['url'] : null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @return array{label:string,color:string,reason:string,impact:string}|null
|
|
*/
|
|
private function primaryProofState(
|
|
?EvidenceSnapshot $snapshot,
|
|
?ReviewPack $reviewPack,
|
|
?StoredReport $storedReport,
|
|
?OperationRun $operationRun,
|
|
): ?array {
|
|
if (! $snapshot instanceof EvidenceSnapshot || ! $this->isEmptyEvidenceSnapshot($snapshot)) {
|
|
return null;
|
|
}
|
|
|
|
return [
|
|
'label' => 'Proof incomplete',
|
|
'color' => 'warning',
|
|
'reason' => 'Primary evidence snapshot is empty.',
|
|
'impact' => $this->emptySnapshotImpact($reviewPack, $storedReport, $operationRun),
|
|
];
|
|
}
|
|
|
|
private function emptySnapshotImpact(?ReviewPack $reviewPack, ?StoredReport $storedReport, ?OperationRun $operationRun): string
|
|
{
|
|
if ($reviewPack instanceof ReviewPack && $storedReport instanceof StoredReport && $operationRun instanceof OperationRun) {
|
|
return 'Supporting proof exists through the review pack, stored report, and operation record.';
|
|
}
|
|
|
|
return 'Supporting proof is limited; use the available evidence path items before relying on this snapshot.';
|
|
}
|
|
|
|
private function isEmptyEvidenceSnapshot(EvidenceSnapshot $snapshot): bool
|
|
{
|
|
return $this->snapshotTruth($snapshot)->contentState === 'empty';
|
|
}
|
|
|
|
private function productSafeEvidenceReason(string $reason): string
|
|
{
|
|
return $reason === 'The artifact row exists, but it does not contain usable captured content.'
|
|
? 'A proof record exists, but no usable captured evidence is available yet.'
|
|
: $reason;
|
|
}
|
|
|
|
private function snapshotTruth(EvidenceSnapshot $snapshot, bool $fresh = false): ArtifactTruthEnvelope
|
|
{
|
|
$presenter = app(ArtifactTruthPresenter::class);
|
|
|
|
return $fresh
|
|
? $presenter->forEvidenceSnapshotFresh($snapshot)
|
|
: $presenter->forEvidenceSnapshot($snapshot);
|
|
}
|
|
|
|
private function snapshotOutcome(EvidenceSnapshot $snapshot, bool $fresh = false): CompressedGovernanceOutcome
|
|
{
|
|
$presenter = app(ArtifactTruthPresenter::class);
|
|
|
|
return $presenter->compressedOutcomeFor($snapshot, SurfaceCompressionContext::evidenceOverview(), $fresh)
|
|
?? $presenter->compressedOutcomeFromEnvelope(
|
|
$this->snapshotTruth($snapshot, $fresh),
|
|
SurfaceCompressionContext::evidenceOverview(),
|
|
);
|
|
}
|
|
|
|
private function authorizeWorkspaceAccess(): void
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
throw new AuthenticationException;
|
|
}
|
|
|
|
app(WorkspaceContext::class)->currentWorkspaceForMemberOrFail($user, request());
|
|
}
|
|
|
|
/**
|
|
* @return array<int, ManagedEnvironment>
|
|
*/
|
|
private function accessibleTenants(): array
|
|
{
|
|
if (is_array($this->accessibleTenants)) {
|
|
return $this->accessibleTenants;
|
|
}
|
|
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
return $this->accessibleTenants = [];
|
|
}
|
|
|
|
$workspaceId = $this->workspaceId();
|
|
|
|
return $this->accessibleTenants = $user->accessibleManagedEnvironmentsQuery($workspaceId)
|
|
->orderBy('managed_environments.name')
|
|
->get()
|
|
->filter(fn (ManagedEnvironment $tenant): bool => (int) $tenant->workspace_id === $workspaceId && $user->can('evidence.view', $tenant))
|
|
->values()
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @return array<string, string>
|
|
*/
|
|
private function tenantFilterOptions(): array
|
|
{
|
|
return collect($this->accessibleTenants())
|
|
->mapWithKeys(static fn (ManagedEnvironment $tenant): array => [
|
|
(string) $tenant->getKey() => $tenant->name,
|
|
])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $filters
|
|
* @return Collection<string, array<string, mixed>>
|
|
*/
|
|
private function rowsForState(array $filters = [], ?string $search = null): Collection
|
|
{
|
|
$rows = $this->baseRows();
|
|
$tenantFilter = $this->normalizeTenantFilter($filters['managed_environment_id']['value'] ?? data_get($this->tableFilters, 'managed_environment_id.value'));
|
|
$normalizedSearch = Str::lower(trim((string) ($search ?? $this->tableSearch)));
|
|
|
|
if ($tenantFilter !== null) {
|
|
$rows = $rows->where('managed_environment_id', $tenantFilter);
|
|
}
|
|
|
|
if ($normalizedSearch === '') {
|
|
return $rows;
|
|
}
|
|
|
|
return $rows->filter(function (array $row) use ($normalizedSearch): bool {
|
|
$haystack = implode(' ', [
|
|
(string) ($row['tenant_name'] ?? ''),
|
|
(string) ($row['artifact_truth_label'] ?? ''),
|
|
(string) ($row['artifact_truth_explanation'] ?? ''),
|
|
(string) ($row['freshness_label'] ?? ''),
|
|
(string) ($row['next_step'] ?? ''),
|
|
]);
|
|
|
|
return str_contains(Str::lower($haystack), $normalizedSearch);
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return Collection<string, array<string, mixed>>
|
|
*/
|
|
private function baseRows(): Collection
|
|
{
|
|
$snapshots = $this->latestAccessibleSnapshots();
|
|
$currentReviewTenantIds = $this->currentReviewTenantIds($snapshots);
|
|
|
|
return $snapshots->mapWithKeys(function (EvidenceSnapshot $snapshot) use ($currentReviewTenantIds): array {
|
|
return [(string) $snapshot->getKey() => $this->rowForSnapshot($snapshot, $currentReviewTenantIds)];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* @return Collection<int, EvidenceSnapshot>
|
|
*/
|
|
private function latestAccessibleSnapshots(): Collection
|
|
{
|
|
if ($this->cachedSnapshots instanceof Collection) {
|
|
return $this->cachedSnapshots;
|
|
}
|
|
|
|
$tenantIds = collect($this->accessibleTenants())
|
|
->map(static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey())
|
|
->all();
|
|
|
|
$query = EvidenceSnapshot::query()
|
|
->with([
|
|
'tenant',
|
|
'operationRun',
|
|
'reviewPacks.operationRun',
|
|
'reviewPacks.environmentReview.currentExportReviewPack',
|
|
'items',
|
|
])
|
|
->where('workspace_id', $this->workspaceId())
|
|
->where('status', 'active')
|
|
->latest('generated_at');
|
|
|
|
if ($tenantIds === []) {
|
|
$query->whereRaw('1 = 0');
|
|
} else {
|
|
$query->whereIn('managed_environment_id', $tenantIds);
|
|
}
|
|
|
|
return $this->cachedSnapshots = $query->get()->unique('managed_environment_id')->values();
|
|
}
|
|
|
|
/**
|
|
* @param Collection<int, EvidenceSnapshot> $snapshots
|
|
* @return array<int, bool>
|
|
*/
|
|
private function currentReviewTenantIds(Collection $snapshots): array
|
|
{
|
|
return EnvironmentReview::query()
|
|
->where('workspace_id', $this->workspaceId())
|
|
->whereIn('managed_environment_id', $snapshots->pluck('managed_environment_id')->map(static fn (mixed $tenantId): int => (int) $tenantId)->all())
|
|
->whereIn('status', [
|
|
EnvironmentReviewStatus::Draft->value,
|
|
EnvironmentReviewStatus::Ready->value,
|
|
EnvironmentReviewStatus::Published->value,
|
|
])
|
|
->pluck('managed_environment_id')
|
|
->mapWithKeys(static fn (mixed $tenantId): array => [(int) $tenantId => true])
|
|
->all();
|
|
}
|
|
|
|
/**
|
|
* @param array<int, bool> $currentReviewTenantIds
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function rowForSnapshot(EvidenceSnapshot $snapshot, array $currentReviewTenantIds): array
|
|
{
|
|
$truth = $this->snapshotTruth($snapshot);
|
|
$outcome = $this->snapshotOutcome($snapshot);
|
|
$tenantId = (int) $snapshot->managed_environment_id;
|
|
$hasCurrentReview = $currentReviewTenantIds[$tenantId] ?? false;
|
|
$nextStep = ! $hasCurrentReview && $truth->contentState === 'trusted' && $truth->freshnessState === 'current'
|
|
? 'Create a current review from this evidence snapshot'
|
|
: $outcome->nextActionText;
|
|
|
|
return [
|
|
'tenant_name' => $snapshot->tenant?->name ?? 'Unknown tenant',
|
|
'managed_environment_id' => $tenantId,
|
|
'snapshot_id' => (int) $snapshot->getKey(),
|
|
'generated_at' => $snapshot->generated_at?->toDateTimeString(),
|
|
'artifact_truth_label' => $truth->contentState === 'empty' ? 'Proof incomplete' : $outcome->primaryLabel,
|
|
'artifact_truth_color' => $outcome->primaryBadge->color,
|
|
'artifact_truth_icon' => $outcome->primaryBadge->icon,
|
|
'artifact_truth_explanation' => $this->productSafeEvidenceReason($outcome->primaryReason),
|
|
'artifact_truth' => [
|
|
'label' => $truth->contentState === 'empty' ? 'Proof incomplete' : $outcome->primaryLabel,
|
|
'color' => $outcome->primaryBadge->color,
|
|
'icon' => $outcome->primaryBadge->icon,
|
|
'explanation' => $this->productSafeEvidenceReason($outcome->primaryReason),
|
|
],
|
|
'next_step' => $nextStep,
|
|
'view_url' => $snapshot->tenant
|
|
? EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $snapshot->tenant)
|
|
: null,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param Collection<string, array<string, mixed>> $rows
|
|
* @return Collection<string, array<string, mixed>>
|
|
*/
|
|
private function sortRows(Collection $rows, ?string $sortColumn, ?string $sortDirection): Collection
|
|
{
|
|
$sortColumn = in_array($sortColumn, ['tenant_name', 'artifact_truth_label', 'generated_at'], true)
|
|
? $sortColumn
|
|
: 'tenant_name';
|
|
$descending = Str::lower((string) ($sortDirection ?? 'asc')) === 'desc';
|
|
|
|
$records = $rows->all();
|
|
|
|
uasort($records, static function (array $left, array $right) use ($sortColumn, $descending): int {
|
|
$comparison = strnatcasecmp((string) ($left[$sortColumn] ?? ''), (string) ($right[$sortColumn] ?? ''));
|
|
|
|
if ($comparison === 0) {
|
|
$comparison = strnatcasecmp((string) ($left['tenant_name'] ?? ''), (string) ($right['tenant_name'] ?? ''));
|
|
}
|
|
|
|
return $descending ? ($comparison * -1) : $comparison;
|
|
});
|
|
|
|
return collect($records);
|
|
}
|
|
|
|
/**
|
|
* @param Collection<string, array<string, mixed>> $rows
|
|
*/
|
|
private function paginateRows(Collection $rows, int $page, int $recordsPerPage): LengthAwarePaginator
|
|
{
|
|
return new LengthAwarePaginator(
|
|
items: $rows->forPage($page, $recordsPerPage),
|
|
total: $rows->count(),
|
|
perPage: $recordsPerPage,
|
|
currentPage: $page,
|
|
);
|
|
}
|
|
|
|
private function seedTableStateFromQuery(): void
|
|
{
|
|
$query = request()->query();
|
|
|
|
if (array_key_exists('search', $query)) {
|
|
$this->tableSearch = trim((string) request()->query('search', ''));
|
|
}
|
|
|
|
if (! array_key_exists('environment_id', $query)) {
|
|
return;
|
|
}
|
|
|
|
$workspace = $this->workspace();
|
|
|
|
if (! $workspace instanceof Workspace) {
|
|
return;
|
|
}
|
|
|
|
$filter = WorkspaceHubEnvironmentFilter::fromRequest(request(), $workspace);
|
|
|
|
if (! $filter instanceof WorkspaceHubEnvironmentFilter) {
|
|
return;
|
|
}
|
|
|
|
$tenantFilter = $this->normalizeTenantFilter($filter->environmentId());
|
|
|
|
if ($tenantFilter === null) {
|
|
throw new NotFoundHttpException;
|
|
}
|
|
|
|
$this->tableFilters = [
|
|
'managed_environment_id' => ['value' => (string) $tenantFilter],
|
|
];
|
|
$this->tableDeferredFilters = $this->tableFilters;
|
|
}
|
|
|
|
private function normalizeTenantFilter(mixed $value): ?int
|
|
{
|
|
if (! is_numeric($value)) {
|
|
return null;
|
|
}
|
|
|
|
$requestedTenantId = (int) $value;
|
|
$allowedTenantIds = collect($this->accessibleTenants())
|
|
->map(static fn (ManagedEnvironment $tenant): int => (int) $tenant->getKey())
|
|
->all();
|
|
|
|
return in_array($requestedTenantId, $allowedTenantIds, true)
|
|
? $requestedTenantId
|
|
: null;
|
|
}
|
|
|
|
private function hasActiveOverviewFilters(): bool
|
|
{
|
|
return filled(data_get($this->tableFilters, 'managed_environment_id.value'))
|
|
|| trim((string) $this->tableSearch) !== '';
|
|
}
|
|
|
|
private function overviewUrl(array $overrides = []): string
|
|
{
|
|
return route(
|
|
'admin.evidence.overview',
|
|
array_filter($overrides, static fn (mixed $value): bool => $value !== null && $value !== '' && $value !== []),
|
|
);
|
|
}
|
|
|
|
private function workspaceId(): int
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
throw new AuthenticationException;
|
|
}
|
|
|
|
return (int) app(WorkspaceContext::class)
|
|
->currentWorkspaceForMemberOrFail($user, request())
|
|
->getKey();
|
|
}
|
|
|
|
private function workspace(): ?Workspace
|
|
{
|
|
$user = auth()->user();
|
|
|
|
if (! $user instanceof User) {
|
|
throw new AuthenticationException;
|
|
}
|
|
|
|
return app(WorkspaceContext::class)->currentWorkspaceForMemberOrFail($user, request());
|
|
}
|
|
|
|
private function filteredTenant(): ?ManagedEnvironment
|
|
{
|
|
$tenantId = $this->normalizeTenantFilter(data_get($this->tableFilters, 'managed_environment_id.value'));
|
|
|
|
if (! is_int($tenantId)) {
|
|
return null;
|
|
}
|
|
|
|
foreach ($this->accessibleTenants() as $tenant) {
|
|
if ((int) $tenant->getKey() === $tenantId) {
|
|
return $tenant;
|
|
}
|
|
}
|
|
|
|
return null;
|
|
}
|
|
}
|