diff --git a/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-source-catalog.component.ts b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-source-catalog.component.ts index 973f1fa89..90c7710a2 100644 --- a/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-source-catalog.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/integrations/advisory-vex-sources/advisory-source-catalog.component.ts @@ -14,6 +14,7 @@ import { SourceStatusItem, SourceConnectivityResultDto, } from './source-management.api'; +import { buildMirrorCommands, getAdvisoryVexNavigationExtras } from './advisory-vex-route-helpers'; const CATEGORY_ORDER = [ 'Primary', @@ -83,8 +84,8 @@ interface CategoryGroup { [class]="'mirror-mode-badge mirror-mode-badge--' + mirrorConfig()!.mode.toLowerCase()"> {{ mirrorConfig()!.mode }} - Configure Mirror - Connect to Mirror + Configure Mirror + Connect to Mirror
@if (isConsumerMode()) { @@ -1053,7 +1054,15 @@ export class AdvisorySourceCatalogComponent implements OnInit { } onCreateMirrorDomain(): void { - this.router.navigate(['/integrations', 'advisory-vex-sources', 'mirror', 'new']); + this.router.navigate(buildMirrorCommands(this.router, 'new'), getAdvisoryVexNavigationExtras()); + } + + mirrorDashboardLink(): string[] { + return buildMirrorCommands(this.router); + } + + mirrorClientSetupLink(): string[] { + return buildMirrorCommands(this.router, 'client-setup'); } toggleExpanded(sourceId: string): void { @@ -1124,19 +1133,65 @@ export class AdvisorySourceCatalogComponent implements OnInit { onCheckAll(): void { const items = this.catalog(); - this.checking.set(true); - this.checkProgress.set({ done: 0, total: items.length }); + const enabledIds = items + .filter((item) => this.isSourceEnabled(item.id)) + .map((item) => item.id); - this.api.checkAll().pipe(take(1)).subscribe({ - next: () => { + if (enabledIds.length === 0) { + return; + } + + this.checking.set(true); + this.checkProgress.set({ done: 0, total: enabledIds.length }); + + // Check sources individually in parallel batches of 6 to avoid gateway timeout. + // The batch /check endpoint times out at 30s when checking 40+ sources. + const batchSize = 6; + let completed = 0; + + const checkNext = (startIndex: number): void => { + const batch = enabledIds.slice(startIndex, startIndex + batchSize); + if (batch.length === 0) { this.checking.set(false); this.reloadStatus(); - }, - error: () => { - this.checking.set(false); - this.reloadStatus(); - }, - }); + return; + } + + let batchDone = 0; + for (const sourceId of batch) { + this.api.checkSource(sourceId).pipe(take(1)).subscribe({ + next: (result) => { + this.statuses.update((current) => { + const next = new Map(current); + const existing = next.get(sourceId); + if (existing) { + next.set(sourceId, { ...existing, lastCheck: result }); + } else { + next.set(sourceId, { sourceId, enabled: true, lastCheck: result }); + } + return next; + }); + + completed++; + batchDone++; + this.checkProgress.set({ done: completed, total: enabledIds.length }); + if (batchDone === batch.length) { + checkNext(startIndex + batchSize); + } + }, + error: () => { + completed++; + batchDone++; + this.checkProgress.set({ done: completed, total: enabledIds.length }); + if (batchDone === batch.length) { + checkNext(startIndex + batchSize); + } + }, + }); + } + }; + + checkNext(0); } onCheckSource(sourceId: string): void {