Spec 329: productize evidence and audit log disclosure (#390)
## Summary - productize the Monitoring audit log disclosure flow with richer detail inspection and updated disclosure UI - expand the evidence overview disclosure experience, including filtering and presentation updates - wire the monitoring pages into the Filament admin panel and workspace sidebar navigation - add English and German disclosure copy for the new audit and evidence surfaces - include Spec 329 implementation artifacts and supporting presenter/route updates ## Tests - added/updated monitoring acceptance and feature coverage for the disclosure flow - touched tests include `Spec329EvidenceAuditDisclosureSmokeTest`, `Spec329EvidenceAuditDisclosureProductizationTest`, `AuditLogPageTest`, `AuditLogDetailInspectionTest`, `AuditLogInspectFlowTest`, and related monitoring/navigation coverage - no additional test run was performed as part of this commit/push/PR workflow Co-authored-by: Ahmed Darrazi <ahmed.darrazi@live.de> Reviewed-on: #390
@ -7,6 +7,7 @@
|
||||
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
||||
use App\Models\AuditLog as AuditLogModel;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\SupportAccessGrant;
|
||||
use App\Models\User;
|
||||
use App\Models\Workspace;
|
||||
@ -22,6 +23,7 @@
|
||||
use App\Support\Navigation\CanonicalNavigationContext;
|
||||
use App\Support\Navigation\RelatedNavigationResolver;
|
||||
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\OperateHub\OperateHubShell;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDefaults;
|
||||
@ -40,6 +42,7 @@
|
||||
use Filament\Tables\Filters\SelectFilter;
|
||||
use Filament\Tables\Table;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Facades\Gate;
|
||||
use Illuminate\Support\Str;
|
||||
use Symfony\Component\HttpFoundation\StreamedResponse;
|
||||
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
|
||||
@ -388,6 +391,174 @@ public function environmentFilterChip(): ?array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function auditDisclosurePayload(): array
|
||||
{
|
||||
$selectedAudit = $this->selectedAuditRecord();
|
||||
$focusAudit = $selectedAudit instanceof AuditLogModel
|
||||
? $selectedAudit
|
||||
: $this->auditBaseQuery()->first();
|
||||
|
||||
return [
|
||||
'scope_label' => $this->auditScopeLabel(),
|
||||
'scope_description' => $this->auditScopeDescription(),
|
||||
'event_count' => (clone $this->auditBaseQuery())->reorder()->count(),
|
||||
'focus' => $focusAudit instanceof AuditLogModel ? $this->auditProofPayload($focusAudit) : null,
|
||||
'has_selected_event' => $selectedAudit instanceof AuditLogModel,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>|null
|
||||
*/
|
||||
public function selectedAuditProofPayload(): ?array
|
||||
{
|
||||
$record = $this->selectedAuditRecord();
|
||||
|
||||
return $record instanceof AuditLogModel
|
||||
? $this->auditProofPayload($record)
|
||||
: null;
|
||||
}
|
||||
|
||||
private function auditScopeLabel(): string
|
||||
{
|
||||
return $this->filteredTenant() instanceof ManagedEnvironment
|
||||
? 'Environment audit scope'
|
||||
: 'Workspace audit scope';
|
||||
}
|
||||
|
||||
private function auditScopeDescription(): string
|
||||
{
|
||||
$tenant = $this->filteredTenant();
|
||||
|
||||
if ($tenant instanceof ManagedEnvironment) {
|
||||
return 'Filtered by direct environment attribution for '.$tenant->name.'. Selected event detail remains inside this filter.';
|
||||
}
|
||||
|
||||
return 'Workspace-wide audit disclosure across authorized environment and workspace events. Clean entry ignores remembered environment context.';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
private function auditProofPayload(AuditLogModel $record): array
|
||||
{
|
||||
$outcome = $record->normalizedOutcome()->value;
|
||||
|
||||
return [
|
||||
'id' => (int) $record->getKey(),
|
||||
'summary' => $record->summaryText(),
|
||||
'actor' => [
|
||||
'label' => $record->actorDisplayLabel(),
|
||||
'type' => $record->actorSnapshot()->type->label(),
|
||||
'email' => $record->actorSnapshot()->email,
|
||||
],
|
||||
'action' => AuditActionId::labelFor((string) $record->action),
|
||||
'target' => [
|
||||
'label' => $record->targetDisplayLabel() ?? 'No target snapshot',
|
||||
'type' => $record->resource_type ? Str::headline((string) $record->resource_type) : 'Workspace event',
|
||||
],
|
||||
'outcome' => [
|
||||
'label' => BadgeRenderer::label(BadgeDomain::AuditOutcome)($outcome),
|
||||
'color' => BadgeRenderer::color(BadgeDomain::AuditOutcome)($outcome),
|
||||
],
|
||||
'time' => $record->recorded_at?->toDayDateTimeString() ?? 'Time unavailable',
|
||||
'scope' => [
|
||||
'label' => $record->tenant?->name ?? 'Workspace-wide event',
|
||||
'detail' => is_numeric($record->workspace_id)
|
||||
? 'Workspace #'.(int) $record->workspace_id
|
||||
: 'Workspace unavailable',
|
||||
],
|
||||
'inspect_url' => $this->auditLogUrl(['event' => (int) $record->getKey()]),
|
||||
'related_links' => $this->auditProofLinks($record),
|
||||
'context_items' => $this->auditReadableContextItems($record),
|
||||
'technical_metadata' => $this->auditTechnicalMetadata($record),
|
||||
'diagnostics_available' => $this->canViewAuditDiagnostics($record),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{label:string,url:string}>
|
||||
*/
|
||||
private function auditProofLinks(AuditLogModel $record): array
|
||||
{
|
||||
$links = [];
|
||||
$operationRun = $record->operationRun;
|
||||
|
||||
if ($operationRun instanceof OperationRun && $this->canViewOperationRun($operationRun)) {
|
||||
$links[] = [
|
||||
'label' => 'Operation proof',
|
||||
'url' => OperationRunLinks::tenantlessView($operationRun),
|
||||
];
|
||||
}
|
||||
|
||||
$targetLink = $this->auditTargetLink($record);
|
||||
|
||||
if (is_array($targetLink)) {
|
||||
$links[] = [
|
||||
'label' => $targetLink['label'],
|
||||
'url' => $targetLink['url'],
|
||||
];
|
||||
}
|
||||
|
||||
return array_values(array_unique($links, SORT_REGULAR));
|
||||
}
|
||||
|
||||
private function canViewOperationRun(OperationRun $run): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
return $user instanceof User && Gate::forUser($user)->allows('view', $run);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{label:string,value:string|int|float|bool}>
|
||||
*/
|
||||
private function auditReadableContextItems(AuditLogModel $record): array
|
||||
{
|
||||
return collect($record->contextItems())
|
||||
->reject(fn (array $item): bool => $this->looksLikeRawDiagnosticValue($item['label'].' '.$item['value']))
|
||||
->values()
|
||||
->all();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string|int|null>
|
||||
*/
|
||||
private function auditTechnicalMetadata(AuditLogModel $record): array
|
||||
{
|
||||
if (! $this->canViewAuditDiagnostics($record)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
return $record->technicalMetadata();
|
||||
}
|
||||
|
||||
private function canViewAuditDiagnostics(AuditLogModel $record): bool
|
||||
{
|
||||
$tenant = $record->tenant;
|
||||
$user = auth()->user();
|
||||
|
||||
return $tenant instanceof ManagedEnvironment
|
||||
&& $user instanceof User
|
||||
&& $user->can(Capabilities::SUPPORT_DIAGNOSTICS_VIEW, $tenant);
|
||||
}
|
||||
|
||||
private function looksLikeRawDiagnosticValue(string $text): bool
|
||||
{
|
||||
$normalized = Str::lower($text);
|
||||
|
||||
foreach (['raw', 'payload', 'provider secret', 'provider response', 'stack trace', 'debug', 'internal exception', 'token', 'credential'] as $needle) {
|
||||
if (str_contains($normalized, $needle)) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private function authorizePageAccess(): void
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
@ -6,14 +6,22 @@
|
||||
|
||||
use App\Filament\Concerns\ClearsWorkspaceHubEnvironmentFilterState;
|
||||
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\Support\Badges\BadgeDomain;
|
||||
use App\Support\Badges\BadgeRenderer;
|
||||
use App\Support\EnvironmentReviewStatus;
|
||||
use App\Support\Filament\TablePaginationProfiles;
|
||||
use App\Support\Navigation\WorkspaceHubEnvironmentFilter;
|
||||
use App\Support\OperationRunLinks;
|
||||
use App\Support\Ui\ActionSurface\ActionSurfaceDeclaration;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceInspectAffordance;
|
||||
use App\Support\Ui\ActionSurface\Enums\ActionSurfaceProfile;
|
||||
@ -35,6 +43,7 @@
|
||||
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;
|
||||
@ -171,7 +180,7 @@ public function table(Table $table): Table
|
||||
->persistSearchInSession()
|
||||
->persistSortInSession()
|
||||
->searchable()
|
||||
->searchPlaceholder('Search tenant or next step')
|
||||
->searchPlaceholder('Search evidence or next step')
|
||||
->records(function (
|
||||
?string $sortColumn,
|
||||
?string $sortDirection,
|
||||
@ -275,6 +284,420 @@ public function environmentFilterChip(): ?array
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, mixed>
|
||||
*/
|
||||
public function evidenceDisclosurePayload(): array
|
||||
{
|
||||
$snapshots = $this->scopedSnapshots();
|
||||
$primarySnapshot = $snapshots->first();
|
||||
$primaryTenant = $primarySnapshot?->tenant;
|
||||
$primaryReviewPack = $primarySnapshot instanceof EvidenceSnapshot
|
||||
? $this->latestReviewPackForSnapshot($primarySnapshot)
|
||||
: null;
|
||||
$primaryStoredReport = $primaryTenant instanceof ManagedEnvironment
|
||||
? $this->latestStoredReportForTenant($primaryTenant)
|
||||
: null;
|
||||
$primaryOperationRun = $this->primaryOperationRun($primarySnapshot, $primaryReviewPack);
|
||||
$primaryAction = $this->primaryEvidenceAction($primarySnapshot, $primaryReviewPack, $primaryStoredReport, $primaryOperationRun);
|
||||
$primaryState = $this->primaryProofState($primarySnapshot, $primaryReviewPack, $primaryStoredReport, $primaryOperationRun);
|
||||
|
||||
return [
|
||||
'scope_label' => $this->evidenceScopeLabel(),
|
||||
'scope_description' => $this->evidenceScopeDescription($snapshots),
|
||||
'snapshot_count' => $snapshots->count(),
|
||||
'primary_title' => $primaryTenant instanceof ManagedEnvironment
|
||||
? $primaryTenant->name
|
||||
: 'No evidence for this scope',
|
||||
'primary_summary' => $primarySnapshot instanceof EvidenceSnapshot
|
||||
? $this->productSafeEvidenceReason($this->snapshotOutcome($primarySnapshot)->primaryReason)
|
||||
: 'No evidence snapshot has been generated for the active workspace scope.',
|
||||
'primary_state' => $primaryState,
|
||||
'primary_action' => $primaryAction,
|
||||
'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),
|
||||
],
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @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() ?? 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 = $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(?EvidenceSnapshot $snapshot, ?ReviewPack $reviewPack, ?StoredReport $storedReport, ?OperationRun $operationRun): ?array
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* @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);
|
||||
@ -401,7 +824,12 @@ private function latestAccessibleSnapshots(): Collection
|
||||
->all();
|
||||
|
||||
$query = EvidenceSnapshot::query()
|
||||
->with('tenant')
|
||||
->with([
|
||||
'tenant',
|
||||
'operationRun',
|
||||
'reviewPacks.operationRun',
|
||||
'items',
|
||||
])
|
||||
->where('workspace_id', $this->workspaceId())
|
||||
->where('status', 'active')
|
||||
->latest('generated_at');
|
||||
@ -453,15 +881,15 @@ private function rowForSnapshot(EvidenceSnapshot $snapshot, array $currentReview
|
||||
'managed_environment_id' => $tenantId,
|
||||
'snapshot_id' => (int) $snapshot->getKey(),
|
||||
'generated_at' => $snapshot->generated_at?->toDateTimeString(),
|
||||
'artifact_truth_label' => $outcome->primaryLabel,
|
||||
'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' => $outcome->primaryReason,
|
||||
'artifact_truth_explanation' => $this->productSafeEvidenceReason($outcome->primaryReason),
|
||||
'artifact_truth' => [
|
||||
'label' => $outcome->primaryLabel,
|
||||
'label' => $truth->contentState === 'empty' ? 'Proof incomplete' : $outcome->primaryLabel,
|
||||
'color' => $outcome->primaryBadge->color,
|
||||
'icon' => $outcome->primaryBadge->icon,
|
||||
'explanation' => $outcome->primaryReason,
|
||||
'explanation' => $this->productSafeEvidenceReason($outcome->primaryReason),
|
||||
],
|
||||
'next_step' => $nextStep,
|
||||
'view_url' => $snapshot->tenant
|
||||
|
||||
@ -173,6 +173,11 @@ public function panel(Panel $panel): Panel
|
||||
->icon('heroicon-o-bell-alert')
|
||||
->group(fn (): string => __('localization.navigation.monitoring'))
|
||||
->sort(23),
|
||||
NavigationItem::make(fn (): string => __('localization.navigation.evidence_overview'))
|
||||
->url(fn (): string => WorkspaceHubRegistry::cleanUrl(route('admin.evidence.overview')))
|
||||
->icon('heroicon-o-shield-check')
|
||||
->group(fn (): string => __('localization.navigation.monitoring'))
|
||||
->sort(27),
|
||||
NavigationItem::make(fn (): string => __('localization.navigation.audit_log'))
|
||||
->url(fn (): string => WorkspaceHubRegistry::cleanUrl(route('admin.monitoring.audit-log')))
|
||||
->icon('heroicon-o-clipboard-document-list')
|
||||
|
||||
@ -64,6 +64,9 @@ public function build(): NavigationBuilder
|
||||
->icon(AlertDeliveryResource::getNavigationIcon())
|
||||
->visible(fn (): bool => AlertDeliveryResource::canViewAny()),
|
||||
])),
|
||||
NavigationItem::make(__('localization.navigation.evidence_overview'))
|
||||
->url(fn (): string => $this->workspaceHubUrl(route('admin.evidence.overview')))
|
||||
->icon('heroicon-o-shield-check'),
|
||||
NavigationItem::make(__('localization.navigation.audit_log'))
|
||||
->url(fn (): string => $this->workspaceHubUrl(route('admin.monitoring.audit-log')))
|
||||
->icon('heroicon-o-clipboard-document-list'),
|
||||
|
||||
@ -1171,7 +1171,7 @@ private function contentExplanation(string $contentState): string
|
||||
'missing_input' => 'The artifact is blocked by missing upstream inputs or failed capture prerequisites.',
|
||||
'metadata_only' => 'Only metadata was captured for this artifact. Use diagnostics for context, not as the primary truth signal.',
|
||||
'reference_only' => 'Only reference-level placeholders were captured for this artifact.',
|
||||
'empty' => 'The artifact row exists, but it does not contain usable captured content.',
|
||||
'empty' => 'A proof record exists, but no usable captured evidence is available yet.',
|
||||
'unsupported' => 'Structured support is limited for this artifact family, so the current rendering should be treated as diagnostic only.',
|
||||
default => 'The artifact content is available for the intended workflow.',
|
||||
};
|
||||
|
||||
@ -89,6 +89,7 @@
|
||||
'integrations' => 'Integrationen',
|
||||
'manage_workspaces' => 'Workspaces verwalten',
|
||||
'operations' => 'Operationen',
|
||||
'evidence_overview' => 'Nachweise',
|
||||
'audit_log' => 'Audit-Log',
|
||||
'alerts' => 'Alerts',
|
||||
'governance' => 'Governance',
|
||||
|
||||
@ -89,6 +89,7 @@
|
||||
'integrations' => 'Integrations',
|
||||
'manage_workspaces' => 'Manage workspaces',
|
||||
'operations' => 'Operations',
|
||||
'evidence_overview' => 'Evidence',
|
||||
'audit_log' => 'Audit Log',
|
||||
'alerts' => 'Alerts',
|
||||
'governance' => 'Governance',
|
||||
|
||||
@ -1,44 +1,205 @@
|
||||
<x-filament-panels::page>
|
||||
@php($selectedAudit = $this->selectedAuditRecord())
|
||||
@php($selectedAuditLink = $this->selectedAuditTargetLink())
|
||||
@php($environmentFilterChip = $this->environmentFilterChip())
|
||||
@php
|
||||
$selectedAudit = $this->selectedAuditRecord();
|
||||
$selectedAuditLink = $this->selectedAuditTargetLink();
|
||||
$selectedAuditProof = $this->selectedAuditProofPayload();
|
||||
$environmentFilterChip = $this->environmentFilterChip();
|
||||
$payload = $this->auditDisclosurePayload();
|
||||
$focus = $payload['focus'];
|
||||
@endphp
|
||||
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-3">
|
||||
<div class="text-lg font-semibold text-gray-900 dark:text-gray-100">
|
||||
Summary-first audit history
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="max-w-4xl space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::badge color="gray">
|
||||
{{ $payload['scope_label'] }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge color="gray">
|
||||
Audit proof workbench
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold text-gray-950 dark:text-white">
|
||||
Which event proves what happened?
|
||||
</h2>
|
||||
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $payload['scope_description'] }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if ($environmentFilterChip !== null)
|
||||
<div class="shrink-0">
|
||||
@include('filament.partials.workspace-hub-environment-filter-chip', [
|
||||
'label' => $environmentFilterChip['label'],
|
||||
'clearUrl' => $environmentFilterChip['clear_url'],
|
||||
'description' => $environmentFilterChip['description'],
|
||||
])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Review governance, operational, and workspace-admin events in reverse chronological order without leaving the canonical Monitoring route.
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
Actor, outcome, target, and readable context stay visible even when the original record changes or disappears later.
|
||||
</div>
|
||||
|
||||
<div class="text-sm text-gray-600 dark:text-gray-300">
|
||||
The selected event is URL-addressable through the <span class="font-mono text-xs">event</span> query parameter. If the event is no longer visible in the current history view, the page quietly falls back to the unselected log.
|
||||
</div>
|
||||
|
||||
@if ($environmentFilterChip !== null)
|
||||
@include('filament.partials.workspace-hub-environment-filter-chip', [
|
||||
'label' => $environmentFilterChip['label'],
|
||||
'clearUrl' => $environmentFilterChip['clear_url'],
|
||||
'description' => $environmentFilterChip['description'],
|
||||
])
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
|
||||
@if ($selectedAudit)
|
||||
<x-filament::section>
|
||||
@include('filament.pages.monitoring.partials.audit-log-inspect-event', [
|
||||
'selectedAudit' => $selectedAudit,
|
||||
'selectedAuditLink' => $selectedAuditLink,
|
||||
])
|
||||
</x-filament::section>
|
||||
@endif
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_22rem]" data-testid="audit-disclosure-workbench">
|
||||
<main class="min-w-0 space-y-4" data-testid="audit-proof-primary">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
@if ($focus)
|
||||
<div class="flex flex-col gap-4">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::badge :color="$focus['outcome']['color']">
|
||||
{{ $focus['outcome']['label'] }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge color="gray">
|
||||
{{ $payload['has_selected_event'] ? 'Selected event proof' : 'Latest event proof' }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
{{ $this->table }}
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Event proof
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-950 dark:text-white">
|
||||
{{ $focus['summary'] }}
|
||||
</h3>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-5">
|
||||
<div class="rounded-lg border border-gray-200 p-3 text-sm dark:border-white/10">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Actor</div>
|
||||
<div class="mt-2 font-medium text-gray-950 dark:text-white">{{ $focus['actor']['label'] }}</div>
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ $focus['actor']['type'] }}</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 p-3 text-sm dark:border-white/10">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Action</div>
|
||||
<div class="mt-2 font-medium text-gray-950 dark:text-white">{{ $focus['action'] }}</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 p-3 text-sm dark:border-white/10">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Target</div>
|
||||
<div class="mt-2 font-medium text-gray-950 dark:text-white">{{ $focus['target']['label'] }}</div>
|
||||
<div class="mt-1 text-xs text-gray-500 dark:text-gray-400">{{ $focus['target']['type'] }}</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 p-3 text-sm dark:border-white/10">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Outcome</div>
|
||||
<div class="mt-2">
|
||||
<x-filament::badge :color="$focus['outcome']['color']">
|
||||
{{ $focus['outcome']['label'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border border-gray-200 p-3 text-sm dark:border-white/10">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">Time</div>
|
||||
<div class="mt-2 font-medium text-gray-950 dark:text-white">{{ $focus['time'] }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex flex-wrap gap-2">
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
:href="$focus['inspect_url']"
|
||||
icon="heroicon-o-eye"
|
||||
color="gray"
|
||||
>
|
||||
Inspect event proof
|
||||
</x-filament::button>
|
||||
|
||||
@foreach ($focus['related_links'] as $link)
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
:href="$link['url']"
|
||||
icon="heroicon-o-arrow-top-right-on-square"
|
||||
color="gray"
|
||||
>
|
||||
{{ $link['label'] }}
|
||||
</x-filament::button>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
@else
|
||||
<div class="space-y-2">
|
||||
<h3 class="text-lg font-semibold text-gray-950 dark:text-white">
|
||||
No audit events in scope
|
||||
</h3>
|
||||
<p class="text-sm text-gray-600 dark:text-gray-300">
|
||||
No event currently proves actor, action, target, outcome, and time for the active view.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($selectedAudit)
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
@include('filament.pages.monitoring.partials.audit-log-inspect-event', [
|
||||
'selectedAudit' => $selectedAudit,
|
||||
'selectedAuditLink' => $selectedAuditLink,
|
||||
'selectedAuditProof' => $selectedAuditProof,
|
||||
])
|
||||
</div>
|
||||
@endif
|
||||
</main>
|
||||
|
||||
<aside class="min-w-0 space-y-4" data-testid="audit-proof-aside">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Related proof
|
||||
</h3>
|
||||
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
Event proof links only appear when a repo-real route and authorization path exist.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@if ($focus && $focus['related_links'] !== [])
|
||||
@foreach ($focus['related_links'] as $link)
|
||||
<a class="block rounded-lg border border-gray-200 p-3 text-sm hover:bg-gray-50 dark:border-white/10 dark:hover:bg-white/5" href="{{ $link['url'] }}">
|
||||
<span class="font-medium text-gray-950 dark:text-white">{{ $link['label'] }}</span>
|
||||
<span class="mt-1 block text-xs text-gray-500 dark:text-gray-400">Open related proof for this event.</span>
|
||||
</a>
|
||||
@endforeach
|
||||
@else
|
||||
<div class="rounded-lg border border-gray-200 p-3 text-sm dark:border-white/10">
|
||||
<div class="font-medium text-gray-950 dark:text-white">Related proof unavailable</div>
|
||||
<p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
This event has no authorized related proof link in the current repository state.
|
||||
</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 text-sm shadow-sm dark:border-white/10 dark:bg-gray-900"
|
||||
data-testid="audit-disclosure-diagnostics"
|
||||
>
|
||||
<summary class="cursor-pointer font-medium text-gray-950 dark:text-white">
|
||||
Diagnostics - Collapsed
|
||||
</summary>
|
||||
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Raw metadata, provider payloads, stack traces, and debug fields stay behind explicit diagnostic inspection and are not part of the first-read audit proof.
|
||||
</p>
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Audit event history
|
||||
</h2>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Secondary event inventory for filtering, search, and historical investigation after the proof summary is clear.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{ $this->table }}
|
||||
</div>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
||||
@ -1,20 +1,208 @@
|
||||
<x-filament-panels::page>
|
||||
@php($environmentFilterChip = $this->environmentFilterChip())
|
||||
@php
|
||||
$environmentFilterChip = $this->environmentFilterChip();
|
||||
$payload = $this->evidenceDisclosurePayload();
|
||||
@endphp
|
||||
|
||||
<div class="space-y-6">
|
||||
<x-filament::section>
|
||||
<div class="flex flex-col gap-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
<p>This workspace evidence overview stays workspace-scoped; environment-owned entries appear as an explicit page filter.</p>
|
||||
<div class="space-y-4">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="flex flex-col gap-3 xl:flex-row xl:items-start xl:justify-between">
|
||||
<div class="max-w-4xl space-y-2">
|
||||
<div class="flex flex-wrap items-center gap-2">
|
||||
<x-filament::badge color="gray">
|
||||
{{ $payload['scope_label'] }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge color="gray">
|
||||
Evidence proof workbench
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<h2 class="text-xl font-semibold text-gray-950 dark:text-white">
|
||||
What proof is available for this scope?
|
||||
</h2>
|
||||
|
||||
<p class="text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $payload['scope_description'] }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@if ($environmentFilterChip !== null)
|
||||
@include('filament.partials.workspace-hub-environment-filter-chip', [
|
||||
'label' => $environmentFilterChip['label'],
|
||||
'clearUrl' => $environmentFilterChip['clear_url'],
|
||||
])
|
||||
<div class="shrink-0">
|
||||
@include('filament.partials.workspace-hub-environment-filter-chip', [
|
||||
'label' => $environmentFilterChip['label'],
|
||||
'clearUrl' => $environmentFilterChip['clear_url'],
|
||||
])
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
</x-filament::section>
|
||||
</div>
|
||||
|
||||
{{ $this->table }}
|
||||
<div class="grid grid-cols-1 gap-4 lg:grid-cols-[minmax(0,1fr)_22rem]" data-testid="evidence-disclosure-workbench">
|
||||
<main class="min-w-0 space-y-4" data-testid="evidence-proof-primary">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="flex flex-col gap-4 md:flex-row md:items-start md:justify-between">
|
||||
<div class="space-y-2">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Primary proof path
|
||||
</div>
|
||||
|
||||
<h3 class="text-lg font-semibold text-gray-950 dark:text-white">
|
||||
{{ $payload['primary_title'] }}
|
||||
</h3>
|
||||
|
||||
<p class="max-w-3xl text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
{{ $payload['primary_summary'] }}
|
||||
</p>
|
||||
|
||||
@if ($payload['primary_state'] !== null)
|
||||
<div class="space-y-3 rounded-lg border border-gray-200 bg-gray-50 p-3 dark:border-white/10 dark:bg-white/5">
|
||||
<x-filament::badge :color="$payload['primary_state']['color']">
|
||||
{{ $payload['primary_state']['label'] }}
|
||||
</x-filament::badge>
|
||||
|
||||
<div class="grid gap-2 text-sm text-gray-600 dark:text-gray-300 sm:grid-cols-2">
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Reason
|
||||
</div>
|
||||
<p class="mt-1 leading-5">
|
||||
{{ $payload['primary_state']['reason'] }}
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
Impact
|
||||
</div>
|
||||
<p class="mt-1 leading-5">
|
||||
{{ $payload['primary_state']['impact'] }}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@if ($payload['primary_action'])
|
||||
<x-filament::button
|
||||
tag="a"
|
||||
:href="$payload['primary_action']['url']"
|
||||
icon="heroicon-o-arrow-top-right-on-square"
|
||||
class="shrink-0 self-start whitespace-nowrap"
|
||||
data-testid="evidence-primary-proof-action"
|
||||
>
|
||||
{{ $payload['primary_action']['label'] }}
|
||||
</x-filament::button>
|
||||
@else
|
||||
<x-filament::badge color="gray">
|
||||
No open proof action
|
||||
</x-filament::badge>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="grid gap-3 md:grid-cols-2 xl:grid-cols-4" data-testid="evidence-proof-cards">
|
||||
@foreach ($payload['cards'] as $card)
|
||||
<div class="flex h-full rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="flex min-h-36 grow flex-col gap-3">
|
||||
<div class="text-xs font-semibold uppercase text-gray-500 dark:text-gray-400">
|
||||
{{ $card['label'] }}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<x-filament::badge :color="$card['color']">
|
||||
{{ $card['value'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
<p class="text-sm leading-5 text-gray-600 dark:text-gray-300">
|
||||
{{ $card['description'] }}
|
||||
</p>
|
||||
|
||||
<div class="mt-auto flex flex-wrap items-center gap-2 text-xs text-gray-500 dark:text-gray-400">
|
||||
<span>{{ $card['meta'] }}</span>
|
||||
|
||||
@if ($card['url'])
|
||||
<a class="font-medium text-primary-600 hover:underline dark:text-primary-400" href="{{ $card['url'] }}">
|
||||
Open
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</main>
|
||||
|
||||
<aside class="min-w-0 space-y-4" data-testid="evidence-proof-aside">
|
||||
<div class="rounded-xl border border-gray-200 bg-white p-4 shadow-sm dark:border-white/10 dark:bg-gray-900">
|
||||
<div class="space-y-1">
|
||||
<h3 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Evidence path
|
||||
</h3>
|
||||
<p class="text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
Only repo-supported proof sources are listed. Missing pieces stay explicit.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="mt-4 space-y-3">
|
||||
@foreach ($payload['path_items'] as $item)
|
||||
<div class="rounded-lg border border-gray-200 p-3 text-sm dark:border-white/10">
|
||||
<div class="flex items-start justify-between gap-3">
|
||||
<div class="min-w-0">
|
||||
<div class="font-medium text-gray-950 dark:text-white">
|
||||
{{ $item['label'] }}
|
||||
</div>
|
||||
<p class="mt-1 text-xs leading-5 text-gray-500 dark:text-gray-400">
|
||||
{{ $item['description'] }}
|
||||
</p>
|
||||
</div>
|
||||
<x-filament::badge
|
||||
:color="$item['color']"
|
||||
size="sm"
|
||||
class="shrink-0 whitespace-nowrap"
|
||||
data-testid="evidence-path-state-badge"
|
||||
>
|
||||
{{ $item['state'] }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
|
||||
@if ($item['url'])
|
||||
<a class="mt-2 inline-flex text-xs font-medium text-primary-600 hover:underline dark:text-primary-400" href="{{ $item['url'] }}">
|
||||
Open proof
|
||||
</a>
|
||||
@endif
|
||||
</div>
|
||||
@endforeach
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<details
|
||||
class="rounded-xl border border-gray-200 bg-white p-4 text-sm shadow-sm dark:border-white/10 dark:bg-gray-900"
|
||||
data-testid="evidence-disclosure-diagnostics"
|
||||
>
|
||||
<summary class="cursor-pointer font-medium text-gray-950 dark:text-white">
|
||||
Diagnostics - Collapsed
|
||||
</summary>
|
||||
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Raw snapshot payloads, provider responses, operation context, stack traces, and debug metadata stay off this overview. Use authorized evidence, report, or operation detail pages when diagnostic inspection is required.
|
||||
</p>
|
||||
</details>
|
||||
</aside>
|
||||
</div>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<h2 class="text-sm font-semibold text-gray-950 dark:text-white">
|
||||
Evidence inventory
|
||||
</h2>
|
||||
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
|
||||
Secondary context for scanning historical proof records after the current path is clear.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{{ $this->table }}
|
||||
</div>
|
||||
</div>
|
||||
</x-filament-panels::page>
|
||||
|
||||
@ -1,17 +1,21 @@
|
||||
@php
|
||||
$selectedAudit = $selectedAudit ?? null;
|
||||
$selectedAuditLink = $selectedAuditLink ?? null;
|
||||
$selectedAuditProof = $selectedAuditProof ?? null;
|
||||
@endphp
|
||||
|
||||
@if ($selectedAudit)
|
||||
<div class="flex flex-col gap-6">
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
|
||||
{{ \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditOutcome)($selectedAudit->normalizedOutcome()->value) }}
|
||||
</span>
|
||||
<span class="inline-flex items-center rounded-full bg-gray-100 px-3 py-1 text-xs font-medium text-gray-700 dark:bg-gray-800 dark:text-gray-200">
|
||||
<x-filament::badge :color="$selectedAuditProof['outcome']['color'] ?? 'gray'">
|
||||
{{ $selectedAuditProof['outcome']['label'] ?? \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditOutcome)($selectedAudit->normalizedOutcome()->value) }}
|
||||
</x-filament::badge>
|
||||
<x-filament::badge color="gray">
|
||||
Event proof
|
||||
</x-filament::badge>
|
||||
<x-filament::badge color="gray">
|
||||
{{ \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditActorType)($selectedAudit->actorSnapshot()->type->value) }}
|
||||
</span>
|
||||
</x-filament::badge>
|
||||
|
||||
@if (is_array($selectedAuditLink))
|
||||
<a
|
||||
@ -23,7 +27,7 @@ class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="grid gap-4 lg:grid-cols-3">
|
||||
<div class="grid gap-4 lg:grid-cols-5">
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Actor
|
||||
@ -41,6 +45,15 @@ class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Action
|
||||
</div>
|
||||
<div class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $selectedAuditProof['action'] ?? \App\Support\Audit\AuditActionId::labelFor((string) $selectedAudit->action) }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Target
|
||||
@ -53,6 +66,26 @@ class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Outcome
|
||||
</div>
|
||||
<div class="mt-2">
|
||||
<x-filament::badge :color="$selectedAuditProof['outcome']['color'] ?? 'gray'">
|
||||
{{ $selectedAuditProof['outcome']['label'] ?? \App\Support\Badges\BadgeRenderer::label(\App\Support\Badges\BadgeDomain::AuditOutcome)($selectedAudit->normalizedOutcome()->value) }}
|
||||
</x-filament::badge>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Time
|
||||
</div>
|
||||
<div class="mt-2 text-sm font-medium text-gray-900 dark:text-gray-100">
|
||||
{{ $selectedAuditProof['time'] ?? $selectedAudit->recorded_at?->toDayDateTimeString() ?? 'Time unavailable' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
Scope
|
||||
@ -72,13 +105,15 @@ class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text
|
||||
Readable context
|
||||
</div>
|
||||
|
||||
@if ($selectedAudit->contextItems() === [])
|
||||
@php($contextItems = $selectedAuditProof['context_items'] ?? [])
|
||||
|
||||
@if ($contextItems === [])
|
||||
<div class="mt-3 text-sm text-gray-600 dark:text-gray-300">
|
||||
No additional context was recorded for this event.
|
||||
</div>
|
||||
@else
|
||||
<dl class="mt-3 space-y-3">
|
||||
@foreach ($selectedAudit->contextItems() as $item)
|
||||
@foreach ($contextItems as $item)
|
||||
<div>
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ $item['label'] }}
|
||||
@ -92,24 +127,39 @@ class="inline-flex items-center rounded-lg border border-gray-300 px-3 py-2 text
|
||||
@endif
|
||||
</div>
|
||||
|
||||
<div class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900">
|
||||
<div class="text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
Technical metadata
|
||||
</div>
|
||||
<details
|
||||
class="rounded-2xl border border-gray-200 bg-white p-4 shadow-sm dark:border-gray-800 dark:bg-gray-900"
|
||||
data-testid="audit-event-diagnostics"
|
||||
>
|
||||
<summary class="cursor-pointer text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
Diagnostics - Collapsed
|
||||
</summary>
|
||||
|
||||
<dl class="mt-3 space-y-3">
|
||||
@foreach ($selectedAudit->technicalMetadata() as $label => $value)
|
||||
<div>
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ $label }}
|
||||
</dt>
|
||||
<dd class="mt-1 break-all text-sm text-gray-900 dark:text-gray-100">
|
||||
{{ $value }}
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
</div>
|
||||
@php($technicalMetadata = $selectedAuditProof['technical_metadata'] ?? [])
|
||||
|
||||
@if ($technicalMetadata === [])
|
||||
<p class="mt-3 text-sm leading-6 text-gray-600 dark:text-gray-300">
|
||||
Technical metadata is unavailable for this event in the current capability context.
|
||||
</p>
|
||||
@else
|
||||
<div class="mt-3 text-sm font-semibold text-gray-900 dark:text-gray-100">
|
||||
Technical metadata
|
||||
</div>
|
||||
|
||||
<dl class="mt-3 space-y-3">
|
||||
@foreach ($technicalMetadata as $label => $value)
|
||||
<div>
|
||||
<dt class="text-xs font-semibold uppercase tracking-wide text-gray-500 dark:text-gray-400">
|
||||
{{ $label }}
|
||||
</dt>
|
||||
<dd class="mt-1 break-all text-sm text-gray-900 dark:text-gray-100">
|
||||
{{ $value }}
|
||||
</dd>
|
||||
</div>
|
||||
@endforeach
|
||||
</dl>
|
||||
@endif
|
||||
</details>
|
||||
</div>
|
||||
</div>
|
||||
@endif
|
||||
@endif
|
||||
|
||||
@ -449,18 +449,6 @@
|
||||
->get('/admin/evidence/overview', \App\Filament\Pages\Monitoring\EvidenceOverview::class)
|
||||
->name('admin.evidence.overview');
|
||||
|
||||
Route::middleware([
|
||||
'web',
|
||||
'panel:admin',
|
||||
'ensure-correct-guard:web',
|
||||
DisableBladeIconComponents::class,
|
||||
DispatchServingFilamentEvent::class,
|
||||
FilamentAuthenticate::class,
|
||||
'ensure-workspace-selected',
|
||||
])
|
||||
->get('/admin/evidence/overview', \App\Filament\Pages\Monitoring\EvidenceOverview::class)
|
||||
->name('admin.evidence.overview');
|
||||
|
||||
Route::middleware([
|
||||
'web',
|
||||
'panel:admin',
|
||||
|
||||
@ -106,7 +106,7 @@
|
||||
->assertNoConsoleLogs();
|
||||
|
||||
visit(route('admin.monitoring.audit-log', ['event' => (int) $audit->getKey()]))
|
||||
->waitForText('Summary-first audit history')
|
||||
->waitForText('Which event proves what happened?')
|
||||
->assertSee('Close details')
|
||||
->assertSee('Readable context')
|
||||
->assertNoJavaScriptErrors()
|
||||
@ -123,7 +123,7 @@
|
||||
'environment_id' => (int) $tenant->getKey(),
|
||||
'search' => $tenant->name,
|
||||
]))
|
||||
->waitForText('This workspace evidence overview stays workspace-scoped; environment-owned entries appear as an explicit page filter.')
|
||||
->waitForText('What proof is available for this scope?')
|
||||
->assertSee($tenant->name)
|
||||
->assertSee('Clear filters')
|
||||
->assertNoJavaScriptErrors()
|
||||
|
||||
@ -0,0 +1,485 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Models\AuditLog;
|
||||
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\Support\Evidence\EvidenceCompletenessState;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
|
||||
pest()->browser()->timeout(60_000);
|
||||
|
||||
it('Spec329 smokes Evidence Overview proof-first disclosure and filter clearing', function (): void {
|
||||
$fixture = spec329BrowserDisclosureFixture();
|
||||
spec329AuthenticateDisclosureBrowser($this, $fixture['user'], $fixture['environmentA']);
|
||||
|
||||
$cleanPath = json_encode((string) parse_url(route('admin.evidence.overview'), PHP_URL_PATH), JSON_THROW_ON_ERROR);
|
||||
|
||||
$page = visit(route('admin.evidence.overview'))
|
||||
->resize(1440, 1100)
|
||||
->waitForText('What proof is available for this scope?')
|
||||
->assertSee(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee('Evidence proof workbench')
|
||||
->assertSee('Primary proof path')
|
||||
->assertSee('Evidence path')
|
||||
->assertSee('Evidence snapshot')
|
||||
->assertSee('Review pack')
|
||||
->assertSee('Stored report / export')
|
||||
->assertSee('Operation proof')
|
||||
->assertSee('Proof incomplete')
|
||||
->assertSee('A proof record exists, but no usable captured evidence is available yet.')
|
||||
->assertSee('Primary evidence snapshot is empty.')
|
||||
->assertSee('Supporting proof exists through the review pack, stored report, and operation record.')
|
||||
->assertSee('Empty')
|
||||
->assertSee('Ready')
|
||||
->assertSee('Available')
|
||||
->assertSourceHas('Search evidence or next step')
|
||||
->assertSee('Evidence inventory')
|
||||
->assertSee($fixture['environmentA']->name)
|
||||
->assertSee($fixture['environmentB']->name)
|
||||
->assertDontSee('The artifact row exists, but it does not contain usable captured content.')
|
||||
->assertDontSee('artifact row exists')
|
||||
->assertSourceMissing('Search tenant or next')
|
||||
->assertDontSee('Empty...')
|
||||
->assertDontSee('Re...')
|
||||
->assertDontSee('raw payload should stay hidden')
|
||||
->assertDontSee('provider secret should stay hidden')
|
||||
->assertDontSee('stack trace should stay hidden')
|
||||
->assertDontSee('debug metadata should stay hidden')
|
||||
->assertDontSee('internal exception should stay hidden')
|
||||
->assertDontSee('current tenant')
|
||||
->assertDontSee('tenant filter')
|
||||
->assertDontSee('entitled tenant')
|
||||
->assertDontSee('all tenants')
|
||||
->assertScript('document.querySelector("[data-testid=\"evidence-disclosure-diagnostics\"]")?.open === false', true)
|
||||
->assertScript('(() => {
|
||||
const action = document.querySelector("[data-testid=\"evidence-primary-proof-action\"]");
|
||||
|
||||
if (! action) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const box = action.getBoundingClientRect();
|
||||
|
||||
return action.textContent.trim() === "Open evidence snapshot"
|
||||
&& box.height > 0
|
||||
&& box.height <= 44
|
||||
&& action.scrollWidth <= Math.ceil(action.clientWidth) + 1
|
||||
&& getComputedStyle(action).whiteSpace === "nowrap";
|
||||
})()', true)
|
||||
->assertScript('(() => {
|
||||
const badges = Array.from(document.querySelectorAll("[data-testid=\"evidence-path-state-badge\"]"));
|
||||
|
||||
return badges.length >= 4
|
||||
&& badges.every((badge) => {
|
||||
const label = badge.textContent.trim();
|
||||
const box = badge.getBoundingClientRect();
|
||||
|
||||
return ! ["Empty...", "Re..."].includes(label)
|
||||
&& box.width > 0
|
||||
&& badge.scrollWidth <= Math.ceil(badge.clientWidth) + 1;
|
||||
});
|
||||
})()', true)
|
||||
->assertScript('(() => {
|
||||
const grid = document.querySelector("[data-testid=\"evidence-disclosure-workbench\"]");
|
||||
const main = document.querySelector("[data-testid=\"evidence-proof-primary\"]");
|
||||
const aside = document.querySelector("[data-testid=\"evidence-proof-aside\"]");
|
||||
|
||||
if (! grid || ! main || ! aside) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const children = Array.from(grid.children);
|
||||
const mainBox = main.getBoundingClientRect();
|
||||
const asideBox = aside.getBoundingClientRect();
|
||||
|
||||
return window.innerWidth >= 1024
|
||||
&& grid.classList.contains("lg:grid-cols-[minmax(0,1fr)_22rem]")
|
||||
&& aside.tagName === "ASIDE"
|
||||
&& children.indexOf(main) !== -1
|
||||
&& children.indexOf(aside) > children.indexOf(main)
|
||||
&& asideBox.left > mainBox.right
|
||||
&& Math.abs(asideBox.top - mainBox.top) <= 8;
|
||||
})()', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec329DisclosureScreenshot('evidence-overview--clean'));
|
||||
|
||||
spec329CopyDisclosureScreenshot('evidence-overview--clean');
|
||||
|
||||
$page = visit(route('admin.evidence.overview', [
|
||||
'environment_id' => (int) $fixture['environmentA']->getKey(),
|
||||
]))
|
||||
->waitForText('Environment filter:')
|
||||
->assertSee('Environment filter: '.$fixture['environmentA']->name)
|
||||
->assertSee('What proof is available for this scope?')
|
||||
->assertSee($fixture['environmentA']->name)
|
||||
->assertSee('Proof incomplete')
|
||||
->assertSee('Primary evidence snapshot is empty.')
|
||||
->assertDontSee('The artifact row exists, but it does not contain usable captured content.')
|
||||
->assertDontSee('Empty...')
|
||||
->assertDontSee('Re...')
|
||||
->assertDontSee($fixture['environmentB']->name)
|
||||
->assertScript('document.querySelector("[data-testid=\"evidence-disclosure-diagnostics\"]")?.open === false', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec329DisclosureScreenshot('evidence-overview--filtered'));
|
||||
|
||||
spec329CopyDisclosureScreenshot('evidence-overview--filtered');
|
||||
|
||||
spec329ClearDisclosureEnvironmentFilter($page)
|
||||
->waitForText(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->waitForText($fixture['environmentB']->name)
|
||||
->assertScript("window.location.pathname === {$cleanPath}", true)
|
||||
->assertScript('! window.location.search.includes("environment_id=")', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec329DisclosureScreenshot('evidence-overview--after-clear'));
|
||||
|
||||
spec329CopyDisclosureScreenshot('evidence-overview--after-clear');
|
||||
|
||||
$page->script('window.location.reload();');
|
||||
|
||||
$page
|
||||
->waitForText(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee($fixture['environmentB']->name)
|
||||
->assertScript("window.location.pathname === {$cleanPath}", true)
|
||||
->assertScript('! window.location.search.includes("environment_id=")', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec329DisclosureScreenshot('evidence-overview--after-reload'));
|
||||
|
||||
spec329CopyDisclosureScreenshot('evidence-overview--after-reload');
|
||||
});
|
||||
|
||||
it('Spec329 smokes Audit Log event-proof disclosure and filter clearing', function (): void {
|
||||
$fixture = spec329BrowserDisclosureFixture();
|
||||
spec329AuthenticateDisclosureBrowser($this, $fixture['user'], $fixture['environmentA']);
|
||||
|
||||
$cleanPath = json_encode((string) parse_url(route('admin.monitoring.audit-log'), PHP_URL_PATH), JSON_THROW_ON_ERROR);
|
||||
|
||||
$page = visit(route('admin.monitoring.audit-log', [
|
||||
'event' => (int) $fixture['auditA']->getKey(),
|
||||
]))
|
||||
->resize(1440, 1100)
|
||||
->waitForText('Which event proves what happened?')
|
||||
->assertSee(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee('Audit proof workbench')
|
||||
->assertSee('Selected event proof')
|
||||
->assertSee('Event proof')
|
||||
->assertSee('Actor')
|
||||
->assertSee('Action')
|
||||
->assertSee('Target')
|
||||
->assertSee('Outcome')
|
||||
->assertSee('Time')
|
||||
->assertSee('Related proof')
|
||||
->assertSee('Operation proof')
|
||||
->assertSee('Readable context')
|
||||
->assertSee('Audit event history')
|
||||
->assertSee('Spec329 Browser Operator A')
|
||||
->assertSee('Permission posture checked')
|
||||
->assertSee('Permission posture report')
|
||||
->assertDontSee('raw payload should stay hidden')
|
||||
->assertDontSee('provider secret should stay hidden')
|
||||
->assertDontSee('stack trace should stay hidden')
|
||||
->assertDontSee('debug metadata should stay hidden')
|
||||
->assertDontSee('internal exception should stay hidden')
|
||||
->assertDontSee('provider response should stay hidden')
|
||||
->assertDontSee('current tenant')
|
||||
->assertDontSee('tenant filter')
|
||||
->assertDontSee('entitled tenant')
|
||||
->assertDontSee('all tenants')
|
||||
->assertScript('document.querySelector("[data-testid=\"audit-disclosure-diagnostics\"]")?.open === false', true)
|
||||
->assertScript('document.querySelector("[data-testid=\"audit-event-diagnostics\"]")?.open === false', true)
|
||||
->assertScript('(() => {
|
||||
const grid = document.querySelector("[data-testid=\"audit-disclosure-workbench\"]");
|
||||
const main = document.querySelector("[data-testid=\"audit-proof-primary\"]");
|
||||
const aside = document.querySelector("[data-testid=\"audit-proof-aside\"]");
|
||||
|
||||
if (! grid || ! main || ! aside) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const children = Array.from(grid.children);
|
||||
const mainBox = main.getBoundingClientRect();
|
||||
const asideBox = aside.getBoundingClientRect();
|
||||
|
||||
return window.innerWidth >= 1024
|
||||
&& grid.classList.contains("lg:grid-cols-[minmax(0,1fr)_22rem]")
|
||||
&& aside.tagName === "ASIDE"
|
||||
&& children.indexOf(main) !== -1
|
||||
&& children.indexOf(aside) > children.indexOf(main)
|
||||
&& asideBox.left > mainBox.right
|
||||
&& Math.abs(asideBox.top - mainBox.top) <= 8;
|
||||
})()', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec329DisclosureScreenshot('audit-log--clean'));
|
||||
|
||||
spec329CopyDisclosureScreenshot('audit-log--clean');
|
||||
|
||||
$page = visit(route('admin.monitoring.audit-log', [
|
||||
'environment_id' => (int) $fixture['environmentA']->getKey(),
|
||||
'event' => (int) $fixture['auditA']->getKey(),
|
||||
]))
|
||||
->waitForText('Environment filter:')
|
||||
->assertSee('Environment filter: '.$fixture['environmentA']->name)
|
||||
->assertSee('Which event proves what happened?')
|
||||
->assertSee('Permission posture checked')
|
||||
->assertDontSee('Workspace selected by browser proof B')
|
||||
->assertScript('document.querySelector("[data-testid=\"audit-disclosure-diagnostics\"]")?.open === false', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec329DisclosureScreenshot('audit-log--filtered'));
|
||||
|
||||
spec329CopyDisclosureScreenshot('audit-log--filtered');
|
||||
|
||||
spec329ClearDisclosureEnvironmentFilter($page)
|
||||
->waitForText(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->waitForText('Workspace selected by browser proof B')
|
||||
->assertScript("window.location.pathname === {$cleanPath}", true)
|
||||
->assertScript('! window.location.search.includes("environment_id=")', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec329DisclosureScreenshot('audit-log--after-clear'));
|
||||
|
||||
spec329CopyDisclosureScreenshot('audit-log--after-clear');
|
||||
|
||||
$page->script('window.location.reload();');
|
||||
|
||||
$page
|
||||
->waitForText(__('localization.shell.no_environment_selected'))
|
||||
->assertDontSee('Environment filter:')
|
||||
->assertSee('Workspace selected by browser proof B')
|
||||
->assertScript("window.location.pathname === {$cleanPath}", true)
|
||||
->assertScript('! window.location.search.includes("environment_id=")', true)
|
||||
->assertNoJavaScriptErrors()
|
||||
->assertNoConsoleLogs()
|
||||
->screenshot(true, spec329DisclosureScreenshot('audit-log--after-reload'));
|
||||
|
||||
spec329CopyDisclosureScreenshot('audit-log--after-reload');
|
||||
});
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* user: User,
|
||||
* environmentA: ManagedEnvironment,
|
||||
* environmentB: ManagedEnvironment,
|
||||
* auditA: AuditLog,
|
||||
* auditB: AuditLog
|
||||
* }
|
||||
*/
|
||||
function spec329BrowserDisclosureFixture(): array
|
||||
{
|
||||
$environmentA = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => 'Spec329 Browser Tenant Environment A',
|
||||
'external_id' => 'spec329-browser-environment-a',
|
||||
]);
|
||||
|
||||
[$user, $environmentA] = createUserWithTenant(
|
||||
tenant: $environmentA,
|
||||
role: 'owner',
|
||||
workspaceRole: 'owner',
|
||||
);
|
||||
|
||||
$environmentB = ManagedEnvironment::factory()->active()->create([
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'name' => 'Spec329 Browser Environment B',
|
||||
'external_id' => 'spec329-browser-environment-b',
|
||||
]);
|
||||
|
||||
createUserWithTenant(
|
||||
tenant: $environmentB,
|
||||
user: $user,
|
||||
role: 'owner',
|
||||
workspaceRole: 'owner',
|
||||
);
|
||||
|
||||
$runA = OperationRun::factory()->forTenant($environmentA)->create([
|
||||
'type' => 'tenant.evidence.snapshot.generate',
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
'context' => [
|
||||
'raw_payload' => 'raw payload should stay hidden',
|
||||
'provider_secret' => 'provider secret should stay hidden',
|
||||
'stack_trace' => 'stack trace should stay hidden',
|
||||
'debug_metadata' => 'debug metadata should stay hidden',
|
||||
'internal_exception' => 'internal exception should stay hidden',
|
||||
],
|
||||
'completed_at' => now()->subMinutes(10),
|
||||
]);
|
||||
|
||||
$runB = OperationRun::factory()->forTenant($environmentB)->create([
|
||||
'type' => 'policy.sync',
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
'completed_at' => now()->subMinutes(5),
|
||||
]);
|
||||
|
||||
$snapshotA = EvidenceSnapshot::query()->create([
|
||||
'managed_environment_id' => (int) $environmentA->getKey(),
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'operation_run_id' => (int) $runA->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||
'summary' => [
|
||||
'missing_dimensions' => 0,
|
||||
'stale_dimensions' => 0,
|
||||
'raw_payload' => 'raw payload should stay hidden',
|
||||
],
|
||||
'generated_at' => now()->subMinutes(4),
|
||||
'expires_at' => now()->addDays(30),
|
||||
]);
|
||||
|
||||
EvidenceSnapshot::query()->create([
|
||||
'managed_environment_id' => (int) $environmentB->getKey(),
|
||||
'workspace_id' => (int) $environmentB->workspace_id,
|
||||
'operation_run_id' => (int) $runB->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Partial->value,
|
||||
'summary' => [
|
||||
'missing_dimensions' => 1,
|
||||
'stale_dimensions' => 0,
|
||||
],
|
||||
'generated_at' => now()->subMinutes(9),
|
||||
'expires_at' => now()->addDays(30),
|
||||
]);
|
||||
|
||||
ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $environmentA->getKey(),
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'evidence_snapshot_id' => (int) $snapshotA->getKey(),
|
||||
'operation_run_id' => (int) $runA->getKey(),
|
||||
'status' => ReviewPackStatus::Ready->value,
|
||||
]);
|
||||
|
||||
$storedReport = StoredReport::factory()->permissionPosture([
|
||||
'raw_payload' => 'raw payload should stay hidden',
|
||||
])->create([
|
||||
'managed_environment_id' => (int) $environmentA->getKey(),
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'fingerprint' => 'spec329-browser-report',
|
||||
]);
|
||||
|
||||
$auditA = AuditLog::query()->create([
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'managed_environment_id' => (int) $environmentA->getKey(),
|
||||
'operation_run_id' => (int) $runA->getKey(),
|
||||
'actor_email' => 'spec329-a@example.test',
|
||||
'actor_name' => 'Spec329 Browser Operator A',
|
||||
'actor_type' => 'human',
|
||||
'action' => 'permission_posture.checked',
|
||||
'status' => 'success',
|
||||
'resource_type' => 'stored_report',
|
||||
'resource_id' => (string) $storedReport->getKey(),
|
||||
'target_label' => 'Permission posture report',
|
||||
'summary' => 'Permission posture checked',
|
||||
'metadata' => [
|
||||
'reason' => 'Evidence review',
|
||||
'raw_payload' => 'raw payload should stay hidden',
|
||||
'provider_secret' => 'provider secret should stay hidden',
|
||||
'stack_trace' => 'stack trace should stay hidden',
|
||||
'debug_metadata' => 'debug metadata should stay hidden',
|
||||
'internal_exception' => 'internal exception should stay hidden',
|
||||
'provider_response' => 'provider response should stay hidden',
|
||||
],
|
||||
'recorded_at' => now()->subMinutes(2),
|
||||
]);
|
||||
|
||||
$auditB = AuditLog::query()->create([
|
||||
'workspace_id' => (int) $environmentB->workspace_id,
|
||||
'managed_environment_id' => (int) $environmentB->getKey(),
|
||||
'actor_email' => 'spec329-b@example.test',
|
||||
'actor_name' => 'Spec329 Browser Operator B',
|
||||
'actor_type' => 'human',
|
||||
'action' => 'workspace.selected',
|
||||
'status' => 'success',
|
||||
'resource_type' => 'workspace',
|
||||
'resource_id' => (string) $environmentB->workspace_id,
|
||||
'target_label' => 'Workspace '.$environmentB->workspace_id,
|
||||
'summary' => 'Workspace selected by browser proof B',
|
||||
'recorded_at' => now()->subMinute(),
|
||||
]);
|
||||
|
||||
return [
|
||||
'user' => $user,
|
||||
'environmentA' => $environmentA,
|
||||
'environmentB' => $environmentB,
|
||||
'auditA' => $auditA,
|
||||
'auditB' => $auditB,
|
||||
];
|
||||
}
|
||||
|
||||
function spec329AuthenticateDisclosureBrowser(
|
||||
mixed $test,
|
||||
User $user,
|
||||
ManagedEnvironment $rememberedEnvironment,
|
||||
): void {
|
||||
$workspaceId = (int) $rememberedEnvironment->workspace_id;
|
||||
|
||||
$session = [
|
||||
WorkspaceContext::SESSION_KEY => $workspaceId,
|
||||
WorkspaceContext::LAST_ENVIRONMENT_IDS_SESSION_KEY => [
|
||||
(string) $workspaceId => (int) $rememberedEnvironment->getKey(),
|
||||
],
|
||||
];
|
||||
|
||||
$test->actingAs($user)->withSession($session);
|
||||
|
||||
foreach ($session as $key => $value) {
|
||||
session()->put($key, $value);
|
||||
}
|
||||
|
||||
setAdminPanelContext($rememberedEnvironment);
|
||||
}
|
||||
|
||||
function spec329ClearDisclosureEnvironmentFilter(mixed $page): mixed
|
||||
{
|
||||
$page->assertScript('document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\') instanceof HTMLAnchorElement', true);
|
||||
$page->script('window.location.assign(document.querySelector(\'[data-testid="workspace-hub-environment-filter-clear"]\').href);');
|
||||
|
||||
return $page;
|
||||
}
|
||||
|
||||
function spec329DisclosureScreenshot(string $name): string
|
||||
{
|
||||
return 'spec329-'.$name;
|
||||
}
|
||||
|
||||
function spec329CopyDisclosureScreenshot(string $name, ?string $targetFilename = null): void
|
||||
{
|
||||
$filename = spec329DisclosureScreenshot($name).'.png';
|
||||
$source = base_path('tests/Browser/Screenshots/'.$filename);
|
||||
$targetDirectory = repo_path('specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots');
|
||||
$targetFilename ??= $filename;
|
||||
|
||||
if (! is_dir($targetDirectory)) {
|
||||
@mkdir($targetDirectory, 0755, true);
|
||||
}
|
||||
|
||||
if (! is_dir($targetDirectory) || ! is_writable($targetDirectory)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (! is_file($source)) {
|
||||
$source = \Pest\Browser\Support\Screenshot::path($filename);
|
||||
}
|
||||
|
||||
if (is_file($source)) {
|
||||
@copy($source, $targetDirectory.DIRECTORY_SEPARATOR.$targetFilename);
|
||||
}
|
||||
}
|
||||
@ -62,6 +62,7 @@ function auditLogDetailTestRecord(ManagedEnvironment $tenant, array $attributes
|
||||
->assertCanSeeTableRecords([$audit])
|
||||
->assertSet('selectedAuditLogId', (int) $audit->getKey())
|
||||
->assertSee('Readable context')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Technical metadata')
|
||||
->assertSee('Nightly iOS backup')
|
||||
->assertSee('Open backup set');
|
||||
@ -88,6 +89,7 @@ function auditLogDetailTestRecord(ManagedEnvironment $tenant, array $attributes
|
||||
->assertCanSeeTableRecords([$audit])
|
||||
->assertSet('selectedAuditLogId', (int) $audit->getKey())
|
||||
->assertSee('Archived backup')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Technical metadata')
|
||||
->assertDontSee('Open backup set');
|
||||
});
|
||||
|
||||
@ -44,18 +44,19 @@ function auditLogPageTestRecord(?ManagedEnvironment $tenant, array $attributes =
|
||||
], $attributes));
|
||||
}
|
||||
|
||||
it('renders the canonical audit route as a summary-first monitoring surface', function (): void {
|
||||
it('renders the canonical audit route as an event-proof-first monitoring surface', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.monitoring.audit-log'))
|
||||
->assertOk()
|
||||
->assertSee('Summary-first audit history')
|
||||
->assertSee('Review governance, operational, and workspace-admin events in reverse chronological order');
|
||||
->assertSee('Which event proves what happened?')
|
||||
->assertSee('Audit proof workbench')
|
||||
->assertSee('Workspace-wide audit disclosure across authorized environment and workspace events.');
|
||||
});
|
||||
|
||||
it('keeps preselected audit detail subordinate to the summary-first route', function (): void {
|
||||
it('keeps preselected audit detail inside the event-proof-first route', function (): void {
|
||||
[$user, $tenant] = createUserWithTenant(role: 'owner');
|
||||
|
||||
$audit = auditLogPageTestRecord($tenant, [
|
||||
@ -66,7 +67,8 @@ function auditLogPageTestRecord(?ManagedEnvironment $tenant, array $attributes =
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.monitoring.audit-log', ['event' => (int) $audit->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Summary-first audit history')
|
||||
->assertSee('Which event proves what happened?')
|
||||
->assertSee('Selected event proof')
|
||||
->assertSee('Preselected audit detail')
|
||||
->assertDontSee('Focused review lane');
|
||||
});
|
||||
|
||||
@ -156,7 +156,7 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a
|
||||
|
||||
expect(workspaceSidebarLabelsByGroup())->toBe([
|
||||
'' => ['Overview'],
|
||||
'Monitoring' => ['Finding exceptions', 'Operations', 'Alerts', 'Audit Log'],
|
||||
'Monitoring' => ['Finding exceptions', 'Operations', 'Alerts', 'Evidence', 'Audit Log'],
|
||||
'Reporting' => ['Reviews', 'Customer reviews'],
|
||||
'Settings' => ['Manage workspaces', 'Integrations', 'Settings'],
|
||||
'Governance' => ['Governance inbox', 'Decision register'],
|
||||
@ -170,6 +170,10 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a
|
||||
'operations',
|
||||
fn ($workspace): string => route('admin.operations.index', ['workspace' => $workspace]),
|
||||
],
|
||||
'evidence overview' => [
|
||||
'evidence overview',
|
||||
fn (): string => route('admin.evidence.overview'),
|
||||
],
|
||||
'customer reviews' => [
|
||||
'customer reviews',
|
||||
fn (): string => CustomerReviewWorkspace::getUrl(panel: 'admin'),
|
||||
@ -205,7 +209,7 @@ function seedWorkspaceSidebarVisibleDecision(ManagedEnvironment $tenant, User $a
|
||||
|
||||
expect(workspaceSidebarLabelsByGroup())->toBe([
|
||||
'' => ['Overview'],
|
||||
'Monitoring' => ['Finding exceptions', 'Operations', 'Alerts', 'Audit Log'],
|
||||
'Monitoring' => ['Finding exceptions', 'Operations', 'Alerts', 'Evidence', 'Audit Log'],
|
||||
'Reporting' => ['Reviews', 'Customer reviews'],
|
||||
'Settings' => ['Manage workspaces', 'Integrations', 'Settings'],
|
||||
'Governance' => ['Governance inbox', 'Decision register'],
|
||||
|
||||
@ -45,6 +45,7 @@
|
||||
->assertSet('selectedAuditLogId', (int) $audit->getKey())
|
||||
->assertSee('Workspace selected for Workspace 1')
|
||||
->assertSee('Readable context')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Technical metadata')
|
||||
->assertActionVisible('close_selected_audit_event');
|
||||
});
|
||||
@ -187,19 +188,21 @@
|
||||
'recorded_at' => now(),
|
||||
]);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.monitoring.audit-log'))
|
||||
->assertOk()
|
||||
->assertDontSee('Close details')
|
||||
->assertDontSee('Open operation');
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $tenant->workspace_id);
|
||||
Filament::setTenant(null, true);
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $tenant->workspace_id])
|
||||
->get(route('admin.monitoring.audit-log', ['event' => (int) $audit->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Close details')
|
||||
->assertSee('Open operation');
|
||||
Livewire::actingAs($user)
|
||||
->test(AuditLogPage::class)
|
||||
->assertActionDoesNotExist('close_selected_audit_event')
|
||||
->assertActionDoesNotExist('open_selected_audit_target');
|
||||
|
||||
Livewire::withQueryParams([
|
||||
'event' => (int) $audit->getKey(),
|
||||
])
|
||||
->actingAs($user)
|
||||
->test(AuditLogPage::class)
|
||||
->assertActionVisible('close_selected_audit_event')
|
||||
->assertActionVisible('open_selected_audit_target');
|
||||
});
|
||||
|
||||
it('surfaces origin context quietly when deep-linked to a selected audit event', function (): void {
|
||||
|
||||
@ -0,0 +1,317 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
use App\Filament\Pages\Monitoring\AuditLog as AuditLogPage;
|
||||
use App\Filament\Pages\Monitoring\EvidenceOverview;
|
||||
use App\Filament\Resources\EvidenceSnapshotResource;
|
||||
use App\Filament\Resources\ReviewPackResource;
|
||||
use App\Filament\Resources\StoredReportResource;
|
||||
use App\Models\AuditLog;
|
||||
use App\Models\EvidenceSnapshot;
|
||||
use App\Models\ManagedEnvironment;
|
||||
use App\Models\OperationRun;
|
||||
use App\Models\ReviewPack;
|
||||
use App\Models\StoredReport;
|
||||
use App\Support\Evidence\EvidenceCompletenessState;
|
||||
use App\Support\Evidence\EvidenceSnapshotStatus;
|
||||
use App\Support\OperationRunOutcome;
|
||||
use App\Support\OperationRunStatus;
|
||||
use App\Support\ReviewPackStatus;
|
||||
use App\Support\Workspaces\WorkspaceContext;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
use Livewire\Livewire;
|
||||
|
||||
uses(RefreshDatabase::class);
|
||||
|
||||
it('Spec329 keeps the repo truth map present for evidence and audit disclosure', function (): void {
|
||||
$map = file_get_contents(repo_path('specs/329-evidence-audit-log-disclosure-productization/repo-truth-map.md'));
|
||||
|
||||
expect($map)->toBeString()
|
||||
->and($map)->toContain('Evidence Overview route')
|
||||
->and($map)->toContain('Audit Log route')
|
||||
->and($map)->toContain('Evidence Snapshots')
|
||||
->and($map)->toContain('Review Packs')
|
||||
->and($map)->toContain('Stored Reports / export artifacts')
|
||||
->and($map)->toContain('Actor/action/target/outcome/time')
|
||||
->and($map)->toContain('Diagnostics/raw metadata availability');
|
||||
});
|
||||
|
||||
it('Spec329 renders Evidence Overview as a proof-first disclosure surface', function (): void {
|
||||
[$user, $environment, $snapshot, $reviewPack, $storedReport, $run] = spec329EvidenceFixture();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
||||
->get(route('admin.evidence.overview'))
|
||||
->assertOk()
|
||||
->assertSee('What proof is available for this scope?')
|
||||
->assertSee('Evidence proof workbench')
|
||||
->assertSee('Evidence path')
|
||||
->assertSee('Evidence snapshot')
|
||||
->assertSee('Review pack')
|
||||
->assertSee('Operation proof')
|
||||
->assertSee('Stored report / export')
|
||||
->assertSee('Spec 329 Demo Tenant - Produktion')
|
||||
->assertSee('Proof incomplete')
|
||||
->assertSee('A proof record exists, but no usable captured evidence is available yet.')
|
||||
->assertSee('Primary evidence snapshot is empty.')
|
||||
->assertSee('Supporting proof exists through the review pack, stored report, and operation record.')
|
||||
->assertSee('Empty')
|
||||
->assertSee('Ready')
|
||||
->assertSee('Available')
|
||||
->assertSee('Search evidence or next step')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Evidence inventory')
|
||||
->assertSee(EvidenceSnapshotResource::getUrl('view', ['record' => $snapshot], tenant: $environment, panel: 'admin'), false)
|
||||
->assertSee(ReviewPackResource::getUrl('view', ['record' => $reviewPack], tenant: $environment, panel: 'admin'), false)
|
||||
->assertSee(StoredReportResource::getUrl('view', ['record' => $storedReport], tenant: $environment, panel: 'admin'), false)
|
||||
->assertSee(\App\Support\OperationRunLinks::tenantlessView($run), false)
|
||||
->assertDontSee('The artifact row exists, but it does not contain usable captured content.')
|
||||
->assertDontSee('artifact row exists')
|
||||
->assertDontSee('Search tenant or next')
|
||||
->assertDontSee('Empty...')
|
||||
->assertDontSee('Re...')
|
||||
->assertDontSee('raw payload should stay hidden')
|
||||
->assertDontSee('provider secret should stay hidden')
|
||||
->assertDontSee('stack trace should stay hidden')
|
||||
->assertDontSee('debug metadata should stay hidden')
|
||||
->assertDontSee('internal exception should stay hidden')
|
||||
->assertDontSee('current tenant')
|
||||
->assertDontSee('tenant filter')
|
||||
->assertDontSee('entitled tenant')
|
||||
->assertDontSee('all tenants')
|
||||
->assertDontSee('production tenant');
|
||||
|
||||
session()->put(WorkspaceContext::SESSION_KEY, (int) $environment->workspace_id);
|
||||
|
||||
$component = Livewire::actingAs($user)->test(EvidenceOverview::class);
|
||||
|
||||
expect($component->instance()->getTable()->getSearchPlaceholder())
|
||||
->toBe('Search evidence or next step')
|
||||
->and((string) file_get_contents(app_path('Filament/Pages/Monitoring/EvidenceOverview.php')))
|
||||
->not->toContain('Search tenant or next');
|
||||
});
|
||||
|
||||
it('Spec329 renders Audit Log as an event-proof-first disclosure surface', function (): void {
|
||||
[$user, $environment, $audit, $run] = spec329AuditFixture();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
||||
->get(route('admin.monitoring.audit-log', ['event' => (int) $audit->getKey()]))
|
||||
->assertOk()
|
||||
->assertSee('Which event proves what happened?')
|
||||
->assertSee('Audit proof workbench')
|
||||
->assertSee('Event proof')
|
||||
->assertSee('Actor')
|
||||
->assertSee('Action')
|
||||
->assertSee('Target')
|
||||
->assertSee('Outcome')
|
||||
->assertSee('Time')
|
||||
->assertSee('Related proof')
|
||||
->assertSee('Diagnostics - Collapsed')
|
||||
->assertSee('Audit event history')
|
||||
->assertSee('Spec329 Operator')
|
||||
->assertSee('Permission posture checked')
|
||||
->assertSee('Permission posture report')
|
||||
->assertSee(\App\Support\OperationRunLinks::tenantlessView($run), false)
|
||||
->assertDontSee('raw payload should stay hidden')
|
||||
->assertDontSee('provider secret should stay hidden')
|
||||
->assertDontSee('stack trace should stay hidden')
|
||||
->assertDontSee('debug metadata should stay hidden')
|
||||
->assertDontSee('internal exception should stay hidden')
|
||||
->assertDontSee('provider response should stay hidden')
|
||||
->assertDontSee('current tenant')
|
||||
->assertDontSee('tenant filter')
|
||||
->assertDontSee('entitled tenant')
|
||||
->assertDontSee('all tenants')
|
||||
->assertDontSee('production tenant');
|
||||
});
|
||||
|
||||
it('Spec329 keeps audit selected event state inside the active environment filter', function (): void {
|
||||
[$user, $environmentA, $environmentB, $auditA, $auditB] = spec329FilteredAuditFixture();
|
||||
|
||||
Livewire::withQueryParams([
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
'event' => (int) $auditB->getKey(),
|
||||
])
|
||||
->actingAs($user)
|
||||
->test(AuditLogPage::class)
|
||||
->assertSee('Environment filter:')
|
||||
->assertSee('Which event proves what happened?')
|
||||
->assertSet('tableFilters.managed_environment_id.value', (string) $environmentA->getKey())
|
||||
->assertSet('selectedAuditLogId', null)
|
||||
->assertCanSeeTableRecords([$auditA])
|
||||
->assertCanNotSeeTableRecords([$auditB]);
|
||||
|
||||
Livewire::withQueryParams([
|
||||
'environment_id' => (int) $environmentA->getKey(),
|
||||
'event' => (int) $auditA->getKey(),
|
||||
])
|
||||
->actingAs($user)
|
||||
->test(AuditLogPage::class)
|
||||
->assertSet('selectedAuditLogId', (int) $auditA->getKey())
|
||||
->assertSee('Event proof')
|
||||
->assertSee('Audit A proof');
|
||||
});
|
||||
|
||||
it('Spec329 rejects cross-workspace environment filters for both disclosure pages', function (): void {
|
||||
$environment = ManagedEnvironment::factory()->active()->create();
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner');
|
||||
$foreignEnvironment = ManagedEnvironment::factory()->active()->create();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
||||
->get(route('admin.evidence.overview', ['environment_id' => (int) $foreignEnvironment->getKey()]))
|
||||
->assertNotFound();
|
||||
|
||||
$this->actingAs($user)
|
||||
->withSession([WorkspaceContext::SESSION_KEY => (int) $environment->workspace_id])
|
||||
->get(route('admin.monitoring.audit-log', ['environment_id' => (int) $foreignEnvironment->getKey()]))
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
/**
|
||||
* @return array{0: \App\Models\User, 1: ManagedEnvironment, 2: EvidenceSnapshot, 3: ReviewPack, 4: StoredReport, 5: OperationRun}
|
||||
*/
|
||||
function spec329EvidenceFixture(): array
|
||||
{
|
||||
$environment = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => 'Spec 329 Demo Tenant - Produktion',
|
||||
]);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
$run = OperationRun::factory()->forTenant($environment)->create([
|
||||
'type' => 'tenant.evidence.snapshot.generate',
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
'context' => [
|
||||
'raw_payload' => 'raw payload should stay hidden',
|
||||
'provider_secret' => 'provider secret should stay hidden',
|
||||
'stack_trace' => 'stack trace should stay hidden',
|
||||
'debug_metadata' => 'debug metadata should stay hidden',
|
||||
'internal_exception' => 'internal exception should stay hidden',
|
||||
],
|
||||
]);
|
||||
|
||||
$snapshot = EvidenceSnapshot::query()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'status' => EvidenceSnapshotStatus::Active->value,
|
||||
'completeness_state' => EvidenceCompletenessState::Complete->value,
|
||||
'summary' => [
|
||||
'missing_dimensions' => 0,
|
||||
'stale_dimensions' => 0,
|
||||
'raw_payload' => 'raw payload should stay hidden',
|
||||
],
|
||||
'generated_at' => now(),
|
||||
'expires_at' => now()->addDays(30),
|
||||
]);
|
||||
|
||||
$reviewPack = ReviewPack::factory()->ready()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'evidence_snapshot_id' => (int) $snapshot->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'status' => ReviewPackStatus::Ready->value,
|
||||
]);
|
||||
|
||||
$storedReport = StoredReport::factory()->permissionPosture()->create([
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'fingerprint' => 'spec329-evidence-report',
|
||||
]);
|
||||
|
||||
return [$user, $environment, $snapshot, $reviewPack, $storedReport, $run];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: \App\Models\User, 1: ManagedEnvironment, 2: AuditLog, 3: OperationRun}
|
||||
*/
|
||||
function spec329AuditFixture(): array
|
||||
{
|
||||
$environment = ManagedEnvironment::factory()->active()->create([
|
||||
'name' => 'Spec329 Audit Environment',
|
||||
]);
|
||||
[$user, $environment] = createUserWithTenant(tenant: $environment, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
$run = OperationRun::factory()->forTenant($environment)->create([
|
||||
'type' => 'permission_posture.check',
|
||||
'status' => OperationRunStatus::Completed->value,
|
||||
'outcome' => OperationRunOutcome::Succeeded->value,
|
||||
]);
|
||||
|
||||
$audit = AuditLog::query()->create([
|
||||
'workspace_id' => (int) $environment->workspace_id,
|
||||
'managed_environment_id' => (int) $environment->getKey(),
|
||||
'operation_run_id' => (int) $run->getKey(),
|
||||
'actor_email' => 'spec329@example.test',
|
||||
'actor_name' => 'Spec329 Operator',
|
||||
'actor_type' => 'human',
|
||||
'action' => 'permission_posture.checked',
|
||||
'status' => 'success',
|
||||
'resource_type' => 'stored_report',
|
||||
'resource_id' => '329',
|
||||
'target_label' => 'Permission posture report',
|
||||
'summary' => 'Permission posture checked',
|
||||
'metadata' => [
|
||||
'reason' => 'Evidence review',
|
||||
'raw_payload' => 'raw payload should stay hidden',
|
||||
'provider_secret' => 'provider secret should stay hidden',
|
||||
'stack_trace' => 'stack trace should stay hidden',
|
||||
'debug_metadata' => 'debug metadata should stay hidden',
|
||||
'internal_exception' => 'internal exception should stay hidden',
|
||||
'provider_response' => 'provider response should stay hidden',
|
||||
],
|
||||
'recorded_at' => now(),
|
||||
]);
|
||||
|
||||
return [$user, $environment, $audit, $run];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{0: \App\Models\User, 1: ManagedEnvironment, 2: ManagedEnvironment, 3: AuditLog, 4: AuditLog}
|
||||
*/
|
||||
function spec329FilteredAuditFixture(): array
|
||||
{
|
||||
$environmentA = ManagedEnvironment::factory()->active()->create(['name' => 'Spec329 Audit A']);
|
||||
[$user, $environmentA] = createUserWithTenant(tenant: $environmentA, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
$environmentB = ManagedEnvironment::factory()->active()->create([
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'name' => 'Spec329 Audit B',
|
||||
]);
|
||||
createUserWithTenant(tenant: $environmentB, user: $user, role: 'owner', workspaceRole: 'owner');
|
||||
|
||||
$auditA = AuditLog::query()->create([
|
||||
'workspace_id' => (int) $environmentA->workspace_id,
|
||||
'managed_environment_id' => (int) $environmentA->getKey(),
|
||||
'actor_email' => 'spec329-a@example.test',
|
||||
'actor_name' => 'Spec329 A',
|
||||
'actor_type' => 'human',
|
||||
'action' => 'workspace.selected',
|
||||
'status' => 'success',
|
||||
'resource_type' => 'workspace',
|
||||
'resource_id' => (string) $environmentA->workspace_id,
|
||||
'target_label' => 'Workspace A',
|
||||
'summary' => 'Audit A proof',
|
||||
'recorded_at' => now(),
|
||||
]);
|
||||
|
||||
$auditB = AuditLog::query()->create([
|
||||
'workspace_id' => (int) $environmentB->workspace_id,
|
||||
'managed_environment_id' => (int) $environmentB->getKey(),
|
||||
'actor_email' => 'spec329-b@example.test',
|
||||
'actor_name' => 'Spec329 B',
|
||||
'actor_type' => 'human',
|
||||
'action' => 'workspace.selected',
|
||||
'status' => 'success',
|
||||
'resource_type' => 'workspace',
|
||||
'resource_id' => (string) $environmentB->workspace_id,
|
||||
'target_label' => 'Workspace B',
|
||||
'summary' => 'Audit B proof',
|
||||
'recorded_at' => now()->addMinute(),
|
||||
]);
|
||||
|
||||
return [$user, $environmentA, $environmentB, $auditA, $auditB];
|
||||
}
|
||||
@ -302,16 +302,20 @@ function spec321QueryKeys(string $url): array
|
||||
->assertNotFound();
|
||||
});
|
||||
|
||||
it('alerts_and_audit_log_sidebar_entry_is_workspace_wide', function (): void {
|
||||
it('alerts_evidence_overview_and_audit_log_sidebar_entries_are_workspace_wide', function (): void {
|
||||
[, $environmentA] = spec321WorkspaceFixture();
|
||||
|
||||
$alertUrl = route('filament.admin.alerts');
|
||||
$evidenceUrl = route('admin.evidence.overview');
|
||||
$auditUrl = route('admin.monitoring.audit-log');
|
||||
|
||||
expect(WorkspaceHubRegistry::cleanUrl($alertUrl))->toBe($alertUrl)
|
||||
->and(WorkspaceHubRegistry::cleanUrl($evidenceUrl))->toBe($evidenceUrl)
|
||||
->and(WorkspaceHubRegistry::cleanUrl($auditUrl))->toBe($auditUrl)
|
||||
->and(WorkspaceHubRegistry::cleanUrl(route('filament.admin.alerts', ['environment_id' => (int) $environmentA->getKey()])))
|
||||
->toBe($alertUrl)
|
||||
->and(WorkspaceHubRegistry::cleanUrl(route('admin.evidence.overview', ['environment_id' => (int) $environmentA->getKey()])))
|
||||
->toBe($evidenceUrl)
|
||||
->and(WorkspaceHubRegistry::cleanUrl(route('admin.monitoring.audit-log', ['environment_id' => (int) $environmentA->getKey()])))
|
||||
->toBe($auditUrl);
|
||||
});
|
||||
|
||||
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 169 KiB |
|
After Width: | Height: | Size: 222 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 200 KiB |
|
After Width: | Height: | Size: 205 KiB |
|
After Width: | Height: | Size: 195 KiB |
@ -0,0 +1,65 @@
|
||||
# Requirements Checklist: Spec 329 - Evidence / Audit Log Disclosure Productization
|
||||
|
||||
**Purpose**: Validate preparation artifact quality before implementation.
|
||||
**Created**: 2026-05-19
|
||||
**Feature**: `specs/329-evidence-audit-log-disclosure-productization/spec.md`
|
||||
|
||||
## Content Quality
|
||||
|
||||
- [x] No implementation details leak into product requirements beyond required repo constraints.
|
||||
- [x] User value and operator/auditor workflow are clear.
|
||||
- [x] Scope is bounded to two existing runtime surfaces.
|
||||
- [x] Non-goals explicitly prevent backend/workflow overbuild.
|
||||
- [x] Dependencies and historical specs are listed.
|
||||
|
||||
## Repo Truth And Safety
|
||||
|
||||
- [x] Existing route/class/view/partial paths are named.
|
||||
- [x] Repo truth map exists and uses required classifications.
|
||||
- [x] No new persisted truth is proposed.
|
||||
- [x] No migrations/packages/env/queues/scheduler/storage changes are expected.
|
||||
- [x] No legacy tenant query alias support is allowed.
|
||||
- [x] No false immutability/certification/compliance/health claims are allowed.
|
||||
|
||||
## Workspace / Environment Contract
|
||||
|
||||
- [x] Clean workspace-wide entry is specified.
|
||||
- [x] Canonical `environment_id` filter is specified.
|
||||
- [x] Visible chip and clear filter are specified.
|
||||
- [x] Legacy aliases are rejected.
|
||||
- [x] Cross-workspace environment guard is specified.
|
||||
- [x] Audit route shell/middleware drift is called out for implementation verification.
|
||||
|
||||
## RBAC / Audit / Diagnostics
|
||||
|
||||
- [x] Existing capabilities and policies remain authoritative.
|
||||
- [x] Unauthorized action behavior is specified.
|
||||
- [x] Diagnostics are collapsed/hidden by default.
|
||||
- [x] Dangerous actions are out of scope unless spec/plan are updated.
|
||||
- [x] No raw payloads/provider secrets/debug traces are default-visible.
|
||||
- [x] Audit event first-read fields are specified.
|
||||
- [x] Evidence path first-read fields are specified.
|
||||
|
||||
## Testability
|
||||
|
||||
- [x] Feature tests are listed.
|
||||
- [x] Browser smoke flows are listed.
|
||||
- [x] Navigation/scope guard tests are listed.
|
||||
- [x] `pint --dirty` and `git diff --check` are listed.
|
||||
- [x] Full-suite status must be reported honestly.
|
||||
|
||||
## Surface Guardrail Review
|
||||
|
||||
- [x] UI Surface Impact is completed and not contradicted by no-impact wording.
|
||||
- [x] Decision-first role is classified for both pages.
|
||||
- [x] Audience-aware disclosure hierarchy is explicit.
|
||||
- [x] OperationRun link-only impact is explicit.
|
||||
- [x] Provider boundary posture is explicit.
|
||||
- [x] Test lane and browser family are explicit.
|
||||
|
||||
## Readiness Decision
|
||||
|
||||
- [x] Spec is ready for implementation planning.
|
||||
- [x] No open question blocks a bounded implementation loop.
|
||||
- [x] Review outcome class: acceptable-special-case.
|
||||
- [x] Workflow outcome: keep.
|
||||
379
specs/329-evidence-audit-log-disclosure-productization/plan.md
Normal file
@ -0,0 +1,379 @@
|
||||
# Implementation Plan: Spec 329 - Evidence / Audit Log Disclosure Productization
|
||||
|
||||
**Branch**: `329-evidence-audit-log-disclosure-productization` | **Date**: 2026-05-19 | **Spec**: `specs/329-evidence-audit-log-disclosure-productization/spec.md`
|
||||
**Input**: User-provided Spec 329 and repo inspection.
|
||||
|
||||
## Summary
|
||||
|
||||
Productize the existing Evidence Overview and Audit Log into proof-first and event-proof-first disclosure surfaces. The implementation must keep current routes, source truth, RBAC, and workspace/environment contracts, introduce no backend foundation, and make the first viewport answer:
|
||||
|
||||
```text
|
||||
What proof is available for this scope?
|
||||
Which event proves what happened?
|
||||
```
|
||||
|
||||
Evidence Overview will elevate proof availability, freshness, evidence path, review/export/report state, and operation proof before its inventory table. Audit Log will elevate actor/action/target/outcome/time, selected/latest event proof, and related proof before raw metadata and the event table. Diagnostics and raw metadata stay collapsed and capability-aware.
|
||||
|
||||
## Implementation Close-Out
|
||||
|
||||
Implemented on 2026-05-19. The runtime change stayed inside the existing Evidence Overview and Audit Log routes/pages, added the existing Evidence Overview route to the Workspace Monitoring sidebar with the concise `Evidence` / `Nachweise` navigation label, removed the duplicated Evidence Overview route registration, kept the existing tables as secondary context, and added targeted Feature plus Pest Browser coverage. No route/archetype/coverage classification changed, so UI registry documents were not updated; the active spec package carries close-out proof through `repo-truth-map.md`, tasks, tests, and screenshots.
|
||||
|
||||
Post-review UI corrections on 2026-05-19 keep dynamic Environment display names unchanged even when they contain `Tenant`, replace implementation-heavy empty-snapshot copy with product-safe proof language, add an explicit `Proof incomplete` hierarchy for empty primary snapshots, keep right-panel Evidence Path badge labels short and unclipped (`Empty`, `Ready`, `Available`), and replace the static table search placeholder with `Search evidence or next step`.
|
||||
|
||||
## Technical Context
|
||||
|
||||
**Language/Version**: PHP 8.4.15, Laravel 12.52.0.
|
||||
**Primary Dependencies**: Filament 5.2.1, Livewire 4.1.4, Pest 4.3.1, Tailwind CSS 4.2.2.
|
||||
**Storage**: PostgreSQL; no schema change expected.
|
||||
**Testing**: Pest 4 Feature/Livewire/Browser tests.
|
||||
**Validation Lanes**: confidence and browser; targeted navigation guard tests.
|
||||
**Target Platform**: Laravel Sail locally; Dokploy/container deployment posture unchanged.
|
||||
**Project Type**: Laravel monolith under `apps/platform`.
|
||||
**Performance Goals**: DB-only page render; no Graph/provider API calls during render; no broad new query family beyond existing source queries unless bounded/eager-loaded.
|
||||
**Constraints**: No new persisted truth, migration, package, queue, scheduler, storage, env var, deployment asset, compatibility route, or legacy alias support.
|
||||
**Scale/Scope**: Two existing Filament pages, their views/partial, feature-local payload helpers if needed, focused tests, and browser smoke.
|
||||
|
||||
## UI / Surface Guardrail Plan
|
||||
|
||||
- **Guardrail scope**: changed existing operator-facing strategic surfaces.
|
||||
- **Affected routes/pages/actions/states/navigation/panel/provider surfaces**:
|
||||
- `/admin/evidence/overview`
|
||||
- `/admin/audit-log`
|
||||
- `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php`
|
||||
- `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php`
|
||||
- `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php`
|
||||
- `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php`
|
||||
- `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php`
|
||||
- **No-impact class, if applicable**: N/A.
|
||||
- **Native vs custom classification summary**: Native Filament pages/tables plus existing Blade composition; no new UI framework.
|
||||
- **Shared-family relevance**: evidence/report viewers, audit event detail, status messaging, proof links, OperationRun links, workspace/environment filter chip, diagnostics disclosure.
|
||||
- **State layers in scope**: page payload, URL query (`environment_id`, `event`, `supportAccess` where existing), table state, selected audit event state, diagnostics disclosure.
|
||||
- **Audience modes in scope**: auditor, customer-adjacent reviewer, operator-MSP, manager, support reviewer where authorized.
|
||||
- **Decision/diagnostic/raw hierarchy plan**: proof/event first, evidence/context second, diagnostics collapsed third, raw/support hidden.
|
||||
- **Raw/support gating plan**: collapsed by default and capability-gated through existing support diagnostics capability where any raw metadata is exposed.
|
||||
- **One-primary-action / duplicate-truth control**: each workbench owns one proof/open next action; table and raw/detail helpers remain secondary.
|
||||
- **Handling modes by drift class or surface**: review-mandatory for UI-025 and UI-044 strategic surfaces; document-in-feature for any UI coverage registry no-change decision.
|
||||
- **Repository-signal treatment**: Spec 325 target images are visual direction only; runtime claims must be repo-verified or unavailable.
|
||||
- **Special surface test profiles**: `global-context-shell`, `monitoring-state-page`, `shared-detail-family`.
|
||||
- **Required tests or manual smoke**: Feature/Livewire tests for layout/RBAC/scope/disclosure plus Pest Browser smoke for clean/filtered/clear/reload/non-empty/empty/diagnostics/table-secondary behavior.
|
||||
- **Exception path and spread control**: none expected. Any new dangerous action, export engine, schema, capability, or raw-disclosure mechanism requires spec/plan update first.
|
||||
- **Active feature PR close-out entry**: Smoke Coverage.
|
||||
- **UI/Productization coverage decision**: active spec package carries productization proof. Update UI coverage registry only if route/archetype/coverage classification changes; otherwise document why UI-025/UI-044 plus Spec 329 artifacts are sufficient.
|
||||
- **Coverage artifacts to update**: none expected unless implementation changes route/archetype state.
|
||||
- **Navigation / Filament provider-panel handling**: no panel provider registration changes expected. Laravel 12 panel providers remain in `apps/platform/bootstrap/providers.php`.
|
||||
- **Navigation update**: add the existing Evidence Overview route to the Workspace Monitoring sidebar through the manual `WorkspaceSidebarNavigation` path and the admin panel's default workspace navigation items using a concise area label; no panel provider registration change.
|
||||
- **Screenshot or page-report need**: screenshots required; full page report optional unless implementation materially changes coverage classification.
|
||||
|
||||
## Shared Pattern & System Fit
|
||||
|
||||
- **Cross-cutting feature marker**: yes.
|
||||
- **Systems touched**: Evidence/Audit pages, EvidenceSnapshot/ReviewPack/StoredReport/OperationRun/AuditLog models, resource policies, `OperationRunLinks`, `RelatedNavigationResolver`, `BadgeRenderer`, `ArtifactTruthPresenter`, workspace hub filter/reset helpers.
|
||||
- **Shared abstractions reused**: existing policies/capabilities, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CanonicalAdminEnvironmentFilterState`, `OperationRunLinks`, `RelatedNavigationResolver`, `BadgeRenderer`, `ArtifactTruthPresenter`.
|
||||
- **New abstraction introduced? why?**: none. Page-local private helpers only if needed to keep pages/views reviewable.
|
||||
- **Why the existing abstraction was sufficient or insufficient**: existing paths already provide truth, authorization, related links, and filters. They do not currently impose the proof-first/event-proof-first hierarchy.
|
||||
- **Bounded deviation / spread control**: no public reusable disclosure system; keep presentation local to these two surfaces.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: link/proof presentation only.
|
||||
- **Central contract reused**: `OperationRunLinks`, `OperationRunUrl`, existing OperationRun policies and detail routes.
|
||||
- **Delegated UX behaviors**: open operation/proof links only where existing link helpers and authorization allow.
|
||||
- **Surface-owned behavior kept local**: proof availability labels and unavailable states.
|
||||
- **Queued DB-notification policy**: unchanged / N/A.
|
||||
- **Terminal notification path**: unchanged.
|
||||
- **Exception path**: none.
|
||||
|
||||
## Provider Boundary & Portability Fit
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no new provider seam.
|
||||
- **Provider-owned seams**: existing Microsoft/Entra/Intune terms only where existing source records use them.
|
||||
- **Platform-core seams**: workspace, environment, evidence, audit, proof, operation, report, disclosure.
|
||||
- **Neutral platform terms / contracts preserved**: workspace, environment, actor, action, target, outcome, time, proof, diagnostics, raw metadata.
|
||||
- **Retained provider-specific semantics and why**: provider-specific report or audit target copy may remain where source data is explicitly provider-bound.
|
||||
- **Bounded extraction or follow-up path**: none for Spec 329.
|
||||
|
||||
## Constitution Check
|
||||
|
||||
- **Inventory-first, snapshots-second**: Evidence snapshots remain explicit artifact truth. No new snapshot or inventory persistence is introduced.
|
||||
- **Read/write separation by default**: Pages remain read-first. Any unexpected mutation or destructive action requires spec/plan update, confirmation, authorization, audit, notification, and tests.
|
||||
- **Single Contract Path to Graph**: No Graph/provider API calls may be added to page render.
|
||||
- **Deterministic Capabilities**: Reuse existing `Capabilities`, `CapabilityResolver`, `WorkspaceCapabilityResolver`, resource policies, and report-type capability mapping.
|
||||
- **Proportionality / anti-bloat**: No new source of truth, persisted entity, enum/status family, public abstraction, proof engine, or cross-domain UI framework.
|
||||
- **Workspace isolation**: Clean URLs stay workspace-wide. `environment_id` resolves through current workspace and actor entitlement.
|
||||
- **Tenant/environment language**: Runtime copy must avoid tenant as platform context. Provider-specific tenant wording is allowed only where explicitly external/provider-bound.
|
||||
- **OperationRun UX**: Deep links only through existing OperationRun link helpers; no operation start or lifecycle changes.
|
||||
- **UI-COV-001**: Existing strategic surfaces UI-025 and UI-044 change. Active spec package must carry repo-truth map, tests, and browser screenshots; implementation close-out must decide whether route inventory/coverage matrix updates are needed.
|
||||
- **TEST-GOV-001**: Targeted Feature and Browser tests are explicit; no broad heavy-governance lane unless implementation reveals structural risk.
|
||||
- **Filament-native UI**: Use native Filament components and shared primitives first; custom Blade must preserve Filament visual language, accessibility, and disclosure hierarchy.
|
||||
- **Filament v5 / Livewire v4**: Livewire v4.0+ compliance required. No Livewire v3 or Filament v3/v4 APIs.
|
||||
|
||||
## Current Repo Truth Summary
|
||||
|
||||
Existing verified surfaces:
|
||||
|
||||
- `EvidenceOverview` is a Filament `Page` at `/admin/evidence/overview`, with an existing table over latest active accessible `EvidenceSnapshot` records.
|
||||
- Evidence page currently uses `EvidenceSnapshot`, `EnvironmentReview`, `ArtifactTruthPresenter`, `EvidenceSnapshotResource` links, `WorkspaceHubEnvironmentFilter`, and clear/reset helpers.
|
||||
- `AuditLog` is a Filament `Page` at `/admin/audit-log`, with an existing table over scoped `AuditLog` records, event selection through `event`, support-access filter, related navigation links, and environment filter chip.
|
||||
- `AuditLog` model derives actor snapshots, target snapshots, outcome labels, readable context items, and technical metadata.
|
||||
- `AuditLog` selected-event partial currently renders `Technical metadata` directly when an event is selected; Spec 329 must move that behind collapsed/capability-aware disclosure.
|
||||
- `EvidenceSnapshot`, `ReviewPack`, and `AuditLog` have `operationRun()` relations. `OperationRunLinks::related()` already maps evidence snapshot and review pack generation runs to artifact links.
|
||||
- `StoredReportResource` supports permission posture and Entra admin role report types with capability checks and disabled global search.
|
||||
- `WorkspaceHubEnvironmentFilter::fromRequest()` accepts canonical `environment_id`, scopes to current workspace, checks actor access, and rejects inaccessible/cross-workspace IDs.
|
||||
- Navigation tests already cover canonical environment filter, clear filter, legacy alias rejection, and workspace hub no-drift behavior for several related surfaces.
|
||||
|
||||
Known productization gaps:
|
||||
|
||||
- Evidence Overview is table-first and does not yet show a proof readiness workbench, evidence path panel, export/report availability panel, or collapsed diagnostics affordance.
|
||||
- Audit Log is summary-first but not yet event-proof-first; actor/action/target/outcome/time should dominate the first-read, and raw technical metadata must not be default-visible.
|
||||
- Current Audit Log route middleware includes `ensure-environment-context-selected`; implementation must verify this does not force Environment shell ownership or remembered Environment fallback.
|
||||
- `routes/web.php` contains a duplicated `/admin/evidence/overview` route registration; implementation may document or clean this only if safe and in scope.
|
||||
|
||||
## Existing Repository Surfaces Likely Affected
|
||||
|
||||
Runtime files, only during later implementation:
|
||||
|
||||
- `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php`
|
||||
- `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php`
|
||||
- `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php`
|
||||
- `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php`
|
||||
- `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php`
|
||||
- `apps/platform/resources/lang/en/*` and `apps/platform/resources/lang/de/*` only if surrounding page-copy conventions require localized strings.
|
||||
|
||||
Tests, only during later implementation:
|
||||
|
||||
- `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php`
|
||||
- `apps/platform/tests/Feature/Monitoring/EvidenceOverviewWorkspaceHubContractTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/EvidenceOverviewDerivedStateMemoizationTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/AuditLogPageTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/AuditLogDetailInspectionTest.php`
|
||||
- `apps/platform/tests/Feature/Filament/AuditLogAuthorizationTest.php`
|
||||
- `apps/platform/tests/Feature/Monitoring/AuditLogInspectFlowTest.php`
|
||||
- `apps/platform/tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php`
|
||||
- `apps/platform/tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php`
|
||||
- `apps/platform/tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php`
|
||||
|
||||
Spec/UI artifacts:
|
||||
|
||||
- `specs/329-evidence-audit-log-disclosure-productization/repo-truth-map.md`
|
||||
- screenshot artifacts under `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/`
|
||||
- optional UI coverage registry updates only if implementation materially changes route/archetype/coverage state.
|
||||
|
||||
## Domain / Model Implications
|
||||
|
||||
- No new model, table, migration, enum, status family, source of truth, or persisted display state.
|
||||
- Evidence proof state must derive from:
|
||||
- `EvidenceSnapshot.status`, `completeness_state`, `summary`, `generated_at`, `expires_at`.
|
||||
- `EvidenceSnapshot::items()`, `reviewPacks()`, `environmentReviews()`, and `operationRun()`.
|
||||
- `ReviewPack.status`, `generated_at`, `expires_at`, `file_size`, and related review/snapshot/run.
|
||||
- `StoredReport.report_type`, `payload`, `fingerprint`, report-type capability, and environment/workspace scope.
|
||||
- Existing finding exception evidence references where linked and authorized.
|
||||
- Audit proof state must derive from:
|
||||
- `AuditLog` actor snapshot, action, target snapshot, normalized outcome, recorded time, managed environment/workspace scope, operation run relation, readable context, and related navigation resolver.
|
||||
- If exact evidence, report, export, operation, risk/decision, or proof link is missing, render explicit unavailable/missing/not generated/not applicable state.
|
||||
|
||||
## UI / Filament Implications
|
||||
|
||||
- Filament v5 and Livewire v4.0+ compliance must be preserved.
|
||||
- Panel providers remain registered in `apps/platform/bootstrap/providers.php`; no panel provider changes expected.
|
||||
- No globally searchable resource is added or changed. Related resources must remain disabled for global search or backed by safe View/Edit pages.
|
||||
- Use Filament sections/tables/actions and shared badge/filter primitives where suitable.
|
||||
- Avoid fake charts, fake compliance readiness, fake immutable/certified badges, and generic KPI dashboards.
|
||||
- Main Evidence structure:
|
||||
- header/scope
|
||||
- proof readiness workbench
|
||||
- evidence path panel
|
||||
- export/report availability panel
|
||||
- evidence inventory/table as secondary context
|
||||
- collapsed diagnostics disclosure
|
||||
- Main Audit structure:
|
||||
- header/scope
|
||||
- audit proof workbench
|
||||
- selected/latest event proof panel
|
||||
- actor/action/target/outcome/time first-read
|
||||
- audit event table as secondary context
|
||||
- collapsed raw metadata/diagnostics disclosure
|
||||
- Right-side proof/disclosure panel should be desktop aside and mobile stack where practical.
|
||||
|
||||
## Livewire / Page State Implications
|
||||
|
||||
- Evidence clean entry must clear remembered/stale Environment-like table filters and session state.
|
||||
- Audit clean entry must clear remembered/stale Environment-like table filters and session state.
|
||||
- `environment_id` query state remains the only shareable environment filter key.
|
||||
- Audit `event` query remains selected-event state and must be normalized against current query/table filters and authorization.
|
||||
- `supportAccess` may remain existing Audit Log state if it does not conflict with disclosure hierarchy.
|
||||
- Clear filter must remove `environment_id` and environment-like table/session state through existing helpers.
|
||||
|
||||
## RBAC / Policy Implications
|
||||
|
||||
Reuse existing authorization:
|
||||
|
||||
- Workspace page access through `WorkspaceContext` / `WorkspaceCapabilityResolver`.
|
||||
- Environment access through current accessible environment queries and `User::canAccessTenant()`.
|
||||
- Evidence visibility through `Capabilities::EVIDENCE_VIEW` and `EvidenceSnapshotPolicy`.
|
||||
- Review pack visibility/download through `Capabilities::REVIEW_PACK_VIEW`, `ReviewPackPolicy`, and existing download route authorization.
|
||||
- Stored report visibility through report-type capabilities in `StoredReportResource`.
|
||||
- Audit page access through `Capabilities::AUDIT_VIEW`.
|
||||
- Operation proof visibility through existing `OperationRunPolicy`, link helpers, and related resource policies.
|
||||
- Diagnostics/raw metadata through `Capabilities::SUPPORT_DIAGNOSTICS_VIEW` or stricter existing capability.
|
||||
|
||||
No new permission semantics should be added unless implementation proves existing capabilities cannot express the action and spec/plan/tasks are updated first.
|
||||
|
||||
## Audit / Evidence / Disclosure Implications
|
||||
|
||||
- No new audit event is required for read-only page rendering unless current page-open audit conventions are extended repo-wide.
|
||||
- Evidence should appear as proof path/state:
|
||||
- available
|
||||
- incomplete
|
||||
- stale
|
||||
- unavailable
|
||||
- not generated
|
||||
- not applicable
|
||||
- Audit should appear as event proof:
|
||||
- actor
|
||||
- action
|
||||
- target
|
||||
- outcome
|
||||
- time
|
||||
- scope
|
||||
- related proof
|
||||
- Do not show raw provider payloads, debug metadata, internal exception traces, provider secrets, raw OperationRun payloads, raw audit metadata blobs, or stack traces by default.
|
||||
- If diagnostics disclosure is present, it must be collapsed and capability-aware.
|
||||
|
||||
## Data / Migration Implications
|
||||
|
||||
Expected outcome:
|
||||
|
||||
- No migrations.
|
||||
- No seeders.
|
||||
- No data backfills.
|
||||
- No packages.
|
||||
- No env vars.
|
||||
- No queues/scheduler/storage changes.
|
||||
- No deployment asset changes.
|
||||
- No backwards compatibility layer.
|
||||
- No legacy tenant query alias support.
|
||||
|
||||
If implementation discovers an actual schema need, stop and update spec/plan/tasks/repo-truth-map first. Default decision remains no migration.
|
||||
|
||||
## Localization / Copy Implications
|
||||
|
||||
- Runtime copy must be concise, customer/auditor-safe, and operator-readable.
|
||||
- Stable visible strings should be EN/DE localized if current project pattern routes page copy through language files.
|
||||
- Avoid platform-context `tenant` wording. Use `Workspace` and `Environment` for shell/filter/product context.
|
||||
- Provider-bound tenant wording may remain only when describing an external Microsoft/Entra tenant identifier or provider payload outside the default decision view.
|
||||
|
||||
## Implementation Phases
|
||||
|
||||
### Phase 1 - Repo Truth And Current UI Audit
|
||||
|
||||
- Re-read spec, plan, tasks, and `repo-truth-map.md`.
|
||||
- Inspect current Evidence Overview, Audit Log, selected-event partial, models, policies, related links, and tests.
|
||||
- Update `repo-truth-map.md` before runtime changes if implementation discovers new source truth or gaps.
|
||||
- Confirm no migration/package/env/queue/storage need.
|
||||
|
||||
### Phase 2 - Tests First
|
||||
|
||||
- Add tests for repo truth map existence.
|
||||
- Add Feature/Livewire tests for evidence proof-first layout, audit event-proof-first layout, evidence path, raw metadata hidden, export/report availability, RBAC, canonical environment filter, legacy aliases, cross-workspace guard, and tenant-copy guard.
|
||||
|
||||
### Phase 3 - Evidence Overview Productization
|
||||
|
||||
- Refactor the existing page into proof-first layout.
|
||||
- Bind to existing evidence snapshot, review pack, stored report, operation proof, review/decision/risk sources where repo-supported.
|
||||
- Keep table available as secondary context.
|
||||
- Keep diagnostics collapsed and raw metadata hidden.
|
||||
|
||||
### Phase 4 - Audit Log Productization
|
||||
|
||||
- Refactor the existing page into event-proof-first layout.
|
||||
- Ensure actor/action/target/outcome/time/scope are first-read.
|
||||
- Move selected-event technical metadata behind collapsed/capability-aware disclosure.
|
||||
- Keep audit table available as secondary context.
|
||||
|
||||
### Phase 5 - Shared Disclosure UX
|
||||
|
||||
- Add consistent disclosure rule panel/affordance across both pages:
|
||||
- decision/proof visible
|
||||
- evidence/event visible
|
||||
- diagnostics collapsed
|
||||
- raw/support hidden
|
||||
- Show unavailable/deferred states honestly.
|
||||
|
||||
### Phase 6 - Scope / Filter Integration
|
||||
|
||||
- Preserve clean workspace-wide entry.
|
||||
- Preserve `?environment_id=` filter, visible chip, clear filter, reload/back/forward behavior.
|
||||
- Preserve legacy alias rejection and cross-workspace guard.
|
||||
- Verify Audit Log route middleware does not force Environment shell ownership.
|
||||
|
||||
### Phase 7 - Browser Smoke And Screenshots
|
||||
|
||||
- Add targeted Browser smoke for evidence clean/filtered/clear/reload/non-empty/empty, audit clean/filtered/clear/reload/non-empty/empty, diagnostics hidden, table secondary, and no platform-context tenant wording.
|
||||
- Save screenshots under the spec artifacts path when generated.
|
||||
|
||||
### Phase 8 - Validation And Close-Out
|
||||
|
||||
- Run targeted Feature/navigation tests, Browser smoke, filtered guard tests, `pint --dirty`, and `git diff --check`.
|
||||
- Report full suite status honestly if not run.
|
||||
- Record no migrations/seeders/packages/env/queues/scheduler/storage/deployment asset/backcompat/legacy alias support.
|
||||
|
||||
## Testing Strategy
|
||||
|
||||
Required tests:
|
||||
|
||||
- `it('documents_evidence_audit_log_repo_truth_map')`
|
||||
- `it('renders_evidence_overview_proof_first_layout')`
|
||||
- `it('renders_audit_log_event_proof_first_layout')`
|
||||
- `it('shows_evidence_path_without_raw_metadata_by_default')`
|
||||
- `it('shows_audit_actor_action_target_outcome_time_before_raw_metadata')`
|
||||
- `it('shows_export_or_report_availability_only_when_repo_supported')`
|
||||
- `it('hides_evidence_and_audit_raw_diagnostics_by_default')`
|
||||
- `it('respects_evidence_audit_and_diagnostics_capabilities')`
|
||||
- `it('evidence_overview_supports_canonical_environment_filter')`
|
||||
- `it('audit_log_supports_canonical_environment_filter')`
|
||||
- `it('evidence_and_audit_reject_legacy_environment_aliases')`
|
||||
- `it('evidence_and_audit_reject_cross_workspace_environment_filter')`
|
||||
- `it('evidence_and_audit_do_not_use_tenant_as_platform_context_copy')`
|
||||
- `tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php`
|
||||
|
||||
Required Browser smoke:
|
||||
|
||||
- Evidence Overview clean workspace.
|
||||
- Evidence Overview environment-filtered.
|
||||
- Evidence clear filter and reload.
|
||||
- Audit Log clean workspace.
|
||||
- Audit Log environment-filtered.
|
||||
- Audit clear filter and reload.
|
||||
- Evidence non-empty proof state.
|
||||
- Audit non-empty event state.
|
||||
- Evidence empty state.
|
||||
- Audit empty state.
|
||||
- Diagnostics hidden by default.
|
||||
- Tables remain secondary.
|
||||
- No platform-context tenant wording.
|
||||
|
||||
## Rollout / Deployment Considerations
|
||||
|
||||
- No env vars expected.
|
||||
- No migrations expected.
|
||||
- No queue/scheduler changes expected.
|
||||
- No storage/volume changes expected.
|
||||
- No deployment asset changes expected unless implementation registers new Filament assets, which is not expected. If assets are registered, deployment must include `cd apps/platform && php artisan filament:assets`.
|
||||
- Staging validation should include targeted Browser smoke for light mode, workspace/environment filter behavior, and disclosure hierarchy before production promotion.
|
||||
|
||||
## Risk Controls
|
||||
|
||||
- Do not implement before `repo-truth-map.md` exists.
|
||||
- Do not show any metric, proof state, export state, operation proof, review/risk link, or diagnostic affordance unless mapped to repo truth.
|
||||
- If a planned UI element has no safe source or authorization path, render unavailable/not generated/not applicable or omit it.
|
||||
- Do not introduce backend foundation to make a UI card true.
|
||||
- Do not support legacy query aliases.
|
||||
- Do not rewrite completed Specs 314-328.
|
||||
|
||||
## Candidate Selection Gate
|
||||
|
||||
Passed. The candidate was directly user-provided as Spec 329, explicitly deferred by Specs 326-328, not already present as an active/completed package, aligned with UI-025/UI-044 strategic surface coverage, and scoped to two existing proof/disclosure pages.
|
||||
|
||||
## Spec Readiness Gate
|
||||
|
||||
Expected pass after `spec.md`, `plan.md`, `tasks.md`, `repo-truth-map.md`, and `checklists/requirements.md` are created and preparation analysis has no blocking findings.
|
||||
@ -0,0 +1,120 @@
|
||||
# Spec 329 Repo Truth Map
|
||||
|
||||
Status: implemented
|
||||
Created: 2026-05-19
|
||||
Implemented: 2026-05-19
|
||||
Purpose: classify each Evidence Overview and Audit Log disclosure element before and after runtime implementation. This map is based on repository inspection and the Spec 329 implementation diff.
|
||||
|
||||
## Classification Legend
|
||||
|
||||
- `repo-verified`: exact runtime source exists and was inspected.
|
||||
- `foundation-real`: backend model/service/policy exists, but exact page binding still needs implementation verification.
|
||||
- `derived from existing model`: display value can be derived from existing persisted/domain truth.
|
||||
- `empty/unavailable state`: no safe source/action exists for v1; show explicit unavailable state or omit.
|
||||
- `deferred future capability`: outside Spec 329 and must not be shown as live runtime truth.
|
||||
|
||||
## Required Data Areas
|
||||
|
||||
| Data area | Repo source | Preparation finding |
|
||||
|---|---|---|
|
||||
| Evidence Overview route | `apps/platform/routes/web.php`, route `admin.evidence.overview` | repo-real path is `/admin/evidence/overview`; route appears duplicated and should be verified during implementation |
|
||||
| Workspace sidebar Evidence entry | `WorkspaceSidebarNavigation`, `AdminPanelProvider`, route `admin.evidence.overview` | repo-real sidebar entry under Monitoring links to the existing workspace-owned route |
|
||||
| Evidence Overview page | `EvidenceOverview` and `evidence-overview.blade.php` | repo-real current layout is scope text plus table |
|
||||
| Evidence Snapshots | `EvidenceSnapshot`, `EvidenceSnapshotResource`, `EvidenceSnapshotPolicy` | repo-real snapshot status, completeness, summary, generated/expiry timestamps, tenant/workspace scope, operation run relation, detail route |
|
||||
| Evidence Snapshot Items | `EvidenceSnapshotItem` relation | foundation-real item inventory for deeper proof path; raw item/payload detail must not be default-visible |
|
||||
| Review Packs | `ReviewPack`, `ReviewPackResource`, `ReviewPackPolicy`, `ReviewPackDownloadController` | repo-real statuses and detail/download surfaces; Evidence Overview currently does not expose pack availability |
|
||||
| Stored Reports / export artifacts | `StoredReport`, `StoredReportResource` | repo-real report types and capability-bound detail resources; no generic export engine is implied |
|
||||
| OperationRuns | `OperationRun`, `OperationRunLinks` | repo-real operation proof links for evidence snapshot/review pack generation where linked to run |
|
||||
| Audit Log route | `apps/platform/routes/web.php`, route `admin.monitoring.audit-log` | repo-real path is `/admin/audit-log`; middleware includes `ensure-environment-context-selected` and must be verified for workspace-hub shell safety |
|
||||
| Audit Log page | `AuditLog` and `audit-log.blade.php` | repo-real current page is summary-first history with selected-event detail |
|
||||
| Audit Log events | `AuditLog` model | repo-real actor/action/target/outcome/time/scope fields and derived snapshots |
|
||||
| Actor/action/target/outcome/time | `AuditLog::actorSnapshot()`, `targetSnapshot()`, `normalizedOutcome()`, `recorded_at`, `action` | repo-verified fields; action label via `AuditActionId::labelFor()` |
|
||||
| Risk/Decision links if present | `FindingException`, `FindingExceptionEvidenceReference`, `RelatedNavigationResolver` | foundation-real; only show where related route and authorization exist |
|
||||
| Customer Review Workspace evidence links | `CustomerReviewWorkspace`, `EvidenceSnapshotAuditLogTest`, review/evidence source query params | foundation-real context for evidence proof links; no redesign in Spec 329 |
|
||||
| Governance Inbox evidence links | `GovernanceInbox`, Spec 327 repo truth | foundation-real context only; no redesign in Spec 329 |
|
||||
| Operations proof links | `OperationRunLinks::tenantlessView()`, `OperationRunLinks::related()` | repo-real for operation proof/details and linked evidence/review pack artifacts |
|
||||
| Environment filter state | `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `ClearsWorkspaceHubEnvironmentFilterState`, `CanonicalAdminEnvironmentFilterState`, filter chip partial | repo-real canonical `environment_id`, clear filter, alias rejection, cross-workspace guard |
|
||||
| Diagnostics/raw metadata availability | `AuditLog::technicalMetadata()`, `AuditLog::metadata`, `OperationRun.context`, snapshot/report payload fields | repo-real raw/support sources exist but must stay collapsed/hidden and capability-aware |
|
||||
|
||||
## UI Element Map
|
||||
|
||||
| UI element | Surface | Source model/service/page | Status source | Authorization/capability | Workspace/Environment scope | OperationRun/evidence/audit/export link | Fallback/empty state | Classification |
|
||||
|---|---|---|---|---|---|---|---|---|
|
||||
| Evidence Overview route | Evidence Overview | `admin.evidence.overview` | route | workspace middleware + page access | current workspace | none | 404/workspace chooser per middleware | repo-verified |
|
||||
| Workspace sidebar Evidence entry | Workspace sidebar | `WorkspaceSidebarNavigation`, `AdminPanelProvider`, route `admin.evidence.overview` | static navigation item | workspace sidebar visibility | current workspace | `/admin/evidence/overview` | item absent only if sidebar group is unavailable | repo-verified |
|
||||
| Evidence Overview title/question | Evidence Overview | page/view stable copy | static copy | page access | workspace/filter | none | static title | repo-verified |
|
||||
| Workspace scope label | Evidence Overview | `WorkspaceContext` and shell | current workspace/session | workspace membership | workspace shell | none | 404 if unavailable | repo-verified |
|
||||
| Environment filter chip | Evidence Overview | `environmentFilterChip()`, shared chip partial | `WorkspaceHubEnvironmentFilter` + table state | actor must access environment | `?environment_id={id}` only | none | no chip on clean URL | repo-verified |
|
||||
| Clear filter action | Evidence Overview | `clearOverviewFilters()`, resetter | generated clean route | page access | removes canonical/table/session state | none | hidden when unfiltered | repo-verified |
|
||||
| Legacy alias rejection | Evidence Overview | `WorkspaceHubFilterStateResetter` + navigation tests | forbidden query/session keys | page access | aliases do not set filter | none | workspace-wide view or safe 404 | repo-verified |
|
||||
| Cross-workspace environment guard | Evidence Overview | `WorkspaceHubEnvironmentFilter::fromRequest()` and `normalizeTenantFilter()` | environment scoped by workspace/access | workspace and environment entitlement | current workspace only | none | 404 / safe no-access | repo-verified |
|
||||
| Proof readiness workbench | Evidence Overview | new page-local payload over existing rows | derived from latest accessible snapshots and related artifacts | evidence/report/review/run capabilities | current workspace/filter | evidence/review/report/operation links where authorized | `No evidence for this scope` | derived from existing model |
|
||||
| Evidence snapshot state | Evidence Overview | `EvidenceSnapshot.status`, `completeness_state`, `ArtifactTruthPresenter` | persisted fields + derived presenter | `evidence.view` and `EvidenceSnapshotPolicy` for links | current workspace/filter | `EvidenceSnapshotResource::getUrl('view')` | `Evidence snapshot unavailable` | repo-verified |
|
||||
| Evidence freshness | Evidence Overview | `generated_at`, `expires_at`, `ArtifactTruthPresenter` | timestamps and derived freshness | evidence visibility | current workspace/filter | evidence snapshot detail | `Freshness unavailable` | derived from existing model |
|
||||
| Evidence path: snapshot | Evidence Overview | `EvidenceSnapshot` | active/current snapshot | evidence visibility | current workspace/filter | evidence snapshot detail | unavailable/not generated | repo-verified |
|
||||
| Evidence path: review pack | Evidence Overview | `ReviewPack`, `EvidenceSnapshot::reviewPacks()` | status/generated/expired fields | `review_pack.view`, `ReviewPackPolicy` | current workspace/filter | review pack detail/download if authorized | `Review pack unavailable` / `Not generated` | foundation-real |
|
||||
| Evidence path: operation proof | Evidence Overview | `EvidenceSnapshot::operationRun()`, `ReviewPack::operationRun()`, `OperationRunLinks` | relation/run id | operation visibility | current workspace/filter | operation detail | `Operation proof unavailable` | foundation-real |
|
||||
| Evidence path: stored report/export | Evidence Overview | `StoredReport`, `StoredReportResource` | report type/fingerprint/payload | report-type capability | current workspace/filter | stored report detail | `Stored report unavailable` | foundation-real |
|
||||
| Evidence path: decision/risk record | Evidence Overview | `FindingExceptionEvidenceReference`, related resources | evidence reference relation | finding exception/evidence capabilities | current workspace/filter | finding/exception/evidence route if authorized | `Decision proof unavailable` | foundation-real |
|
||||
| Evidence path: audit trail | Evidence Overview | `AuditLog` events for evidence actions | action/resource metadata | `audit.view` | current workspace/filter | audit log filtered/selected link if implemented | `Audit event unavailable` | foundation-real |
|
||||
| Export/report availability panel | Evidence Overview | `ReviewPack`, `StoredReport` | existing statuses and report types | review/report capabilities | current workspace/filter | review pack download/detail, stored report detail | `Unavailable` / `Not generated` | foundation-real |
|
||||
| Evidence inventory table | Evidence Overview | existing Filament table | latest accessible snapshots | evidence visibility | current workspace/filter | row URL to evidence snapshot | existing empty state | repo-verified |
|
||||
| Evidence diagnostics disclosure | Evidence Overview | raw snapshot/report/run payloads | raw fields exist | `support_diagnostics.view` or stricter | current scope | existing detail/support surfaces only | collapsed/hidden | foundation-real |
|
||||
| Raw provider payloads | Evidence Overview | raw Graph/provider payloads | not safe default | support-only future | N/A | N/A | never default-visible | deferred future capability |
|
||||
| Audit Log route | Audit Log | `admin.monitoring.audit-log` | route | workspace middleware + `audit.view` | current workspace | none | 404/403 per existing resolver | repo-verified |
|
||||
| Audit Log title/question | Audit Log | page/view stable copy | static copy | audit page access | workspace/filter | none | static title | repo-verified |
|
||||
| Workspace scope label | Audit Log | `WorkspaceContext` and shell | current workspace/session | workspace membership | workspace shell | none | 404 if unavailable | repo-verified |
|
||||
| Environment filter chip | Audit Log | `environmentFilterChip()`, shared chip partial | `WorkspaceHubEnvironmentFilter` + table state | actor must access environment | `?environment_id={id}` only | none | no chip on clean URL | repo-verified |
|
||||
| Clear filter action | Audit Log | empty state/header clear flow + resetter | generated clean route | audit page access | removes canonical/table/session state | none | hidden/unavailable when unfiltered | repo-verified |
|
||||
| Legacy alias rejection | Audit Log | resetter and navigation tests | forbidden query/session keys | audit page access | aliases do not set filter | none | workspace-wide view or safe 404; explicit Spec 329 coverage required | foundation-real |
|
||||
| Cross-workspace environment guard | Audit Log | `WorkspaceHubEnvironmentFilter::fromRequest()`, `authorizedTenants()` | environment scoped by workspace/access | workspace and environment entitlement | current workspace only | none | 404 / safe no-access | repo-verified |
|
||||
| Audit proof workbench | Audit Log | new page-local payload over `AuditLog` | latest/selected visible event | `audit.view` | current workspace/filter | selected event, related record, operation link | `No audit events in scope` | derived from existing model |
|
||||
| Selected event proof panel | Audit Log | `selectedAuditRecord()`, selected-event partial | `event` query + normalized table/filter visibility | `audit.view` and row scope | current workspace/filter | related record/proof via resolver | no selected event panel | repo-verified |
|
||||
| Actor | Audit Log | `AuditLog::actorSnapshot()`, `actorDisplayLabel()` | actor fields/metadata | `audit.view` | current workspace/filter | selected event proof | `Actor unavailable` | repo-verified |
|
||||
| Action | Audit Log | `action`, `AuditActionId::labelFor()` | action id | `audit.view` | current workspace/filter | selected event proof | `Action unavailable` | repo-verified |
|
||||
| Target | Audit Log | `targetSnapshot()`, `targetDisplayLabel()` | target fields | `audit.view` | current workspace/filter | related target link if authorized | `No target snapshot` | repo-verified |
|
||||
| Outcome | Audit Log | `normalizedOutcome()`, `BadgeRenderer` | outcome/status | `audit.view` | current workspace/filter | selected event proof | `Outcome unavailable` | repo-verified |
|
||||
| Time | Audit Log | `recorded_at` | timestamp | `audit.view` | current workspace/filter | selected event proof | `Time unavailable` | repo-verified |
|
||||
| Scope | Audit Log | `workspace`, `tenant`, `workspace_id`, `managed_environment_id` | relationship/ids | `audit.view`, environment entitlement | workspace/filter | selected event proof | workspace-wide event | repo-verified |
|
||||
| Related operation proof | Audit Log | `AuditLog::operationRun()`, `RelatedNavigationResolver`, `OperationRunLinks` | operation relation/resource target | operation/source authorization | current workspace/filter | operation detail/source record | `Operation proof unavailable` | foundation-real |
|
||||
| Related evidence/export proof | Audit Log | resource type/id + resolver | target relation where supported | source authorization | current workspace/filter | source detail route | `Related proof unavailable` | foundation-real |
|
||||
| Readable context | Audit Log | `AuditLog::contextItems()` | safe scalar metadata subset | `audit.view` | current workspace/filter | selected event proof | no additional context | repo-verified |
|
||||
| Technical metadata | Audit Log | `AuditLog::technicalMetadata()` | technical fields | raw/diagnostics capability | current scope | collapsed diagnostics only | hidden by default; current default exposure must change | repo-verified |
|
||||
| Raw audit metadata blob | Audit Log | `AuditLog.metadata` | raw JSON/array | support/raw capability only | current scope | collapsed diagnostics only if ever exposed | hidden by default | foundation-real |
|
||||
| Support access history filter/export | Audit Log | existing header actions | supportAccess query/export action | current page access; export needs review | workspace/filter | CSV stream for support actions only | existing action hidden/available per current page; not a generic audit export claim | repo-verified |
|
||||
| Audit table/history | Audit Log | existing Filament table | scoped query, filters, columns | `audit.view` + environment entitlement | workspace/filter | inspect action with event query | existing empty state | repo-verified |
|
||||
| Disclosure rule panel | Both | page-local copy/state | static hierarchy + capabilities | page access | current scope | links only when authorized | compact panel | derived from existing model |
|
||||
| Tenant platform copy guard | Both | runtime copy/tests | string assertions | N/A | page copy | N/A | use Workspace/Environment; implementation test required | repo-verified |
|
||||
|
||||
## Required Runtime Element Decisions
|
||||
|
||||
| Element | v1 decision |
|
||||
|---|---|
|
||||
| New evidence backend | deferred future capability; do not build |
|
||||
| New audit ingestion engine | deferred future capability; do not build |
|
||||
| New immutable/certification/integrity claim | deferred future capability; do not claim |
|
||||
| Generic compliance readiness badge | deferred future capability; do not show |
|
||||
| Generic export engine | deferred future capability; use only existing ReviewPack/StoredReport/download truth |
|
||||
| Evidence freshness | derive from existing generated/expires/artifact truth only |
|
||||
| Review pack state | derive from existing `ReviewPack.status` and timestamps only |
|
||||
| Stored report availability | derive from existing `StoredReport` records and report-type capabilities only |
|
||||
| Operation proof | link only through existing run relations/helpers and authorization |
|
||||
| Audit event selected panel | actor/action/target/outcome/time first; raw metadata collapsed |
|
||||
| Diagnostics | collapsed/hidden by default and capability-aware if exposed |
|
||||
| Raw provider payloads | never default-visible |
|
||||
| Dangerous/mutating actions | do not add unless spec/plan updated first |
|
||||
| Legacy query aliases | rejected/neutralized; do not support |
|
||||
|
||||
## Implementation Update Rule
|
||||
|
||||
If implementation discovers that a planned UI element has no safe source, no authorization path, or would require new persisted truth, the element must become `empty/unavailable state` or `deferred future capability`. Do not create backend foundation inside Spec 329 without updating `spec.md`, `plan.md`, `tasks.md`, and this map first.
|
||||
|
||||
## Implementation Close-Out
|
||||
|
||||
- Evidence Overview now renders a proof-first workbench from existing `EvidenceSnapshot`, `ReviewPack`, `StoredReport`, `OperationRun`, artifact-truth, policy, and workspace-hub filter sources. The existing inventory table remains secondary context, and the existing route is reachable from the Workspace Monitoring sidebar.
|
||||
- Audit Log now renders an event-proof-first workbench from existing `AuditLog` actor/action/target/outcome/time/scope fields, related navigation, and operation proof links. The existing event history table and selected-event inspect flow remain available.
|
||||
- Diagnostics/raw metadata are not default-visible. Evidence diagnostics are collapsed with guidance to use authorized detail surfaces; audit technical metadata is behind collapsed, capability-aware disclosure.
|
||||
- The duplicated `/admin/evidence/overview` route registration was removed; the canonical route name and path remain unchanged.
|
||||
- UI coverage registry files were not changed because route names, paths, archetypes, and strategic surface classifications remain the existing UI-025 and UI-044 entries. Spec 329 carries the implementation proof through this repo truth map, targeted tests, and browser screenshots.
|
||||
- Browser screenshots are stored in `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/`.
|
||||
- No migrations, seeders, packages, environment variables, queues, scheduler changes, storage changes, deployment assets, backwards compatibility layer, or legacy tenant alias support were added.
|
||||
607
specs/329-evidence-audit-log-disclosure-productization/spec.md
Normal file
@ -0,0 +1,607 @@
|
||||
# Feature Specification: Spec 329 - Evidence / Audit Log Disclosure Productization
|
||||
|
||||
**Feature Branch**: `329-evidence-audit-log-disclosure-productization`
|
||||
**Created**: 2026-05-19
|
||||
**Status**: Implemented
|
||||
**Type**: Runtime UI productization / evidence-proof surface / audit disclosure UX
|
||||
**Runtime posture**: Narrow runtime UI implementation. Repo-based. No invented backend foundation.
|
||||
**Input**: User-provided full Spec 329 draft.
|
||||
|
||||
## Dependencies And Historical Context
|
||||
|
||||
Depends on:
|
||||
|
||||
- Spec 314 - Workspace Hub Navigation Context Contract.
|
||||
- Spec 315 - Environment CTA Explicit Filter Contract.
|
||||
- Spec 316 - Workspace Hub Clear Filter Contract.
|
||||
- Spec 317 - Legacy Tenant / Environment Context Cleanup.
|
||||
- Spec 318 - Admin Surface Scope & Shell Context Audit.
|
||||
- Spec 319 - Environment-Owned Surface Routing & Shell Context Contract.
|
||||
- Spec 320 - Workspace-Owned Analysis Surface Registration & Shell Cutover.
|
||||
- Spec 321 - Alerts / Audit Log Environment Filter Contract Decision.
|
||||
- Spec 322 - Browser No-Drift Regression Guard.
|
||||
- Spec 325 - Screenshot-Anchored Strategic Target Images.
|
||||
- Spec 326 - Customer Review Workspace v1 Productization.
|
||||
- Spec 327 - Governance Inbox Decision-First Workbench Productization.
|
||||
- Spec 328 - Operations Hub Decision-First Workbench Productization.
|
||||
|
||||
Repo truth adjustment: the user draft allowed `/admin/evidence` or an existing canonical route. Current repository truth is `admin.evidence.overview` at `/admin/evidence/overview` and `admin.monitoring.audit-log` at `/admin/audit-log`. Spec 329 productizes those existing routes and must not create replacement routes, new evidence/audit engines, new export pipelines, new persistence, or new compliance certification semantics.
|
||||
|
||||
Spec 325 target images are visual calibration only. They are not runtime truth for proof availability, export readiness, immutability, certification, RBAC, disclosure levels, evidence freshness, or audit event verification.
|
||||
|
||||
## Spec Candidate Check
|
||||
|
||||
- **Problem**: Evidence Overview and Audit Log are repo-real, but they still risk reading as technical metadata and event tables instead of proof/disclosure surfaces that answer what proof exists and which event proves what happened.
|
||||
- **Today's failure**: Evidence snapshots, review packs, operation proof, stored reports, actor/action/target/outcome/time, and raw metadata are not consistently ordered by decision value. Audit selected-event detail currently exposes technical metadata in the default selected-event view, and Evidence Overview is still table-first.
|
||||
- **User-visible improvement**: Auditors, security reviewers, MSP operators, and service delivery teams can see scope, proof availability, evidence path, actor/action/target/outcome/time, related proof, disclosure posture, and diagnostics status before any raw metadata.
|
||||
- **Smallest enterprise-capable version**: Productize only the existing Evidence Overview and Audit Log pages using existing `EvidenceSnapshot`, `ReviewPack`, `StoredReport`, `OperationRun`, `AuditLog`, policies/capabilities, related links, and workspace hub filter helpers. Tables remain secondary context.
|
||||
- **Explicit non-goals**: No new audit engine, evidence store, immutable storage, legal attestation, compliance framework mapping, external auditor portal, export engine, report generation engine, retention/hold system, AI summarization, package, queue, scheduler, storage, env var, migration, seed, compatibility route, or legacy query alias.
|
||||
- **Permanent complexity imported**: Feature-local page payloads, targeted Feature/Livewire tests, one Browser smoke, screenshots, and `repo-truth-map.md`. No new persisted truth, public abstraction, enum/status family, status taxonomy, or cross-domain UI framework.
|
||||
- **Why now**: Specs 314-322 stabilized workspace/environment context. Specs 326-328 established the strategic productization pattern. Spec 328 explicitly deferred Evidence / Audit Log Disclosure Productization as the next proof/disclosure lane.
|
||||
- **Why not local**: A column rename or small copy tweak would not change the first-read hierarchy. A new evidence/audit backend would overbuild. The narrow correct slice is a repo-truth-bounded productization pass on two existing pages.
|
||||
- **Approval class**: Core Enterprise.
|
||||
- **Red flags triggered**: Strategic UI productization and evidence/audit disclosure semantics. Defense: scope is limited to existing pages and existing truth sources, forbids new backend/state frameworks, and prevents false proof/certification claims.
|
||||
- **Score**: Nutzen: 2 | Dringlichkeit: 2 | Scope: 2 | Komplexitaet: 2 | Produktnaehe: 2 | Wiederverwendung: 2 | **Gesamt: 12/12**
|
||||
- **Decision**: approve.
|
||||
|
||||
## Candidate Source And Completed-Spec Guardrail
|
||||
|
||||
- **Candidate source**: Direct user-provided manual promotion for Spec 329, aligned with the follow-up list in Specs 326, 327, and 328 and the Audit Log / Evidence Overview strategic rows in `docs/ui-ux-enterprise-audit/`.
|
||||
- **Current package check**: No `specs/329-*` package, branch, or completed package existed before this preparation run.
|
||||
- **Related completed-spec check**: Specs 314-328 include historical/completed foundation and productization signals. They are dependency context only and must not be rewritten by Spec 329.
|
||||
- **Close alternatives deferred**: Environment Dashboard / Baseline Compare Productization, Restore Safety Workflow Productization, and Provider Readiness Productization remain follow-up candidates 330-332.
|
||||
- **Smallest viable implementation slice**: Existing Evidence Overview and Audit Log only: header/scope, proof/event workbench, evidence path/event proof panel, export/report availability, table as secondary context, collapsed diagnostics, RBAC-aware links/actions, canonical `environment_id` filter behavior, empty states, and targeted tests/browser smoke.
|
||||
|
||||
## Spec Scope Fields
|
||||
|
||||
- **Scope**: workspace canonical-view proof/disclosure surfaces, optionally filtered by canonical `environment_id`.
|
||||
- **Primary Routes**:
|
||||
- Existing Evidence Overview route: `/admin/evidence/overview`.
|
||||
- Existing Evidence route name: `admin.evidence.overview`.
|
||||
- Existing Evidence page class: `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php`.
|
||||
- Existing Evidence view: `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php`.
|
||||
- Existing Audit Log route: `/admin/audit-log`.
|
||||
- Existing Audit route name: `admin.monitoring.audit-log`.
|
||||
- Existing Audit page class: `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php`.
|
||||
- Existing Audit view: `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php`.
|
||||
- Existing Audit selected-event partial: `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php`.
|
||||
- **Data Ownership**:
|
||||
- Evidence truth: `EvidenceSnapshot.status`, `completeness_state`, `summary`, `generated_at`, `expires_at`, `operation_run_id`, `workspace_id`, and `managed_environment_id`.
|
||||
- Evidence path truth: `EvidenceSnapshotItem`, `ReviewPack`, `StoredReport`, `EnvironmentReview`, `FindingExceptionEvidenceReference`, and `OperationRun` links where existing relations/queries prove availability.
|
||||
- Audit truth: `AuditLog.actor_*`, `actor_type`, `actor_label`, `action`, `resource_type`, `resource_id`, `target_label`, `status`, `outcome`, `summary`, `metadata`, `operation_run_id`, `recorded_at`, `workspace_id`, and `managed_environment_id`.
|
||||
- Operation proof truth: `OperationRunLinks::tenantlessView()`, `OperationRunLinks::related()`, `AuditLog::operationRun()`, `EvidenceSnapshot::operationRun()`, and `ReviewPack::operationRun()`.
|
||||
- Workspace/environment scope: `WorkspaceContext`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CanonicalAdminEnvironmentFilterState`, and the shared environment filter chip partial.
|
||||
- **RBAC**:
|
||||
- Workspace membership required.
|
||||
- Evidence page data uses existing environment entitlement and `evidence.view`.
|
||||
- Evidence snapshot details use `EvidenceSnapshotPolicy`.
|
||||
- Review pack links/download/open states use `review_pack.view` / `ReviewPackPolicy` and existing download authorization.
|
||||
- Stored report links use report-type capabilities such as `permission_posture.view` and `entra_roles.view`.
|
||||
- Audit Log page access uses workspace membership and `audit.view`.
|
||||
- Operation proof links use existing operation visibility and related-route authorization.
|
||||
- Diagnostics/raw metadata visibility uses `support_diagnostics.view` or a stricter existing capability if implementation finds one.
|
||||
- Non-member or cross-workspace environment access remains deny-as-not-found.
|
||||
- Member with missing capability must not see protected records, raw metadata, or unauthorized actions.
|
||||
|
||||
For canonical-view specs:
|
||||
|
||||
- **Default filter behavior when environment context is active**: clean `/admin/evidence/overview` and `/admin/audit-log` remain workspace-wide and must not inherit remembered Environment context, Filament tenant context, session table filters, or legacy query aliases.
|
||||
- **Explicit entitlement checks preventing cross-environment leakage**: `?environment_id=` must resolve through the current workspace and actor entitlement. Cross-workspace or inaccessible IDs return safe no-access / 404.
|
||||
|
||||
## UI Surface Impact
|
||||
|
||||
Does this spec add, remove, rename, or materially change any reachable UI surface?
|
||||
|
||||
- [ ] No UI surface impact
|
||||
- [x] Existing page changed
|
||||
- [ ] New page/route added
|
||||
- [x] Navigation changed
|
||||
- [ ] Filament panel/provider surface changed
|
||||
- [ ] New modal/drawer/wizard/action added
|
||||
- [x] New table/form/state added
|
||||
- [ ] Customer-facing surface changed
|
||||
- [ ] Dangerous action changed
|
||||
- [x] Status/evidence/review presentation changed
|
||||
- [x] Workspace/environment context presentation changed
|
||||
|
||||
## UI/Productization Coverage
|
||||
|
||||
- **Route/page/surface**: `/admin/evidence/overview`, `/admin/audit-log`, `EvidenceOverview`, `AuditLog`, their Blade views, and Audit selected-event partial.
|
||||
- **Current or new page archetype**: Evidence / Audit strategic surfaces, matching `docs/ui-ux-enterprise-audit/route-inventory.md` rows UI-025 and UI-044.
|
||||
- **Design depth**: Strategic Surface.
|
||||
- **Repo-truth level**: repo-verified route/page/model foundations; individual runtime elements are classified in `repo-truth-map.md`.
|
||||
- **Existing pattern reused**: Filament Page, Filament table, Filament Sections where suitable, badges, shared environment filter chip, `BadgeRenderer`, `ArtifactTruthPresenter`, `OperationRunLinks`, resource policies, and current workspace hub resetter/filter helpers.
|
||||
- **New pattern required**: no new runtime framework; page-local workbench composition only.
|
||||
- **Screenshot required**: yes, Browser smoke screenshots under `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/`.
|
||||
- **Page audit required**: no full new audit unless implementation materially changes route inventory or archetype. Evidence Overview is already UI-044 but lacks a page report; implementation may document no registry update if Spec 329 carries the page productization proof, or update UI coverage artifacts if the route/archetype state changes.
|
||||
- **Customer-safe review required**: yes for default copy because evidence/audit surfaces are auditor-adjacent. Default views must avoid raw JSON, debug vocabulary, false certification, and unsupported verification claims.
|
||||
- **Dangerous-action review required**: no dangerous actions expected. If implementation unexpectedly adds download/export/open support actions, they must remain navigation/download actions with existing authorization. Any destructive/high-impact action requires spec/plan update first and must use `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, audit, notification, and tests.
|
||||
- **Coverage files updated or explicitly not needed**:
|
||||
- [ ] `docs/ui-ux-enterprise-audit/route-inventory.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/design-coverage-matrix.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/page-reports/...`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/strategic-surfaces.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/grouped-follow-up-candidates.md`
|
||||
- [ ] `docs/ui-ux-enterprise-audit/unresolved-pages.md`
|
||||
- [ ] `N/A - no reachable UI surface impact`
|
||||
- [x] Active spec package must carry repo-truth map, tests, browser screenshots, and close-out coverage decision. Registry updates are required only if runtime changes alter route/archetype/coverage classification.
|
||||
|
||||
## Cross-Cutting / Shared Pattern Reuse
|
||||
|
||||
- **Cross-cutting feature?**: yes.
|
||||
- **Interaction class(es)**: evidence/report viewers, audit event detail, status messaging, proof links, OperationRun links, environment filter chip, diagnostics disclosure, table empty states, export/download/open actions.
|
||||
- **Systems touched**: `EvidenceOverview`, `AuditLog`, audit event partial, `EvidenceSnapshotResource`, `ReviewPackResource`, `StoredReportResource`, `OperationRunLinks`, `RelatedNavigationResolver`, `BadgeRenderer`, `ArtifactTruthPresenter`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, `CanonicalAdminEnvironmentFilterState`, resource policies and capabilities.
|
||||
- **Existing pattern(s) to extend**: existing evidence table, audit table, audit selected-event detail, environment chip, related navigation resolver, artifact truth presentation, OperationRun links, resource policies.
|
||||
- **Shared contract / presenter / builder / renderer to reuse**: `BadgeRenderer`, `ArtifactTruthPresenter`, `OperationRunLinks`, `RelatedNavigationResolver`, `WorkspaceHubEnvironmentFilter`, `WorkspaceHubFilterStateResetter`, existing policy/capability resolvers.
|
||||
- **Why the existing shared path is sufficient or insufficient**: Existing paths are sufficient for evidence snapshots, audit events, related operation links, badges, authorization, and filter/reset behavior. They are insufficient only in first-read hierarchy and default disclosure ordering on these pages.
|
||||
- **Allowed deviation and why**: bounded page-local payload/view helpers are allowed if needed to reduce Blade complexity. New public evidence/audit disclosure frameworks, status taxonomies, presenter layers, or proof engines are not allowed.
|
||||
- **Consistency impact**: Evidence, review pack, stored report, operation, audit, scope, diagnostic, export, and action labels must stay aligned across source resources and related links.
|
||||
- **Review focus**: Verify no fake proof, no false green state, no raw diagnostics by default, no unauthorized links/actions, no shell-scope regression, no tenant platform copy, and no duplicate local truth layer.
|
||||
|
||||
## OperationRun UX Impact
|
||||
|
||||
- **Touches OperationRun start/completion/link UX?**: link and proof availability semantics only. No new OperationRun creation, queueing, dedupe, lifecycle transition, summary-count writer, or notification behavior.
|
||||
- **Shared OperationRun UX contract/layer reused**: `OperationRunLinks`, `OperationRunUrl`, related resource links, existing operation visibility, and existing operation detail pages.
|
||||
- **Delegated start/completion UX behaviors**: N/A - no operation start.
|
||||
- **Local surface-owned behavior that remains**: show `Operation proof available`, `Operation proof unavailable`, or authorized open operation link based on existing relations/links.
|
||||
- **Queued DB-notification policy**: unchanged / N/A.
|
||||
- **Terminal notification path**: unchanged.
|
||||
- **Exception required?**: none.
|
||||
|
||||
## Provider Boundary / Platform Core Check
|
||||
|
||||
- **Shared provider/platform boundary touched?**: no new provider seam.
|
||||
- **Boundary classification**: platform-core proof/disclosure views over existing provider-backed evidence and audit records.
|
||||
- **Seams affected**: display/routing over evidence snapshots, review packs, stored reports, OperationRuns, audit events, environment filters, and diagnostics disclosure.
|
||||
- **Neutral platform terms preserved or introduced**: workspace, environment, evidence, proof, audit event, actor, action, target, outcome, time, export artifact, diagnostics, raw metadata.
|
||||
- **Provider-specific semantics retained and why**: Microsoft/Entra/Intune terms may appear only where the underlying provider record or report already uses them. Do not surface raw provider IDs, Graph payloads, provider responses, or provider diagnostics by default.
|
||||
- **Why this does not deepen provider coupling accidentally**: no Graph calls, provider contracts, provider connection changes, provider-shaped persistence, or provider taxonomy changes.
|
||||
- **Follow-up path**: Environment Dashboard / Baseline Compare, Restore Safety Workflow, and Provider Readiness remain separate specs.
|
||||
|
||||
## UI / Surface Guardrail Impact
|
||||
|
||||
| Surface / Change | Operator-facing surface change? | Native vs Custom | Shared-Family Relevance | State Layers Touched | Exception Needed? | Low-Impact / `N/A` Note |
|
||||
|---|---:|---|---|---|---:|---|
|
||||
| Evidence Overview page | yes | Native Filament page plus existing Blade composition | evidence/report viewer, proof path, filter chip | page, URL query, table state, derived payload | no | Existing route only |
|
||||
| Workspace sidebar Evidence entry | yes | Native Filament navigation item | workspace hub navigation | route/link state only | no | Existing route only |
|
||||
| Audit Log page | yes | Native Filament page plus existing Blade composition | audit event proof, selected detail, filter chip | page, URL query, selected event, table state | no | Existing route only |
|
||||
| Evidence proof workbench | yes | Filament sections / page-local Blade | proof status and artifact links | page payload | no | Derived from repo truth |
|
||||
| Audit proof workbench | yes | Filament sections / page-local Blade | actor/action/target/outcome/time | page payload and selected event | no | Derived from repo truth |
|
||||
| Evidence/Audit tables | yes | existing Filament tables | secondary evidence/event inventory | table state | no | Tables remain available |
|
||||
| Diagnostics disclosure | yes | collapsed/progressive disclosure only | support/raw detail | detail links/action visibility | no | Authorized and collapsed by default |
|
||||
|
||||
## Decision-First Surface Role
|
||||
|
||||
| Surface | Decision Role | Human-in-the-loop Moment | Immediately Visible for First Decision | On-Demand Detail / Evidence | Why This Is Primary or Why Not | Workflow Alignment | Attention-load Reduction |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Evidence Overview | Primary proof availability surface | Reviewer decides whether this workspace/environment scope contains usable proof | scope, evidence availability, freshness, evidence path, review pack/export/report availability, operation proof state | evidence inventory table, snapshot detail, review pack detail/download, stored report detail, operation detail, diagnostics | Primary because it answers proof readiness before artifact inspection | Follows evidence/proof path, not storage object browsing | Prevents scanning raw snapshots first |
|
||||
| Audit Log | Primary audit event proof surface | Reviewer decides which event proves what happened | scope, actor, action, target, outcome, time, related record/proof, disclosure status | selected event context, related record, operation detail, raw metadata diagnostics | Primary because it proves actor/action/target/outcome/time | Follows disclosure, not raw event history | Prevents raw metadata from becoming first-read |
|
||||
| Existing tables | Secondary Context | Operator scans inventory/history after proof summary is clear | concise rows, filters, sort, inspect/open affordance | row detail/source route | Secondary because tables support investigation/history | Keeps existing monitoring power | Reduces table-first dominance |
|
||||
| Diagnostics disclosure | Tertiary Evidence / Diagnostics | Support/operator inspects technical data after proof path | collapsed availability only | raw metadata, technical IDs, support diagnostics where authorized | Not primary; diagnostics support proof | Preserves debug depth | Prevents default raw-console experience |
|
||||
|
||||
## Audience-Aware Disclosure
|
||||
|
||||
| Surface | Audience Modes In Scope | Decision-First Default-Visible Content | Operator Diagnostics | Support / Raw Evidence | One Dominant Next Action | Hidden / Gated By Default | Duplicate-Truth Prevention |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| Evidence Overview | auditor, security reviewer, operator-MSP, manager, support reviewer | proof availability, evidence freshness, evidence snapshot, review pack/export/report state, operation proof state, scope | secondary inventory table and artifact detail links | raw snapshot item payloads, raw Graph/provider data, stack traces, debug metadata | open evidence snapshot or review/export proof where authorized | raw metadata, provider payloads, unsupported verification claims, unauthorized links | top workbench states proof state once; table adds inventory context |
|
||||
| Audit Log | auditor, security reviewer, operator-MSP, support reviewer | actor, action, target, outcome, time, scope, related proof, disclosure status | selected-event readable context and related links | raw metadata, technical IDs, internal exception/debug data, provider payloads | inspect/open selected event or related proof where authorized | raw metadata, diagnostics, provider payloads, secrets | workbench states event proof once; selected detail adds proof/context |
|
||||
|
||||
## UI/UX Surface Classification
|
||||
|
||||
| Surface | Action Surface Class | Surface Type | Likely Next Operator Action | Primary Inspect/Open Model | Row Click | Secondary Actions Placement | Destructive Actions Placement | Canonical Collection Route | Canonical Detail Route | Scope Signals | Canonical Noun | Critical Truth Visible by Default | Exception Type / Justification |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Evidence Overview | Workbench / Evidence | Proof-first evidence workspace | Open evidence proof or review/export artifact | explicit primary proof link plus existing table row detail | existing table row URL remains | proof panel/table links | none introduced | `/admin/evidence/overview` | existing evidence/review/report/operation detail routes | active workspace, optional `environment_id` chip | Evidence / Proof | scope, snapshot state, freshness, review/export/report/operation proof state | none |
|
||||
| Audit Log | Workbench / Audit | Event-proof audit history | Inspect event or open related proof | selected event panel and inspect action | existing inspect action | proof panel/table actions | none introduced | `/admin/audit-log` | same route with `event` query and related routes | active workspace, optional `environment_id` chip | Audit Event | actor, action, target, outcome, time, scope | none |
|
||||
| Diagnostics disclosure | Diagnostics / Support Raw | Collapsed diagnostic context | Expand or open diagnostics if authorized | disclosure/detail action | N/A | below/inside proof panel | none | same pages | existing authorized detail surfaces | authorized-only label | Diagnostics | collapsed status only | none |
|
||||
|
||||
## Operator Surface Contract
|
||||
|
||||
| Surface | Primary Persona | Decision / Operator Action Supported | Surface Type | Primary Operator Question | Default-visible Information | Diagnostics-only Information | Status Dimensions Used | Mutation Scope | Primary Actions | Dangerous Actions |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| Evidence Overview | Auditor / MSP operator / security reviewer | Decide whether current scope has proof and where to open it | Workspace evidence proof workbench | What proof is available for this scope? | scope, evidence snapshot, freshness, review pack/export, stored report, operation proof, evidence path, unavailable states | raw snapshot payloads, provider responses, debug metadata, raw OperationRun context | proof availability, freshness, artifact availability, disclosure state | none on page by default | open evidence snapshot, open review pack/report/operation proof where authorized | none introduced |
|
||||
| Audit Log | Auditor / governance admin / support reviewer | Decide which event proves what happened | Workspace audit event proof workbench | Which event proves what happened? | actor, action, target, outcome, time, scope, related proof, disclosure level | raw metadata, technical IDs, provider payloads, stack traces, debug metadata | event outcome, actor type, target type, scope, proof availability, disclosure state | none on page by default | inspect event, open related proof/record where authorized | none introduced |
|
||||
|
||||
## Proportionality Review
|
||||
|
||||
- **New source of truth?**: no.
|
||||
- **New persisted entity/table/artifact?**: no. `repo-truth-map.md` is a Spec Kit preparation artifact, not runtime truth.
|
||||
- **New abstraction?**: no public abstraction. Page-local private helpers are allowed only when they reduce Blade complexity and stay feature-local.
|
||||
- **New enum/state/reason family?**: no domain state. Display states must derive from existing snapshot, review pack, stored report, operation, audit, policy, and capability truth.
|
||||
- **New cross-domain UI framework/taxonomy?**: no.
|
||||
- **Current operator problem**: Existing Evidence and Audit pages must answer proof/disclosure questions without forcing raw table/metadata inspection first.
|
||||
- **Existing structure is insufficient because**: Current pages expose tables and selected-event detail but do not consistently prioritize proof path, actor/action/target/outcome/time, availability, freshness, and disclosure hierarchy before raw metadata.
|
||||
- **Narrowest correct implementation**: Refactor existing page layout and derived payloads, bind to existing sources, keep diagnostics collapsed, and add targeted tests/browser smoke.
|
||||
- **Ownership cost**: Feature-local layout/payload tests, one Browser smoke, screenshots, and spec truth map. No durable backend model or new framework cost.
|
||||
- **Alternative intentionally rejected**: new evidence engine, new audit ingestion, new compliance/certification layer, new export engine, raw log viewer, AI summary, broad design system work, or route replacement.
|
||||
- **Release truth**: current-release runtime UI productization over existing evidence/audit foundations.
|
||||
|
||||
### Compatibility posture
|
||||
|
||||
This feature assumes pre-production runtime posture. Backward compatibility, historical aliases, migration shims, dual-write logic, legacy route redirects, and legacy query aliases are out of scope. Existing legacy query aliases (`tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, `tableFilters`) must not be supported for Evidence Overview or Audit Log filtering.
|
||||
|
||||
## Testing / Lane / Runtime Impact
|
||||
|
||||
- **Test purpose / classification**: Feature, Filament/Livewire/HTTP, Browser.
|
||||
- **Validation lane(s)**: confidence plus browser for critical workspace/environment UI/scope smoke.
|
||||
- **Why this classification and these lanes are sufficient**: The change is user-facing Filament page productization with RBAC, evidence truth, audit event truth, scope, and disclosure behavior. Feature tests prove data/scope/action rules; Browser smoke proves rendered shell/filter/reload/disclosure/table hierarchy behavior.
|
||||
- **New or expanded test families**: additions under `tests/Feature/Monitoring`, `tests/Feature/Evidence`, `tests/Feature/Audit`, `tests/Feature/Navigation`, and `tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php`.
|
||||
- **Fixture / helper cost impact**: reuse existing factories/helpers for `EvidenceSnapshot`, `ReviewPack`, `StoredReport`, `OperationRun`, `AuditLog`, workspace/environment session context, and navigation filter tests. Do not widen expensive defaults.
|
||||
- **Heavy-family visibility / justification**: browser addition is explicit and named for Spec 329.
|
||||
- **Special surface test profile**: `global-context-shell`, `monitoring-state-page`, and `shared-detail-family`.
|
||||
- **Standard-native relief or required special coverage**: special coverage required for canonical filter, clear/reload, evidence path, event proof first-read, diagnostics hidden, RBAC action visibility, empty/non-empty states, and no platform-context tenant copy.
|
||||
- **Reviewer handoff**: confirm diagnostics are collapsed, raw metadata hidden, RBAC actions hidden/disabled correctly, no false proof/certification claims, clean workspace entry, canonical filter, clear filter, cross-workspace guard, and table/history remain secondary context.
|
||||
- **Budget / baseline / trend impact**: no expected material lane-cost shift beyond one targeted browser smoke.
|
||||
- **Escalation needed**: document-in-feature if browser coverage becomes too expensive or requires fixture broadening.
|
||||
- **Active feature PR close-out entry**: Smoke Coverage.
|
||||
- **Planned validation commands**:
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Monitoring tests/Feature/Evidence tests/Feature/Audit tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail artisan test --filter='Evidence|AuditLog|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`
|
||||
- `cd apps/platform && ./vendor/bin/sail pint --dirty`
|
||||
- `git diff --check`
|
||||
|
||||
## Summary
|
||||
|
||||
Productize TenantPilot's existing Evidence Overview and Audit Log into proof-first and disclosure-aware workspaces.
|
||||
|
||||
The pages must answer:
|
||||
|
||||
```text
|
||||
What proof is available for this scope?
|
||||
```
|
||||
|
||||
and:
|
||||
|
||||
```text
|
||||
Which event proves what happened?
|
||||
```
|
||||
|
||||
The implementation must lead with scope, proof availability, actor/action/target/outcome/time, evidence path, export/report availability where repo-supported, disclosure posture, and collapsed diagnostics. Existing tables remain available as secondary context.
|
||||
|
||||
## Product Context
|
||||
|
||||
TenantPilot is a governance-of-record platform. Evidence snapshots, review packs, stored reports, OperationRuns, audit logs, and accepted-risk/review records turn technical state and operations into proof consumable by MSP operators, security reviewers, customer stakeholders, auditors, service delivery managers, and support.
|
||||
|
||||
Evidence-first does not mean raw evidence first. It means proof path, provenance, scope, freshness, and disclosure level appear before raw technical details.
|
||||
|
||||
## Problem Statement
|
||||
|
||||
Evidence and Audit foundations exist, but the current UI can still drift into admin-tool patterns:
|
||||
|
||||
- Evidence Overview is still primarily a snapshot table.
|
||||
- Audit event inspection can show technical metadata by default.
|
||||
- Scope, freshness, review/export availability, OperationRun proof, and raw disclosure hierarchy are not first-read.
|
||||
- Customer/auditor-safe proof language and operator-only diagnostics can be mixed too early.
|
||||
- Unsupported immutability, verification, health, or certification claims must be prevented.
|
||||
|
||||
## Product Decision
|
||||
|
||||
Evidence Overview and Audit Log are workspace-owned proof/disclosure surfaces.
|
||||
|
||||
They may optionally be filtered by Environment using:
|
||||
|
||||
```text
|
||||
?environment_id={id}
|
||||
```
|
||||
|
||||
When filtered:
|
||||
|
||||
- Shell remains Workspace-only.
|
||||
- Visible Environment filter chip appears.
|
||||
- Clear filter returns to clean workspace-wide surface.
|
||||
- Reload/back/forward remain aligned.
|
||||
|
||||
They are not Environment-owned pages, raw log viewers, compliance suites, export engines, or backend evidence/audit engines.
|
||||
|
||||
## User Scenarios & Testing
|
||||
|
||||
### User Story 1 - Evidence proof availability first-read (Priority: P1)
|
||||
|
||||
As an auditor or MSP operator, I can open Evidence Overview and immediately understand whether the current workspace or filtered environment has proof available, stale, incomplete, unavailable, or not generated.
|
||||
|
||||
**Why this priority**: Evidence Overview is the product proof surface. If it remains table-first, reviewers must reconstruct proof readiness from artifacts.
|
||||
|
||||
**Independent Test**: Render Evidence Overview with complete, stale, missing, and empty evidence fixtures. Assert the main proof question, evidence path, freshness/availability states, review/export/report/operation proof states, and secondary table are visible while raw metadata is absent by default.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. Given a workspace has evidence snapshots, when the page loads cleanly, then the proof workbench shows proof availability and scope before the table.
|
||||
2. Given a filtered environment has no evidence, when the page loads with `environment_id`, then the page shows a visible chip and honest no-evidence state without implying proof exists.
|
||||
3. Given raw snapshot/provider/debug payloads exist in underlying records, when the page first renders, then those values are not visible by default.
|
||||
|
||||
### User Story 2 - Audit event proof first-read (Priority: P1)
|
||||
|
||||
As a governance admin or security reviewer, I can open Audit Log and immediately see actor, action, target, outcome, time, scope, and related proof before raw metadata.
|
||||
|
||||
**Why this priority**: Audit Log is a core proof surface. Raw event history must not overpower who did what, when, against what target, and with what outcome.
|
||||
|
||||
**Independent Test**: Render Audit Log with selected and unselected event fixtures. Assert actor/action/target/outcome/time fields appear in the workbench/selected-event panel and raw metadata is collapsed/hidden by default.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. Given audit events exist, when the page loads, then it answers which event proves what happened.
|
||||
2. Given an event is selected through `event`, when it is visible in scope, then the selected panel shows actor, action, target, outcome, time, scope, and related proof before diagnostics.
|
||||
3. Given a selected event is outside the active environment filter or workspace entitlement, then it is not displayed as selected proof.
|
||||
|
||||
### User Story 3 - Scope and filter contract remains stable (Priority: P1)
|
||||
|
||||
As an operator, I can open clean and environment-filtered Evidence/Audit URLs without shell drift, remembered Environment fallback, legacy alias support, or cross-workspace leakage.
|
||||
|
||||
**Why this priority**: Specs 314-322 are prerequisite contracts. Proof and audit disclosure is unsafe if scope is ambiguous.
|
||||
|
||||
**Independent Test**: Open clean and `?environment_id=` URLs for Evidence Overview and Audit Log, clear the filter, reload, use legacy aliases, and attempt cross-workspace environment IDs.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. Given a clean URL, when Evidence or Audit loads, then the data is workspace-wide and no Environment chip appears.
|
||||
2. Given a valid `environment_id` in the current workspace, when the page loads, then the chip appears and data is filtered where supported.
|
||||
3. Given legacy aliases or table filters in the URL, when the page loads, then they do not create Environment filter state.
|
||||
4. Given a cross-workspace Environment ID, when the page loads, then safe no-access / 404 is returned.
|
||||
|
||||
### User Story 4 - RBAC-safe disclosure and proof links (Priority: P2)
|
||||
|
||||
As a least-privilege user, I only see evidence/audit/export/operation/diagnostic actions that I am allowed to access, and unavailable states do not leak sensitive raw data.
|
||||
|
||||
**Why this priority**: Evidence and audit content can contain sensitive operational proof, provider context, or support-only metadata.
|
||||
|
||||
**Independent Test**: Render the pages as users with and without evidence, audit, review pack, stored report, operation, and diagnostics capabilities. Assert protected actions are hidden/disabled and raw diagnostics remain hidden without `support_diagnostics.view`.
|
||||
|
||||
**Acceptance Scenarios**:
|
||||
|
||||
1. Given a user lacks `audit.view`, when they request Audit Log, then access is forbidden or denied according to existing workspace capability semantics.
|
||||
2. Given a user lacks evidence/report/review/operation capability, when proof exists, then the proof state is unavailable or linkless without leaking records.
|
||||
3. Given a user lacks diagnostics capability, when raw metadata exists, then raw/support disclosure is hidden by default and cannot be opened from the page.
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- No evidence snapshots exist in workspace.
|
||||
- Evidence exists only in another workspace.
|
||||
- Evidence snapshot exists but review pack does not.
|
||||
- Review pack exists but is queued, generating, failed, expired, or unavailable.
|
||||
- Stored reports exist only for report types the user cannot view.
|
||||
- OperationRun proof relation is missing, unauthorized, or has no safe route.
|
||||
- Audit events exist with null `managed_environment_id` and should appear only in workspace-wide Audit Log.
|
||||
- Audit events have missing evolved actor/target/outcome fields but legacy metadata can derive readable labels.
|
||||
- Selected audit `event` is invalid, unauthorized, outside filter, or cross-workspace.
|
||||
- Raw metadata includes internal keys, provider payloads, stack trace-like text, or debug metadata.
|
||||
- `environment_id` is malformed, array-valued, cross-workspace, or inaccessible.
|
||||
- Legacy aliases appear with or without canonical `environment_id`.
|
||||
- Existing route middleware or shell helpers must not force active Environment shell ownership on workspace hub pages.
|
||||
|
||||
## Functional Requirements
|
||||
|
||||
- **FR-001**: Evidence Overview MUST have a proof-first layout before the evidence inventory/table.
|
||||
- **FR-002**: Evidence Overview MUST show the stable question `What proof is available for this scope?`.
|
||||
- **FR-003**: Evidence Overview MUST show scope, evidence snapshot state, freshness/availability, review pack/export availability, stored report/export availability where repo-supported, OperationRun proof availability, and evidence path.
|
||||
- **FR-004**: Evidence Overview MUST show honest states only: evidence available, evidence incomplete, evidence unavailable, evidence stale, review pack unavailable, export available, export unavailable, not generated, not applicable, or unavailable.
|
||||
- **FR-005**: Evidence Overview MUST keep the existing evidence inventory/table available as secondary context.
|
||||
- **FR-006**: Audit Log MUST have an event-proof-first layout before the audit event table.
|
||||
- **FR-007**: Audit Log MUST show the stable question `Which event proves what happened?`.
|
||||
- **FR-008**: Audit Log default first-read MUST emphasize actor, action, target, outcome, time, and scope.
|
||||
- **FR-009**: Audit Log selected/latest event proof panel MUST show related record/proof where repo-supported and authorized.
|
||||
- **FR-010**: Audit Log MUST keep the existing audit event table available as secondary context.
|
||||
- **FR-011**: Raw metadata, raw payloads, provider responses, stack traces, provider secrets, internal exception traces, debug metadata, raw OperationRun payloads, and raw audit metadata blobs MUST NOT be visible by default.
|
||||
- **FR-012**: Diagnostics disclosure MUST be collapsed and capability-aware wherever exposed.
|
||||
- **FR-013**: Evidence and Audit pages MUST show the shared disclosure hierarchy: decision/proof visible, evidence/event visible, diagnostics collapsed, raw/support hidden.
|
||||
- **FR-014**: Visible runtime elements MUST be backed by `repo-verified`, `foundation-real`, `derived from existing model`, `empty/unavailable state`, or `deferred future capability` classification in `repo-truth-map.md`.
|
||||
- **FR-015**: No visible UI copy may claim immutable, certified, legally attested, tamper-proof, auditor-approved, compliance-ready, fully verified, 100 percent verified, or health-complete states unless repo truth explicitly proves them.
|
||||
- **FR-016**: Clean Evidence and Audit URLs MUST be workspace-wide, with Workspace shell only and no Environment chip.
|
||||
- **FR-017**: Filtered Evidence and Audit URLs MUST use only `environment_id`, show the visible Environment chip, filter data where supported, and keep Workspace shell ownership.
|
||||
- **FR-018**: Clear filter MUST return to a clean workspace URL and clear URL, Livewire, Filament table, deferred table, and persisted session Environment-like state.
|
||||
- **FR-019**: Legacy aliases `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters` as URL source MUST NOT create Environment filter state.
|
||||
- **FR-020**: Cross-workspace or unauthorized `environment_id` MUST return safe no-access / 404 and MUST NOT switch Workspace.
|
||||
- **FR-021**: Evidence link/actions MUST respect `evidence.view`, `EvidenceSnapshotPolicy`, environment entitlement, and workspace membership.
|
||||
- **FR-022**: Review pack/open/download link/actions MUST respect `review_pack.view`, `ReviewPackPolicy`, and existing download authorization.
|
||||
- **FR-023**: Stored report link/actions MUST respect existing report-type capabilities and `StoredReportResource` visibility.
|
||||
- **FR-024**: Audit access MUST respect workspace membership and `audit.view`.
|
||||
- **FR-025**: Operation proof links MUST route through existing operation link helpers and authorization.
|
||||
- **FR-026**: Diagnostics/raw metadata access MUST require `support_diagnostics.view` or stricter existing support/raw capability.
|
||||
- **FR-027**: Unauthorized actions MUST be hidden, disabled with existing convention, or replaced with safe unavailable state.
|
||||
- **FR-028**: No migration, seeder, package, env var, queue, scheduler, storage, deployment asset, compatibility route, or legacy alias support may be introduced unless spec/plan/tasks are updated before implementation.
|
||||
- **FR-029**: Filament v5 and Livewire v4.0+ patterns MUST be preserved. No Filament v3/v4 APIs or Livewire v3 references are allowed.
|
||||
- **FR-030**: No Graph calls or provider API calls may occur during page render.
|
||||
|
||||
## Non-Functional Requirements
|
||||
|
||||
- **NFR-001**: Workspace and Environment isolation MUST remain enforceable in queries and authorization.
|
||||
- **NFR-002**: Evidence/audit pages MUST remain DB-only render paths over existing persisted records.
|
||||
- **NFR-003**: Page copy MUST use customer/auditor-safe disclosure language and avoid platform-context `tenant` wording.
|
||||
- **NFR-003a**: Dynamic Environment display names are data and MAY contain `Tenant`; static platform-context copy MUST avoid retired tenant-first wording.
|
||||
- **NFR-003b**: Empty primary evidence snapshots MUST use product-safe proof language, show `Proof incomplete`, explain that the primary evidence snapshot is empty, and make supporting proof impact explicit without exposing implementation-heavy artifact-row language.
|
||||
- **NFR-004**: Page layouts MUST prefer native Filament components and shared primitives before custom Blade/Tailwind.
|
||||
- **NFR-005**: The change MUST not create a new cross-surface disclosure framework, proof state engine, or status taxonomy.
|
||||
- **NFR-006**: Browser verification MUST cover clean, filtered, clear, reload, non-empty, empty, diagnostics collapsed, table secondary, and tenant-copy guard states.
|
||||
|
||||
## Out Of Scope
|
||||
|
||||
- New evidence backend.
|
||||
- New audit event ingestion.
|
||||
- New immutable storage implementation.
|
||||
- New legal attestation/certification engine.
|
||||
- New compliance framework mapping.
|
||||
- New external auditor portal.
|
||||
- New export pipeline.
|
||||
- New report generation engine.
|
||||
- New retention/hold system.
|
||||
- AI summarization.
|
||||
- Customer Review Workspace redesign.
|
||||
- Operations Hub redesign.
|
||||
- Governance Inbox redesign.
|
||||
- New migrations by default.
|
||||
- New packages, env vars, queues, scheduler, storage, deployment assets, or external services.
|
||||
|
||||
## Required Repo Truth Map
|
||||
|
||||
Before runtime changes, `repo-truth-map.md` MUST exist under this spec directory and map each UI element to:
|
||||
|
||||
- UI element.
|
||||
- Surface.
|
||||
- Source model/service/page.
|
||||
- Status source.
|
||||
- Authorization/capability.
|
||||
- Workspace/Environment scope.
|
||||
- OperationRun/evidence/audit/export link.
|
||||
- Fallback/empty state.
|
||||
- Classification.
|
||||
|
||||
Required data areas:
|
||||
|
||||
- Evidence Snapshots.
|
||||
- Review Packs.
|
||||
- Stored Reports / export artifacts.
|
||||
- OperationRuns.
|
||||
- Audit Log events.
|
||||
- Actor/action/target/outcome/time fields.
|
||||
- Risk/Decision links if present.
|
||||
- Customer Review Workspace evidence links.
|
||||
- Governance Inbox evidence links.
|
||||
- Operations proof links.
|
||||
- Environment filter state.
|
||||
- Diagnostics/raw metadata availability.
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Evidence Overview
|
||||
|
||||
- [ ] Evidence Overview has proof-first layout.
|
||||
- [ ] Main proof question is visible.
|
||||
- [ ] Evidence path is visible.
|
||||
- [ ] Evidence snapshot state is visible.
|
||||
- [ ] Review pack/export state is visible where repo-supported.
|
||||
- [ ] Stored report/export state is visible where repo-supported.
|
||||
- [ ] OperationRun proof state is visible where repo-supported.
|
||||
- [ ] Evidence inventory/table remains available as secondary context.
|
||||
- [ ] Raw metadata is hidden by default.
|
||||
|
||||
### Audit Log
|
||||
|
||||
- [ ] Audit Log has event-proof-first layout.
|
||||
- [ ] Main audit proof question is visible.
|
||||
- [ ] Actor/action/target/outcome/time are first-read.
|
||||
- [ ] Selected/latest event proof panel exists.
|
||||
- [ ] Audit event table remains available as secondary context.
|
||||
- [ ] Raw metadata is hidden by default.
|
||||
|
||||
### Disclosure Safety
|
||||
|
||||
- [ ] Diagnostics are collapsed by default.
|
||||
- [ ] Raw payloads are hidden by default.
|
||||
- [ ] Provider secrets are not visible.
|
||||
- [ ] Internal exception/debug text is not visible.
|
||||
- [ ] No false immutability/certification/health/compliance claims are introduced.
|
||||
- [ ] No false green success state is introduced.
|
||||
|
||||
### Scope
|
||||
|
||||
- [ ] Clean URLs are workspace-wide.
|
||||
- [ ] Shell is Workspace-only.
|
||||
- [ ] Environment filter uses `environment_id`.
|
||||
- [ ] Visible Environment chip appears when filtered.
|
||||
- [ ] Clear filter works.
|
||||
- [ ] Reload after clear is safe.
|
||||
- [ ] Legacy aliases do not create filter state.
|
||||
- [ ] Cross-workspace Environment is rejected.
|
||||
|
||||
### RBAC
|
||||
|
||||
- [ ] Unauthorized user cannot access protected evidence/audit data.
|
||||
- [ ] Unauthorized actions are hidden/disabled/unavailable.
|
||||
- [ ] Evidence export/open respects capability.
|
||||
- [ ] Audit detail access respects capability.
|
||||
- [ ] Diagnostics/raw metadata access respects capability.
|
||||
- [ ] OperationRun proof access respects capability.
|
||||
|
||||
### UI / Visual
|
||||
|
||||
- [ ] Layout uses Spec 325 direction without treating target images as runtime truth.
|
||||
- [ ] Filament light mode remains readable.
|
||||
- [ ] No heavy one-off CSS.
|
||||
- [ ] Right-side proof/disclosure panel exists on desktop where suitable.
|
||||
- [ ] Tables are not the only default experience.
|
||||
- [ ] Page remains responsive enough for Filament shell.
|
||||
- [ ] Native Filament components are preferred where suitable.
|
||||
|
||||
### Tests / Validation
|
||||
|
||||
- [ ] Repo truth map exists.
|
||||
- [ ] Required Feature tests pass.
|
||||
- [ ] Required Browser smoke passes.
|
||||
- [ ] Relevant Spec 314-322 guards still pass.
|
||||
- [ ] `pint --dirty` passes.
|
||||
- [ ] `git diff --check` passes.
|
||||
- [ ] No broad rebaseline.
|
||||
- [ ] Full suite status is honestly reported if run/not run.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- **SC-001**: A reviewer can determine proof availability on Evidence Overview without opening raw artifact details.
|
||||
- **SC-002**: A reviewer can identify actor/action/target/outcome/time on Audit Log before seeing technical metadata.
|
||||
- **SC-003**: Browser smoke confirms clean, filtered, clear/reload, non-empty, empty, diagnostics-collapsed, table-secondary, and tenant-copy guard states.
|
||||
- **SC-004**: Tests prove raw diagnostic strings are absent by default.
|
||||
- **SC-005**: No migration, package, env var, queue, scheduler, storage, deployment asset, compatibility route, or legacy alias support is added.
|
||||
|
||||
## Required Tests
|
||||
|
||||
- `it('documents_evidence_audit_log_repo_truth_map')`
|
||||
- `it('renders_evidence_overview_proof_first_layout')`
|
||||
- `it('renders_audit_log_event_proof_first_layout')`
|
||||
- `it('shows_evidence_path_without_raw_metadata_by_default')`
|
||||
- `it('shows_audit_actor_action_target_outcome_time_before_raw_metadata')`
|
||||
- `it('shows_export_or_report_availability_only_when_repo_supported')`
|
||||
- `it('hides_evidence_and_audit_raw_diagnostics_by_default')`
|
||||
- `it('respects_evidence_audit_and_diagnostics_capabilities')`
|
||||
- `it('evidence_overview_supports_canonical_environment_filter')`
|
||||
- `it('audit_log_supports_canonical_environment_filter')`
|
||||
- `it('evidence_and_audit_reject_legacy_environment_aliases')`
|
||||
- `it('evidence_and_audit_reject_cross_workspace_environment_filter')`
|
||||
- `it('evidence_and_audit_do_not_use_tenant_as_platform_context_copy')`
|
||||
- `tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php`
|
||||
|
||||
## Browser Verification Required
|
||||
|
||||
Screenshots may be saved under:
|
||||
|
||||
```text
|
||||
specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/
|
||||
```
|
||||
|
||||
Required screenshots:
|
||||
|
||||
- `evidence-overview-proof-workbench.png`
|
||||
- `evidence-overview-filtered.png`
|
||||
- `audit-log-event-proof-workbench.png`
|
||||
- `audit-log-filtered.png`
|
||||
|
||||
Optional screenshots:
|
||||
|
||||
- `evidence-overview-empty.png`
|
||||
- `audit-log-empty.png`
|
||||
- `evidence-overview-after-clear.png`
|
||||
- `audit-log-after-clear.png`
|
||||
|
||||
## Risks
|
||||
|
||||
- Existing Audit Log route currently includes environment-context middleware; implementation must verify it does not force Environment shell ownership or remembered fallback for a workspace hub.
|
||||
- Evidence route is duplicated in `routes/web.php`; implementation may leave it alone if harmless or document a bounded cleanup task if needed.
|
||||
- Audit selected-event detail currently renders `Technical metadata` directly; moving it behind disclosure must preserve authorized proof inspection.
|
||||
- Evidence proof path may not have all links for every environment. Unsupported links must render unavailable or be omitted.
|
||||
- Browser smoke may need focused fixtures to avoid broad lane cost.
|
||||
|
||||
## Assumptions
|
||||
|
||||
- No production data migration compatibility is needed under the repo's pre-production posture.
|
||||
- Evidence and audit data already persisted in the repo are sufficient for v1 productization.
|
||||
- Existing policies/capabilities are authoritative; new capability strings are not expected.
|
||||
- EN/DE localization is added only if implementation follows existing stable-copy localization patterns for these pages.
|
||||
|
||||
## Open Questions
|
||||
|
||||
No open question blocks implementation. Implementation must update this spec/plan/tasks first if repo truth shows a required backend, schema, capability, export, or route contract change.
|
||||
|
||||
## Follow-Up Spec Candidates
|
||||
|
||||
- Spec 330 - Environment Dashboard / Baseline Compare Productization.
|
||||
- Spec 331 - Restore Safety Workflow Productization.
|
||||
- Spec 332 - Provider Readiness Productization.
|
||||
|
||||
Do not start these inside Spec 329.
|
||||
205
specs/329-evidence-audit-log-disclosure-productization/tasks.md
Normal file
@ -0,0 +1,205 @@
|
||||
# Tasks: Spec 329 - Evidence / Audit Log Disclosure Productization
|
||||
|
||||
**Input**: Design documents from `/specs/329-evidence-audit-log-disclosure-productization/`
|
||||
**Prerequisites**: `spec.md`, `plan.md`, `repo-truth-map.md`
|
||||
|
||||
**Tests**: Required. This is a runtime UI/operator proof-disclosure Filament page productization with browser smoke.
|
||||
|
||||
## Test Governance Checklist
|
||||
|
||||
- [x] Lane assignment is named and is the narrowest sufficient proof for the changed behavior.
|
||||
- [x] New or changed tests stay in the smallest honest family, and the browser addition is explicit.
|
||||
- [x] Shared helpers, factories, seeds, fixtures, and context defaults stay cheap by default.
|
||||
- [x] Planned validation commands cover the change without pulling in unrelated lane cost.
|
||||
- [x] The declared surface test profile (`global-context-shell`, `monitoring-state-page`, `shared-detail-family`) is explicit.
|
||||
- [x] Any material budget, baseline, trend, or escalation note is recorded in the active spec or PR.
|
||||
|
||||
## Phase 1: Preparation And Repo Truth
|
||||
|
||||
**Purpose**: Confirm runtime truth and prevent invented claims before page edits.
|
||||
|
||||
- [x] T001 Re-read `specs/329-evidence-audit-log-disclosure-productization/spec.md`, `plan.md`, `tasks.md`, and `repo-truth-map.md`.
|
||||
- [x] T002 Re-read related completed context only: Specs 314-328. Do not modify their artifacts.
|
||||
- [x] T003 Verify current Evidence Overview route/class/view and existing tests before editing: `apps/platform/routes/web.php`, `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php`, `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php`, and `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php`.
|
||||
- [x] T004 Verify current Audit Log route/class/view/partial and existing tests before editing: `apps/platform/routes/web.php`, `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php`, `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php`, `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php`, and `apps/platform/tests/Feature/Filament/AuditLogPageTest.php`.
|
||||
- [x] T005 Verify source models and authorization paths: `EvidenceSnapshot`, `ReviewPack`, `StoredReport`, `OperationRun`, `AuditLog`, `EvidenceSnapshotPolicy`, `ReviewPackPolicy`, `StoredReportResource`, `OperationRunLinks`, and capability resolvers.
|
||||
- [x] T006 Update `repo-truth-map.md` with any newly discovered source, capability, fallback, or classification before runtime changes.
|
||||
- [x] T007 Confirm no migration/package/env/queue/storage/deployment asset change is required; if one appears necessary, stop and update spec/plan first.
|
||||
- [x] T008 Confirm Filament v5 / Livewire v4.0+ compliance and no Livewire v3/Filament legacy API use.
|
||||
- [x] T009 Confirm panel provider registration remains `apps/platform/bootstrap/providers.php`.
|
||||
- [x] T010 Confirm related globally searchable resources stay disabled or have safe View/Edit pages; no global search change is expected.
|
||||
- [x] T011 Verify the duplicated `/admin/evidence/overview` route and Audit Log environment-context middleware do not create scope/shell drift; document any bounded cleanup in spec artifacts before code changes if needed.
|
||||
|
||||
## Phase 2: Feature Tests First
|
||||
|
||||
**Purpose**: Lock proof-first layout, event-first-read, RBAC, scope, and diagnostics behavior before UI refactor.
|
||||
|
||||
- [x] T012 Add or update a feature test asserting `specs/329-evidence-audit-log-disclosure-productization/repo-truth-map.md` exists and lists required Evidence Overview and Audit Log data areas.
|
||||
- [x] T013 Add or update a Feature/Livewire/HTTP test for Evidence Overview layout text: `Evidence`, `What proof is available for this scope?`, `Evidence path`, `Review pack`, `Operation proof`, and `Diagnostics - Collapsed` in `apps/platform/tests/Feature/Evidence/EvidenceOverviewPageTest.php` or a focused Spec 329 monitoring test.
|
||||
- [x] T014 Add or update a Feature/Livewire/HTTP test for Audit Log layout text: `Audit Log`, `Which event proves what happened?`, `Actor`, `Action`, `Target`, `Outcome`, `Time`, and `Diagnostics - Collapsed`.
|
||||
- [x] T015 Add or update a test asserting Evidence Overview shows `Evidence snapshot`, `Review pack`, `Operation proof`, and `Stored report / export` without default-visible raw metadata.
|
||||
- [x] T016 Add or update a test asserting Audit Log shows actor/action/target/outcome/time before raw metadata for a selected event.
|
||||
- [x] T017 Add or update a test asserting export/report availability uses only repo-supported states such as `Available`, `Unavailable`, `Not generated`, or `Not applicable`; no fake download/export action appears.
|
||||
- [x] T018 Add or update a test asserting raw diagnostics are hidden by default on both pages: `raw payload`, `provider secret`, `stack trace`, `debug metadata`, `internal exception`, `provider response`, and raw OperationRun context must not appear.
|
||||
- [x] T019 Add or update RBAC tests covering evidence snapshot open, review pack open/download, stored report open, audit event detail, operation proof, and raw diagnostics visibility where existing capabilities support coverage.
|
||||
- [x] T020 Add or update canonical Evidence Overview environment filter tests for `?environment_id=`, visible chip, workspace shell only, filtered proof data, clear filter, and reload safety.
|
||||
- [x] T021 Add or update canonical Audit Log environment filter tests for `?environment_id=`, visible chip, workspace shell only, filtered audit rows, selected-event normalization, clear filter, and reload safety.
|
||||
- [x] T022 Add or update legacy alias rejection tests for Evidence Overview and Audit Log covering `tenant`, `tenant_id`, `managed_environment_id`, `environment`, `tenant_scope`, and `tableFilters`.
|
||||
- [x] T023 Add or update cross-workspace environment filter guard tests returning safe 404/no-access for both Evidence Overview and Audit Log.
|
||||
- [x] T024 Add or update tenant-copy guard asserting platform-context copy such as `current tenant`, `tenant filter`, `entitled tenant`, `all tenants`, and `production tenant` is not visible on either page.
|
||||
|
||||
## Phase 3: Evidence Overview Productization
|
||||
|
||||
**Purpose**: Refactor Evidence Overview from table-first to proof-first without new backend foundation.
|
||||
|
||||
- [x] T025 Update `apps/platform/app/Filament/Pages/Monitoring/EvidenceOverview.php` to expose repo-truth-bounded payloads for scope, proof readiness, evidence path, export/report availability, proof links, unavailable states, and diagnostics disclosure.
|
||||
- [x] T026 Update `apps/platform/resources/views/filament/pages/monitoring/evidence-overview.blade.php` to render header/scope, proof readiness workbench, evidence path panel, export/report availability panel, secondary table, and collapsed diagnostics disclosure.
|
||||
- [x] T027 Ensure Evidence Overview shows workspace-wide vs environment-filtered context and the shared Environment chip when filtered.
|
||||
- [x] T028 Ensure the main proof workbench shows the stable question, evidence availability, freshness, snapshot state, review pack/export state, stored report/export state, operation proof state, and one dominant open-proof action when authorized.
|
||||
- [x] T029 Ensure evidence path items show only honest states: available, unavailable, stale, not generated, not applicable, or omitted.
|
||||
- [x] T030 Keep the existing Evidence Overview table available as secondary context; do not remove existing search/filter/sort/row navigation functionality.
|
||||
- [x] T031 Ensure Evidence Overview diagnostics/raw metadata are collapsed, hidden, or capability-gated by default.
|
||||
|
||||
## Phase 4: Audit Log Productization
|
||||
|
||||
**Purpose**: Refactor Audit Log from summary/table-first to event-proof-first while preserving event history.
|
||||
|
||||
- [x] T032 Update `apps/platform/app/Filament/Pages/Monitoring/AuditLog.php` to expose repo-truth-bounded payloads for audit proof workbench, selected/latest event proof, related proof links, unavailable states, disclosure hierarchy, and diagnostics gating.
|
||||
- [x] T033 Update `apps/platform/resources/views/filament/pages/monitoring/audit-log.blade.php` to render header/scope, audit proof workbench, selected/latest event proof panel, secondary table, and collapsed diagnostics disclosure.
|
||||
- [x] T034 Update `apps/platform/resources/views/filament/pages/monitoring/partials/audit-log-inspect-event.blade.php` so actor/action/target/outcome/time/scope and related proof are first-read, while technical metadata is behind collapsed/capability-aware disclosure.
|
||||
- [x] T035 Ensure Audit Log shows workspace-wide vs environment-filtered context and the shared Environment chip when filtered.
|
||||
- [x] T036 Ensure selected event proof normalizes against active filter/search/authorization and falls back safely when the event is invalid, inaccessible, or outside scope.
|
||||
- [x] T037 Keep the existing Audit Log table available as secondary event history; do not remove existing filters/search/sort/inspect behavior.
|
||||
- [x] T038 Ensure Audit Log diagnostics/raw metadata are collapsed, hidden, or capability-gated by default.
|
||||
|
||||
## Phase 5: Data Binding And Honest States
|
||||
|
||||
**Purpose**: Bind proof surfaces to repo-verified sources and avoid false claims.
|
||||
|
||||
- [x] T039 Bind evidence snapshot display to `EvidenceSnapshot` fields, `ArtifactTruthPresenter`, and existing snapshot detail links only.
|
||||
- [x] T040 Bind review pack state to existing `ReviewPack` fields/statuses and `ReviewPackResource`/download links only where authorized.
|
||||
- [x] T041 Bind stored report state to existing `StoredReport` records, report-type capabilities, and `StoredReportResource` links only where authorized.
|
||||
- [x] T042 Bind operation proof state only through existing `operationRun()` relations, `OperationRunLinks`, and authorized operation visibility.
|
||||
- [x] T043 Bind audit event proof to `AuditLog` actor snapshot, action/action label, target snapshot, normalized outcome, recorded time, scope, readable context, operation relation, and related navigation resolver.
|
||||
- [x] T044 Render unavailable/missing/not generated/not applicable states for unsupported proof paths rather than inventing backend capabilities.
|
||||
- [x] T045 Ensure no generic green success state, immutable/certified/compliance-ready copy, or environment/governance health claim appears without exact repo proof.
|
||||
|
||||
## Phase 6: Actions, RBAC, And Safety
|
||||
|
||||
**Purpose**: Show only real, authorized actions and preserve read-first default behavior.
|
||||
|
||||
- [x] T046 Keep primary actions singular and context-aware on each proof panel.
|
||||
- [x] T047 Show open evidence snapshot, open review pack, download/open export artifact, open stored report, open operation proof, open audit event, or open related record only when route and authorization are repo-real.
|
||||
- [x] T048 Ensure unauthorized actions are hidden or replaced with safe unavailable state without leaking sensitive details.
|
||||
- [x] T049 Ensure raw diagnostics/metadata disclosure is unavailable without `support_diagnostics.view` or stricter existing raw/support capability.
|
||||
- [x] T050 Verify no default action approves, rejects, accepts risk, deletes, restores, remediates, mutates provider state, or changes evidence/audit storage.
|
||||
- [x] T051 If any high-impact action is unexpectedly required, update spec/plan first, then implement it with `Action::make(...)->action(...)`, `->requiresConfirmation()`, server-side authorization, audit, notification, and tests.
|
||||
|
||||
## Phase 7: Workspace / Environment Scope Contract
|
||||
|
||||
**Purpose**: Preserve Specs 314-322.
|
||||
|
||||
- [x] T052 Verify clean `/admin/evidence/overview` and `/admin/audit-log` do not read remembered environment shell state or persisted table filters.
|
||||
- [x] T053 Verify `/admin/evidence/overview?environment_id={id}` and `/admin/audit-log?environment_id={id}` filter only page data, show visible chip, and keep Workspace shell ownership.
|
||||
- [x] T054 Verify clear filter redirects to clean workspace URL and remains safe after reload.
|
||||
- [x] T055 Verify legacy aliases are removed/neutralized and do not set filter state.
|
||||
- [x] T056 Verify cross-workspace or unauthorized `environment_id` returns safe no-access/404.
|
||||
- [x] T057 Verify back/forward/reload behavior does not resurrect cleared environment filter state.
|
||||
- [x] T058 Verify Audit Log route middleware does not force active Environment shell ownership or remembered fallback; if it does, apply the narrowest route/middleware correction in scope and cover it with tests.
|
||||
|
||||
## Phase 8: Browser Smoke And Screenshots
|
||||
|
||||
**Purpose**: Prove the user-facing contract in the integrated browser lane.
|
||||
|
||||
- [x] T059 Create `apps/platform/tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php` using existing Pest Browser conventions.
|
||||
- [x] T060 Browser Flow A: Evidence Overview clean workspace; assert Workspace shell only, no Environment chip, main proof question, proof workbench, evidence path, diagnostics collapsed, table secondary, screenshot.
|
||||
- [x] T061 Browser Flow B: Evidence Overview filtered entry; assert visible Environment chip, filtered proof state, clear action, no active Environment shell, screenshot.
|
||||
- [x] T062 Browser Flow C: Evidence clear filter and reload; assert clean URL, chip does not return, no active Environment shell.
|
||||
- [x] T063 Browser Flow D: Evidence non-empty and empty proof states; assert available/unavailable/not generated states and no raw metadata.
|
||||
- [x] T064 Browser Flow E: Audit Log clean workspace; assert Workspace shell only, no Environment chip, audit proof question, actor/action/target/outcome/time first-read, diagnostics collapsed, table secondary, screenshot.
|
||||
- [x] T065 Browser Flow F: Audit Log filtered entry; assert visible Environment chip, filtered event proof, clear action, no active Environment shell, screenshot.
|
||||
- [x] T066 Browser Flow G: Audit clear filter and reload; assert clean URL, chip does not return, no active Environment shell.
|
||||
- [x] T067 Browser Flow H: Audit non-empty and empty event states; assert selected/latest event proof and no raw metadata.
|
||||
- [x] T068 Browser Flow I: no platform-context tenant wording appears on either surface.
|
||||
- [x] T069 Save screenshots under `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/` when generated and ensure they contain no secrets.
|
||||
|
||||
## Phase 9: UI Coverage And Documentation Artifacts
|
||||
|
||||
**Purpose**: Satisfy UI-COV without unrelated docs churn.
|
||||
|
||||
- [x] T070 Decide after runtime diff whether `docs/ui-ux-enterprise-audit/route-inventory.md`, `design-coverage-matrix.md`, page reports, or unresolved pages need an update.
|
||||
- [x] T071 If coverage docs are not changed, add a close-out note explaining why existing UI-025/UI-044 rows plus Spec 325 target artifacts and Spec 329 package artifacts remain sufficient.
|
||||
- [x] T072 Update `repo-truth-map.md` final classifications for implemented/empty/deferred elements.
|
||||
- [x] T073 Do not create general documentation files outside required Spec Kit/UI coverage artifacts unless explicitly requested.
|
||||
- [x] T081 Add the existing Evidence Overview route to the Workspace Monitoring sidebar through both workspace navigation paths with a concise area label and cover the navigation entry with existing workspace-hub sidebar regression tests.
|
||||
|
||||
## Phase 10: Validation
|
||||
|
||||
**Purpose**: Run narrow proof and report honestly.
|
||||
|
||||
- [x] T074 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Feature/Monitoring tests/Feature/Evidence tests/Feature/Audit tests/Feature/Navigation/WorkspaceHubEnvironmentFilterContractTest.php tests/Feature/Navigation/WorkspaceHubClearFilterContractTest.php --compact`.
|
||||
- [x] T075 Run `cd apps/platform && ./vendor/bin/sail artisan test tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php --compact`.
|
||||
- [x] T076 Run `cd apps/platform && ./vendor/bin/sail artisan test --filter='Evidence|AuditLog|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact`.
|
||||
- [x] T077 Run `cd apps/platform && ./vendor/bin/sail pint --dirty`.
|
||||
- [x] T078 Run `git diff --check`.
|
||||
- [x] T079 Report full-suite status honestly if not run.
|
||||
- [x] T080 Confirm no migrations, seeders, packages, env vars, queues, scheduler, storage, deployment assets, backwards compatibility layer, or legacy tenant alias support were added.
|
||||
- [x] T082 Replace implementation-heavy empty-snapshot copy with product-safe proof copy and assert the old artifact-row wording is not visible.
|
||||
- [x] T083 Add empty-primary-snapshot proof hierarchy coverage for `Proof incomplete`, reason, and impact.
|
||||
- [x] T084 Keep dynamic display names containing `Tenant` allowed while rejecting static `Search tenant or next` copy.
|
||||
- [x] T085 Prevent clipped Evidence Path badge labels in the right panel and cover `Empty`, `Ready`, and `Available` labels in Feature/Browser tests.
|
||||
|
||||
### Validation Close-Out
|
||||
|
||||
- Focused Spec 329 and impacted Feature tests passed: `./vendor/bin/sail artisan test tests/Feature/Monitoring/Spec329EvidenceAuditDisclosureProductizationTest.php tests/Feature/Filament/AuditLogPageTest.php tests/Feature/Filament/AuditLogDetailInspectionTest.php tests/Feature/Monitoring/AuditLogInspectFlowTest.php tests/Feature/Evidence/EvidenceOverviewPageTest.php --compact`.
|
||||
- Spec 329 Browser smoke passed: `./vendor/bin/sail artisan test tests/Browser/Spec329EvidenceAuditDisclosureSmokeTest.php --compact`.
|
||||
- Neighboring navigation/browser contracts passed: Spec 198, Spec 322, Spec 321, WorkspaceHubEnvironmentFilter, and WorkspaceHubClearFilter targeted run.
|
||||
- Filter-based validation passed: `./vendor/bin/sail artisan test --filter='Evidence|AuditLog|WorkspaceHub|EnvironmentFilter|ClearFilter|LegacyTenant|Spec322' --compact` with 352 passed, 1 skipped, 4,220 assertions.
|
||||
- Broad Feature lane was run and had one unrelated pre-existing failure in `tests/Feature/Audit/ProviderConnectionIdentityAuditTest.php` (`getDefaultTestingSchemaName()` on null). The same test failed in isolation; no provider-connection audit code was changed for Spec 329.
|
||||
- `./vendor/bin/sail pint --dirty` passed.
|
||||
- `git diff --check` passed.
|
||||
- Screenshots are stored under `specs/329-evidence-audit-log-disclosure-productization/artifacts/screenshots/`.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Phase 1 blocks all runtime implementation.
|
||||
- Phase 2 should be written before or alongside implementation to lock behavior.
|
||||
- Phase 3 and Phase 4 can be implemented in parallel only if write scopes stay disjoint:
|
||||
- Evidence write scope: Evidence page class/view/tests.
|
||||
- Audit write scope: Audit page class/view/partial/tests.
|
||||
- Phase 5 and Phase 6 depend on Phases 3-4 payload shape.
|
||||
- Phase 7 must be validated after both surfaces are changed.
|
||||
- Phase 8 depends on user-facing runtime changes.
|
||||
- Phase 10 is final validation.
|
||||
|
||||
## Non-Goals Checklist
|
||||
|
||||
- [x] NT001 Do not build a new evidence backend.
|
||||
- [x] NT002 Do not build a new audit ingestion engine.
|
||||
- [x] NT003 Do not build immutable/legal/certification/compliance attestation.
|
||||
- [x] NT004 Do not build a new export/report generation engine.
|
||||
- [x] NT005 Do not add AI summarization.
|
||||
- [x] NT006 Do not redesign Customer Review Workspace, Governance Inbox, Operations Hub, Environment Dashboard, Baseline Compare, Restore Safety, or Provider Readiness.
|
||||
- [x] NT007 Do not add migrations unless spec/plan are updated first with proof.
|
||||
- [x] NT008 Do not rewrite completed Specs 314-328.
|
||||
- [x] NT009 Do not add legacy tenant query alias support.
|
||||
- [x] NT010 Do not expose raw diagnostics or provider payloads by default.
|
||||
|
||||
## Required Final Report Content
|
||||
|
||||
When implementation later completes, report:
|
||||
|
||||
- Changed behavior.
|
||||
- Evidence Overview proof surface.
|
||||
- Audit Log event-proof surface.
|
||||
- Disclosure / diagnostics default state.
|
||||
- RBAC-visible/hidden actions.
|
||||
- Repo-verified vs unavailable states.
|
||||
- Files changed.
|
||||
- Repo truth map status.
|
||||
- Tests run and results.
|
||||
- Browser verification and screenshots path.
|
||||
- Known gaps.
|
||||
- Remaining follow-ups.
|
||||
- Full suite run/not run.
|
||||
- Explicit no migrations/seeders/packages/env/queues/scheduler/storage/deployment assets/backcompat/legacy aliases statement.
|
||||