diff --git a/src/Web/StellaOps.Web/scripts/live-frontdoor-canonical-route-sweep.mjs b/src/Web/StellaOps.Web/scripts/live-frontdoor-canonical-route-sweep.mjs index 87bc7b610..254b7e3a4 100644 --- a/src/Web/StellaOps.Web/scripts/live-frontdoor-canonical-route-sweep.mjs +++ b/src/Web/StellaOps.Web/scripts/live-frontdoor-canonical-route-sweep.mjs @@ -41,6 +41,9 @@ const canonicalRoutes = [ '/releases/hotfixes/new', '/releases/environments', '/releases/deployments', + '/releases/investigation/timeline', + '/releases/investigation/deploy-diff', + '/releases/investigation/change-trace', '/security', '/security/posture', '/security/triage', @@ -54,6 +57,7 @@ const canonicalRoutes = [ '/evidence', '/evidence/overview', '/evidence/capsules', + '/evidence/threads', '/evidence/verify-replay', '/evidence/proofs', '/evidence/exports', @@ -88,6 +92,7 @@ const canonicalRoutes = [ '/ops/integrations/notifications', '/ops/integrations/sbom-sources', '/ops/integrations/activity', + '/ops/integrations/registry-admin', '/ops/policy', '/ops/policy/overview', '/ops/policy/baselines', diff --git a/src/Web/StellaOps.Web/src/app/app.config.ts b/src/Web/StellaOps.Web/src/app/app.config.ts index f411928f9..37670a3b6 100644 --- a/src/Web/StellaOps.Web/src/app/app.config.ts +++ b/src/Web/StellaOps.Web/src/app/app.config.ts @@ -19,7 +19,6 @@ import { import { NOTIFY_API, NOTIFY_API_BASE_URL, - NOTIFY_TENANT_ID, NotifyApiHttpClient, MockNotifyClient, } from './core/api/notify.client'; @@ -262,6 +261,28 @@ import { MockIdentityProviderClient, } from './core/api/identity-provider.client'; +function resolveApiBaseUrl(baseUrl: string | undefined, path: string): string { + const normalizedBase = (baseUrl ?? '').trim(); + + if (!normalizedBase) { + return path; + } + + try { + return new URL(path, normalizedBase).toString(); + } catch { + if (path.startsWith('/')) { + return path; + } + + const baseWithoutTrailingSlash = normalizedBase.endsWith('/') + ? normalizedBase.slice(0, -1) + : normalizedBase; + + return `${baseWithoutTrailingSlash}/${path.replace(/^\/+/, '')}`; + } +} + export const appConfig: ApplicationConfig = { providers: [ provideRouter(routes, withComponentInputBinding()), @@ -306,15 +327,8 @@ export const appConfig: ApplicationConfig = { provide: AUTHORITY_CONSOLE_API_BASE_URL, deps: [AppConfigService], useFactory: (config: AppConfigService) => { - const authorityBase = config.config.apiBaseUrls.authority; - try { - return new URL('/console', authorityBase).toString(); - } catch { - const normalized = authorityBase.endsWith('/') - ? authorityBase.slice(0, -1) - : authorityBase; - return `${normalized}/console`; - } + const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority; + return resolveApiBaseUrl(gatewayBase, '/console'); }, }, AuthorityConsoleApiHttpClient, @@ -539,7 +553,7 @@ export const appConfig: ApplicationConfig = { deps: [AppConfigService], useFactory: (config: AppConfigService) => { const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority; - return gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase; + return resolveApiBaseUrl(gatewayBase, '/api/v1'); }, }, OrchestratorHttpClient, @@ -617,12 +631,7 @@ export const appConfig: ApplicationConfig = { deps: [AppConfigService], useFactory: (config: AppConfigService) => { const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority; - try { - return new URL('/v1/runs', gatewayBase).toString(); - } catch { - const normalized = gatewayBase.endsWith('/') ? gatewayBase.slice(0, -1) : gatewayBase; - return `${normalized}/v1/runs`; - } + return resolveApiBaseUrl(gatewayBase, '/api/v1/advisory-ai/runs'); }, }, AiRunsHttpClient, @@ -635,25 +644,14 @@ export const appConfig: ApplicationConfig = { provide: CONSOLE_API_BASE_URL, deps: [AppConfigService], useFactory: (config: AppConfigService) => { - const authorityBase = config.config.apiBaseUrls.authority; - try { - return new URL('/console', authorityBase).toString(); - } catch { - const normalized = authorityBase.endsWith('/') - ? authorityBase.slice(0, -1) - : authorityBase; - return `${normalized}/console`; - } + const gatewayBase = config.config.apiBaseUrls.gateway ?? config.config.apiBaseUrls.authority; + return resolveApiBaseUrl(gatewayBase, '/console'); }, }, { provide: EVENT_SOURCE_FACTORY, useValue: DEFAULT_EVENT_SOURCE_FACTORY, }, - { - provide: NOTIFY_TENANT_ID, - useValue: 'tenant-dev', - }, NotifyApiHttpClient, MockNotifyClient, { diff --git a/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.spec.ts new file mode 100644 index 000000000..398871002 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.spec.ts @@ -0,0 +1,49 @@ +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +import { AuthSessionStore } from '../auth/auth-session.store'; +import { EvidencePackHttpClient, EVIDENCE_PACK_API_BASE_URL } from './evidence-pack.client'; + +class FakeAuthSessionStore { + getActiveTenantId(): string | null { + return 'demo-prod'; + } +} + +describe('EvidencePackHttpClient', () => { + let httpMock: HttpTestingController; + let client: EvidencePackHttpClient; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + EvidencePackHttpClient, + { provide: AuthSessionStore, useClass: FakeAuthSessionStore }, + { provide: EVIDENCE_PACK_API_BASE_URL, useValue: '/v1/evidence-packs' }, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }); + + httpMock = TestBed.inject(HttpTestingController); + client = TestBed.inject(EvidencePackHttpClient); + }); + + afterEach(() => { + httpMock.verify(); + }); + + it('lists evidence packs for a run through the public collection route', () => { + client.listByRun('run-42', { traceId: 'trace-ep-42' }).subscribe((response) => { + expect(response.count).toBe(0); + }); + + const request = httpMock.expectOne((pending) => pending.url === '/v1/evidence-packs'); + expect(request.request.method).toBe('GET'); + expect(request.request.params.get('runId')).toBe('run-42'); + expect(request.request.headers.get('X-StellaOps-Tenant')).toBe('demo-prod'); + expect(request.request.headers.get('X-Stella-Trace-Id')).toBe('trace-ep-42'); + request.flush({ count: 0, packs: [] }); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.ts b/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.ts index 79acdb7e7..27fa4acce 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/evidence-pack.client.ts @@ -91,8 +91,9 @@ export class EvidencePackHttpClient implements EvidencePackApi { listByRun(runId: string, options: EvidencePackQueryOptions = {}): Observable { const traceId = options.traceId ?? generateTraceId(); + const params = this.buildQueryParams({ runId }); return this.http - .get(`/v1/runs/${runId}/evidence-packs`, { headers: this.buildHeaders(traceId) }) + .get(this.baseUrl, { headers: this.buildHeaders(traceId), params }) .pipe(catchError((err) => throwError(() => this.mapError(err, traceId)))); } diff --git a/src/Web/StellaOps.Web/src/app/core/api/notify.client.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/notify.client.spec.ts new file mode 100644 index 000000000..3199a0fd4 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/api/notify.client.spec.ts @@ -0,0 +1,68 @@ +import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; +import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http'; + +import { AuthSessionStore } from '../auth/auth-session.store'; +import { TenantActivationService } from '../auth/tenant-activation.service'; +import { NOTIFY_API_BASE_URL, NOTIFY_TENANT_ID, NotifyApiHttpClient } from './notify.client'; + +class FakeAuthSessionStore { + getActiveTenantId(): string | null { + return 'session-tenant'; + } +} + +class FakeTenantActivationService { + activeTenantId(): string | null { + return 'demo-prod'; + } +} + +describe('NotifyApiHttpClient', () => { + let httpMock: HttpTestingController; + let client: NotifyApiHttpClient; + + function configure(optionalProviders: unknown[] = []): void { + TestBed.configureTestingModule({ + providers: [ + NotifyApiHttpClient, + { provide: AuthSessionStore, useClass: FakeAuthSessionStore }, + { provide: TenantActivationService, useClass: FakeTenantActivationService }, + { provide: NOTIFY_API_BASE_URL, useValue: '/api/v1/notify' }, + ...optionalProviders, + provideHttpClient(withInterceptorsFromDi()), + provideHttpClientTesting(), + ], + }); + + httpMock = TestBed.inject(HttpTestingController); + client = TestBed.inject(NotifyApiHttpClient); + } + + afterEach(() => { + if (httpMock) { + httpMock.verify(); + } + TestBed.resetTestingModule(); + }); + + it('uses the active tenant context when no static override is supplied', () => { + configure(); + + client.listChannels().subscribe(); + + const request = httpMock.expectOne('/api/v1/notify/channels'); + expect(request.request.headers.get('X-StellaOps-Tenant')).toBe('demo-prod'); + request.flush([]); + }); + + it('prefers an explicit tenant override when one is supplied', () => { + configure([{ provide: NOTIFY_TENANT_ID, useValue: 'tenant-explicit' }]); + + client.listRules().subscribe(); + + const request = httpMock.expectOne('/api/v1/notify/rules'); + expect(request.request.headers.get('X-StellaOps-Tenant')).toBe('tenant-explicit'); + request.flush([]); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/core/api/notify.client.ts b/src/Web/StellaOps.Web/src/app/core/api/notify.client.ts index e2b19826c..ea2a1ca23 100644 --- a/src/Web/StellaOps.Web/src/app/core/api/notify.client.ts +++ b/src/Web/StellaOps.Web/src/app/core/api/notify.client.ts @@ -362,15 +362,16 @@ export class NotifyApiHttpClient implements NotifyApi { } private buildHeaders(): HttpHeaders { - if (!this.tenantId) { + const tenant = this.resolveTenantId(); + if (!tenant) { return new HttpHeaders(); } - return new HttpHeaders({ 'X-StellaOps-Tenant': this.tenantId }); + return new HttpHeaders({ 'X-StellaOps-Tenant': tenant }); } private buildHeadersWithTrace(traceId: string): HttpHeaders { - const tenant = this.tenantId || this.authSession.getActiveTenantId() || ''; + const tenant = this.resolveTenantId(); return new HttpHeaders({ 'X-StellaOps-Tenant': tenant, 'X-Stella-Trace-Id': traceId, @@ -379,6 +380,10 @@ export class NotifyApiHttpClient implements NotifyApi { }); } + private resolveTenantId(): string { + return this.tenantId || this.tenantService.activeTenantId() || this.authSession.getActiveTenantId() || ''; + } + private buildPaginationParams(options: NotifyQueryOptions): HttpParams { let params = new HttpParams(); if (options.pageToken) { @@ -720,4 +725,3 @@ export class MockNotifyClient implements NotifyApi { }).pipe(delay(100)); } } - diff --git a/src/Web/StellaOps.Web/src/app/core/api/pack-registry-browser.service.spec.ts b/src/Web/StellaOps.Web/src/app/core/api/pack-registry-browser.service.spec.ts new file mode 100644 index 000000000..5a6746b18 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/api/pack-registry-browser.service.spec.ts @@ -0,0 +1,83 @@ +import { TestBed } from '@angular/core/testing'; +import { firstValueFrom, of } from 'rxjs'; + +import { PackRegistryClient } from './pack-registry.client'; +import type { Pack, PackListResponse } from './pack-registry.models'; +import { PackRegistryBrowserService } from '../../features/pack-registry/services/pack-registry-browser.service'; + +describe('PackRegistryBrowserService', () => { + let service: PackRegistryBrowserService; + let client: { + list: jasmine.Spy; + getInstalled: jasmine.Spy; + getVersions: jasmine.Spy; + checkCompatibility: jasmine.Spy; + install: jasmine.Spy; + upgrade: jasmine.Spy; + }; + + const listedPacks: Pack[] = [ + { + id: 'pack-alpha', + name: 'Alpha Pack', + version: '1.2.0', + description: 'Alpha pack', + author: 'Stella Ops', + isOfficial: true, + platformCompatibility: '>=1.0.0', + capabilities: ['deploy', 'scan'], + status: 'installed', + installedVersion: '1.1.0', + latestVersion: '1.2.0', + updatedAt: '2026-03-09T08:00:00Z', + signature: 'sig-alpha', + signedBy: 'stellaops', + }, + { + id: 'pack-beta', + name: 'Beta Pack', + version: '2.0.0', + description: 'Beta pack', + author: 'Stella Ops', + isOfficial: true, + platformCompatibility: '>=1.0.0', + capabilities: ['observe'], + status: 'available', + latestVersion: '2.0.0', + updatedAt: '2026-03-09T09:00:00Z', + }, + ]; + + beforeEach(() => { + client = { + list: jasmine.createSpy('list'), + getInstalled: jasmine.createSpy('getInstalled'), + getVersions: jasmine.createSpy('getVersions'), + checkCompatibility: jasmine.createSpy('checkCompatibility'), + install: jasmine.createSpy('install'), + upgrade: jasmine.createSpy('upgrade'), + }; + client.list.and.returnValue(of({ items: listedPacks, total: listedPacks.length })); + + TestBed.configureTestingModule({ + providers: [ + PackRegistryBrowserService, + { provide: PackRegistryClient, useValue: client as unknown as PackRegistryClient }, + ], + }); + + service = TestBed.inject(PackRegistryBrowserService); + }); + + it('builds the dashboard from the canonical pack list without probing /installed', async () => { + const viewModel = await firstValueFrom(service.loadDashboard()); + + expect(client.list).toHaveBeenCalledWith(undefined, 200); + expect(client.getInstalled).not.toHaveBeenCalled(); + expect(viewModel.totalCount).toBe(2); + expect(viewModel.installedCount).toBe(1); + expect(viewModel.upgradeAvailableCount).toBe(1); + expect(viewModel.capabilities).toEqual(['deploy', 'observe', 'scan']); + expect(viewModel.packs.map((pack) => pack.id)).toEqual(['pack-alpha', 'pack-beta']); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/core/console/console-status.component.spec.ts b/src/Web/StellaOps.Web/src/app/core/console/console-status.component.spec.ts new file mode 100644 index 000000000..e3ffa54b5 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/console/console-status.component.spec.ts @@ -0,0 +1,94 @@ +import { Component, Input } from '@angular/core'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { Subscription } from 'rxjs'; + +import type { ConsoleStatusDto } from '../api/console-status.models'; +import { ConsoleStatusComponent } from '../../features/console/console-status.component'; +import { FirstSignalCardComponent } from '../../features/runs/components/first-signal-card/first-signal-card.component'; +import { ConsoleStatusService } from './console-status.service'; +import { ConsoleStatusStore } from './console-status.store'; + +@Component({ + selector: 'app-first-signal-card', + standalone: true, + template: '', +}) +class StubFirstSignalCardComponent { + @Input() runId = ''; + @Input() enableRealTime = false; + @Input() pollIntervalMs = 0; +} + +class FakeConsoleStatusService { + readonly startPolling = jasmine.createSpy('startPolling').and.returnValue(new Subscription()); + readonly fetchStatus = jasmine.createSpy('fetchStatus'); + readonly subscribeToRun = jasmine.createSpy('subscribeToRun').and.callFake(() => new Subscription()); +} + +describe('ConsoleStatusComponent', () => { + let fixture: ComponentFixture; + let component: ConsoleStatusComponent; + let store: ConsoleStatusStore; + let service: FakeConsoleStatusService; + + const statusFixture: ConsoleStatusDto = { + backlog: 2, + queueLagMs: 250, + activeRuns: 1, + pendingRuns: 1, + healthy: true, + lastCompletedRunId: 'run-123', + lastCompletedAt: '2026-03-09T10:00:00Z', + }; + + beforeEach(async () => { + await TestBed.configureTestingModule({ + imports: [ConsoleStatusComponent], + providers: [ + ConsoleStatusStore, + { provide: ConsoleStatusService, useClass: FakeConsoleStatusService }, + ], + }) + .overrideComponent(ConsoleStatusComponent, { + remove: { imports: [FirstSignalCardComponent] }, + add: { imports: [StubFirstSignalCardComponent] }, + }) + .compileComponents(); + + fixture = TestBed.createComponent(ConsoleStatusComponent); + component = fixture.componentInstance; + store = TestBed.inject(ConsoleStatusStore); + service = TestBed.inject(ConsoleStatusService) as unknown as FakeConsoleStatusService; + }); + + it('waits for a concrete run id before opening the run stream', () => { + fixture.detectChanges(); + + expect(service.startPolling).toHaveBeenCalledWith(30000); + expect(service.subscribeToRun).not.toHaveBeenCalled(); + expect(component.activeRunId()).toBeNull(); + expect(fixture.nativeElement.textContent).toContain('Run stream will start after console status reports a completed run'); + }); + + it('subscribes to the last completed run when console status provides one', () => { + fixture.detectChanges(); + + store.setStatus(statusFixture); + fixture.detectChanges(); + + expect(component.activeRunId()).toBe('run-123'); + expect(service.subscribeToRun).toHaveBeenCalledWith('run-123'); + }); + + it('ignores the synthetic last token and keeps streaming the concrete run id', () => { + fixture.detectChanges(); + store.setStatus(statusFixture); + fixture.detectChanges(); + + component.updateRunId('last'); + fixture.detectChanges(); + + expect(component.activeRunId()).toBe('run-123'); + expect(service.subscribeToRun).toHaveBeenCalledTimes(1); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/features/console/console-status.component.html b/src/Web/StellaOps.Web/src/app/features/console/console-status.component.html index 3f3e967a8..a2bd39be7 100644 --- a/src/Web/StellaOps.Web/src/app/features/console/console-status.component.html +++ b/src/Web/StellaOps.Web/src/app/features/console/console-status.component.html @@ -52,30 +52,39 @@

Run Stream

- -
- @for (evt of runEvents(); track evt) { -
-
- {{ evt.kind }} - {{ evt.updatedAt }} + @if (activeRunId(); as runId) { + +
+ @for (evt of runEvents(); track evt) { +
+
+ {{ evt.kind }} + {{ evt.updatedAt }} +
+
+ Run {{ evt.runId }} + {{ evt.message || '...' }} + @if (evt.progressPercent != null) { + {{ evt.progressPercent }}% + } +
-
- Run {{ evt.runId }} - {{ evt.message || '...' }} - @if (evt.progressPercent != null) { - {{ evt.progressPercent }}% - } -
-
- } - @if (runEvents().length === 0) { -

No events yet.

- } -
+ } + @if (runEvents().length === 0) { +

No events yet for {{ runId }}.

+ } +
+ } @else { +

Run stream will start after console status reports a completed run or you enter a concrete run ID.

+ } diff --git a/src/Web/StellaOps.Web/src/app/features/console/console-status.component.ts b/src/Web/StellaOps.Web/src/app/features/console/console-status.component.ts index ad4cf8c01..703032cd9 100644 --- a/src/Web/StellaOps.Web/src/app/features/console/console-status.component.ts +++ b/src/Web/StellaOps.Web/src/app/features/console/console-status.component.ts @@ -1,5 +1,5 @@ import { CommonModule } from '@angular/common'; -import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, inject, signal } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit, computed, effect, inject, signal } from '@angular/core'; import { ConsoleStatusService } from '../../core/console/console-status.service'; import { ConsoleStatusStore } from '../../core/console/console-status.store'; @@ -18,16 +18,31 @@ export class ConsoleStatusComponent implements OnInit, OnDestroy { private pollSub: ReturnType | null = null; private runSub: ReturnType | null = null; + private currentStreamRunId: string | null = null; readonly status = this.store.status; readonly loading = this.store.loading; readonly error = this.store.error; readonly runEvents = this.store.runEvents; - readonly runId = signal('last'); + readonly runIdInput = signal(''); + readonly activeRunId = computed(() => this.normalizeRunId(this.runIdInput()) ?? this.status()?.lastCompletedRunId ?? null); + + constructor() { + effect(() => { + const runId = this.activeRunId(); + if (runId === this.currentStreamRunId) { + return; + } + + this.runSub?.unsubscribe(); + this.store.clearEvents(); + this.currentStreamRunId = runId; + this.runSub = runId ? this.service.subscribeToRun(runId) : null; + }); + } ngOnInit(): void { this.pollSub = this.service.startPolling(30000); - this.startRunStream(); } ngOnDestroy(): void { @@ -39,8 +54,12 @@ export class ConsoleStatusComponent implements OnInit, OnDestroy { this.service.fetchStatus(); } - startRunStream(): void { - this.runSub?.unsubscribe(); - this.runSub = this.service.subscribeToRun(this.runId()); + updateRunId(value: string): void { + this.runIdInput.set(value); + } + + private normalizeRunId(value: string): string | null { + const runId = value.trim(); + return runId.length > 0 && runId.toLowerCase() !== 'last' ? runId : null; } } diff --git a/src/Web/StellaOps.Web/src/app/features/pack-registry/services/pack-registry-browser.service.ts b/src/Web/StellaOps.Web/src/app/features/pack-registry/services/pack-registry-browser.service.ts index 478b91d6f..ffa9f9be5 100644 --- a/src/Web/StellaOps.Web/src/app/features/pack-registry/services/pack-registry-browser.service.ts +++ b/src/Web/StellaOps.Web/src/app/features/pack-registry/services/pack-registry-browser.service.ts @@ -1,5 +1,5 @@ import { Injectable, inject } from '@angular/core'; -import { Observable, catchError, forkJoin, map, of, switchMap } from 'rxjs'; +import { Observable, catchError, map, of, switchMap } from 'rxjs'; import { CompatibilityResult, Pack, PackStatus, PackVersion } from '../../../core/api/pack-registry.models'; import { PackRegistryClient } from '../../../core/api/pack-registry.client'; @@ -17,14 +17,10 @@ export class PackRegistryBrowserService { private readonly packRegistryClient = inject(PackRegistryClient); loadDashboard(): Observable { - return forkJoin({ - listed: this.packRegistryClient.list(undefined, 200), - installed: this.packRegistryClient.getInstalled().pipe(catchError(() => of([] as Pack[]))), - }).pipe( - map(({ listed, installed }) => { - const installedById = new Map(installed.map((pack) => [pack.id, pack] as const)); + return this.packRegistryClient.list(undefined, 200).pipe( + map((listed) => { const rows = listed.items - .map((pack) => this.toRow(pack, installedById.get(pack.id))) + .map((pack) => this.toRow(pack)) .sort((left, right) => this.compareRows(left, right)); const capabilitySet = new Set(); @@ -114,8 +110,8 @@ export class PackRegistryBrowserService { ); } - private toRow(pack: Pack, installedPack?: Pack): PackRegistryRow { - const installedVersion = installedPack?.version ?? pack.installedVersion; + private toRow(pack: Pack): PackRegistryRow { + const installedVersion = pack.installedVersion; const status = this.resolveStatus(pack.status, installedVersion, pack.latestVersion); const primaryAction: PackPrimaryAction = installedVersion ? 'upgrade' : 'install'; diff --git a/src/Web/StellaOps.Web/tests/e2e/prealpha-canonical-full-sweep.spec.ts b/src/Web/StellaOps.Web/tests/e2e/prealpha-canonical-full-sweep.spec.ts index d576d457b..d4adb24b6 100644 --- a/src/Web/StellaOps.Web/tests/e2e/prealpha-canonical-full-sweep.spec.ts +++ b/src/Web/StellaOps.Web/tests/e2e/prealpha-canonical-full-sweep.spec.ts @@ -88,6 +88,9 @@ const canonicalRoutes = [ '/releases/hotfixes/new', '/releases/environments', '/releases/deployments', + '/releases/investigation/timeline', + '/releases/investigation/deploy-diff', + '/releases/investigation/change-trace', '/security', '/security/posture', '/security/triage', @@ -101,6 +104,7 @@ const canonicalRoutes = [ '/evidence', '/evidence/overview', '/evidence/capsules', + '/evidence/threads', '/evidence/verify-replay', '/evidence/proofs', '/evidence/exports', @@ -135,6 +139,7 @@ const canonicalRoutes = [ '/ops/integrations/notifications', '/ops/integrations/sbom-sources', '/ops/integrations/activity', + '/ops/integrations/registry-admin', '/ops/policy', '/ops/policy/overview', '/ops/policy/baselines',