Fix medium issues: user ID display, mirror toast, post-seal guidance

Fix #14 — User ID hash display:
  Add formatActor() to bundle-version-detail that truncates raw hex
  hashes to "User 209d1257..." instead of showing the full 32-char ID.
  Short IDs, emails, and names pass through unchanged.

Fix #15 — Mirror generate-immediately silent failure:
  When domain creation succeeds but bundle generation fails, show a
  meaningful status message instead of silently swallowing the error:
  "Generation failed — the domain was created but the initial bundle
  could not be generated. You can retry from the mirror dashboard."

Fix #19 — Wizard error handling:
  Already implemented — topology wizard sets wizard.error signal on all
  CRUD failures and displays error banner. No additional changes needed.

Fix #21 — Post-seal guidance:
  After sealing a release (source=release-create query param), show a
  "What's next?" panel with links to: Promote to environment, Review
  approvals, Back to versions. Uses branded action buttons.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
master
2026-03-16 10:45:16 +02:00
parent c13e47dbcb
commit 53157ebb0b
2 changed files with 66 additions and 2 deletions

View File

@@ -41,7 +41,7 @@ import {
</article>
<article>
<h2>Created by</h2>
<p>{{ versionDetailModel.createdBy }}</p>
<p>{{ formatActor(versionDetailModel.createdBy) }}</p>
</article>
<article>
<h2>Promotion readiness</h2>
@@ -49,6 +49,17 @@ import {
</article>
</section>
@if (showPostSealGuide()) {
<section class="bvd__post-seal" aria-label="Next steps">
<h3>Release sealed successfully. What's next?</h3>
<div class="bvd__post-seal-actions">
<a [routerLink]="['/releases/promotions']" queryParamsHandling="merge" class="bvd__action-link">Promote to environment</a>
<a [routerLink]="['/releases/approvals']" queryParamsHandling="merge" class="bvd__action-link">Review approvals</a>
<a [routerLink]="['/releases/versions']" queryParamsHandling="merge" class="bvd__action-link">Back to versions</a>
</div>
</section>
}
<div class="bvd__tabs" role="tablist">
<button role="tab" [class.tab--active]="activeTab() === 'components'" (click)="setTab('components')">Components</button>
<button role="tab" [class.tab--active]="activeTab() === 'validation'" (click)="setTab('validation')">Validation</button>
@@ -207,6 +218,36 @@ import {
font-size: 0.84rem;
}
.bvd__post-seal {
padding: 1rem 1.25rem;
border: 1px solid var(--color-brand-primary, #6366f1);
border-radius: 8px;
background: var(--color-surface-secondary, #f8fafc);
margin-bottom: 1rem;
}
.bvd__post-seal h3 {
margin: 0 0 0.75rem;
font-size: 1rem;
font-weight: 600;
}
.bvd__post-seal-actions {
display: flex;
gap: 0.75rem;
flex-wrap: wrap;
}
.bvd__action-link {
padding: 0.45rem 1rem;
border-radius: 6px;
font-size: 0.85rem;
font-weight: 500;
text-decoration: none;
background: var(--color-brand-primary, #6366f1);
color: #fff;
}
.bvd__tabs {
display: flex;
border-bottom: 2px solid var(--color-border, #e5e7eb);
@@ -366,6 +407,7 @@ export class BundleVersionDetailComponent implements OnInit {
readonly versionId = signal('');
readonly activeTab = signal<'components' | 'validation' | 'releases'>('components');
readonly loading = signal(true);
readonly showPostSealGuide = signal(false);
readonly errorMessage = signal<string | null>(null);
readonly versionDetail = signal<ReleaseControlBundleVersionDetailDto | null>(null);
readonly materializing = signal(false);
@@ -379,6 +421,12 @@ export class BundleVersionDetailComponent implements OnInit {
this.route.snapshot.params['versionId'] ?? this.route.snapshot.params['version'] ?? ''
);
// Show post-seal guidance when arriving from the release creation wizard
const source = this.route.snapshot.queryParams['source'];
if (source === 'release-create') {
this.showPostSealGuide.set(true);
}
if (!this.bundleId() || !this.versionId()) {
this.loading.set(false);
this.errorMessage.set('Bundle or version id is missing from route.');
@@ -435,6 +483,16 @@ export class BundleVersionDetailComponent implements OnInit {
return `${digest.slice(0, 20)}...`;
}
formatActor(actorId: string): string {
if (!actorId) return 'Unknown';
// Short readable IDs or well-known names pass through
if (actorId.length <= 20 || actorId.includes('@') || actorId.includes(' ')) {
return actorId;
}
// Raw hex hashes get truncated with a user indicator
return `User ${actorId.slice(0, 8)}...`;
}
promotionReadiness(status: string): string {
return status.toLowerCase() === 'published'
? 'Ready after validation and materialization checks.'

View File

@@ -1446,7 +1446,13 @@ export class MirrorDomainBuilderComponent implements OnInit {
this.creating.set(false);
},
error: () => {
// Domain was created but generate failed -- still show success
// Domain was created but generate failed
this.generateResult.set({
domainId: domain.domainId,
jobId: 'failed',
status: 'Generation failed — the domain was created but the initial bundle could not be generated. You can retry from the mirror dashboard.',
startedAt: new Date().toISOString(),
});
this.creating.set(false);
},
});