feat: productize evidence and audit log disclosure
Some checks failed
PR Fast Feedback / fast-feedback (pull_request) Failing after 1m40s

This commit is contained in:
Ahmed Darrazi 2026-05-19 23:24:00 +02:00
parent 815262399a
commit ebb68ca9bb
32 changed files with 3304 additions and 115 deletions

View File

@ -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();

View File

@ -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

View File

@ -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')

View File

@ -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'),

View File

@ -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.',
};

View File

@ -89,6 +89,7 @@
'integrations' => 'Integrationen',
'manage_workspaces' => 'Workspaces verwalten',
'operations' => 'Operationen',
'evidence_overview' => 'Nachweise',
'audit_log' => 'Audit-Log',
'alerts' => 'Alerts',
'governance' => 'Governance',

View File

@ -89,6 +89,7 @@
'integrations' => 'Integrations',
'manage_workspaces' => 'Manage workspaces',
'operations' => 'Operations',
'evidence_overview' => 'Evidence',
'audit_log' => 'Audit Log',
'alerts' => 'Alerts',
'governance' => 'Governance',

View File

@ -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>

View File

@ -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>

View File

@ -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

View File

@ -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',

View File

@ -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()

View File

@ -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);
}
}

View File

@ -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');
});

View File

@ -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');
});

View File

@ -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'],

View File

@ -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 {

View File

@ -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];
}

View File

@ -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);
});

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 169 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 222 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 205 KiB

View File

@ -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.

View 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.

View File

@ -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.

View 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.

View 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.