Stabilize release confidence approval decision journey
This commit is contained in:
@@ -0,0 +1,76 @@
|
||||
# Sprint 20260315_002 - Platform Release Confidence Operator Journey Audit
|
||||
|
||||
## Topic & Scope
|
||||
- Use Stella Ops as a release operator trying to gain confidence in a production promotion decision.
|
||||
- Drive the release-control journey end to end: mission control to approvals, promotions, deployments, release health, hotfixes, release evidence, and the adjacent security/evidence pivots a release operator actually uses while deciding.
|
||||
- Treat Playwright as retained evidence only after manual discovery. Every newly discovered release-confidence step or defect becomes retained coverage before the sprint closes.
|
||||
- Group fixes by root cause so the iteration closes whole release-confidence behavior slices, not isolated page patches.
|
||||
- Working directory: `.`.
|
||||
- Expected evidence: operator journey notes, retained Playwright additions, grouped defect analysis, focused regression tests where code changes land, rebuilt-stack retest results, and live journey evidence.
|
||||
|
||||
## Dependencies & Concurrency
|
||||
- Depends on local commit `4a5185121` as the closed baseline from the setup/admin operator journey.
|
||||
- Safe parallelism: avoid environment resets while the live release-confidence journey is being exercised because releases, approvals, and evidence surfaces share the same stack state.
|
||||
|
||||
## Documentation Prerequisites
|
||||
- `AGENTS.md`
|
||||
- `docs/INSTALL_GUIDE.md`
|
||||
- `docs/dev/DEV_ENVIRONMENT_SETUP.md`
|
||||
- `docs/qa/feature-checks/FLOW.md`
|
||||
- `docs/modules/platform/architecture-overview.md`
|
||||
|
||||
## Delivery Tracker
|
||||
|
||||
### PLATFORM-RELEASE-CONFIDENCE-001 - Define and execute the release-confidence operator journey
|
||||
Status: DONE
|
||||
Dependency: none
|
||||
Owners: QA, Product Manager
|
||||
Task description:
|
||||
- Act as a release operator preparing to promote or hotfix with confidence. Walk the visible release-control flow the way a user would: entry from mission control, release health, approvals, promotions, deployment detail, version detail, hotfix flow, and evidence hand-offs needed to justify a decision.
|
||||
|
||||
Completion criteria:
|
||||
- [x] The release-confidence operator journey is explicitly listed before fixes begin.
|
||||
- [x] Playwright is used to execute the journey as an operator would, not only as a route sweep.
|
||||
- [x] Every broken route, page-load, data-load, hand-off, validation rule, or action encountered on the release path is recorded before any fix starts.
|
||||
|
||||
### PLATFORM-RELEASE-CONFIDENCE-002 - Convert newly discovered release steps into retained coverage
|
||||
Status: DONE
|
||||
Dependency: PLATFORM-RELEASE-CONFIDENCE-001
|
||||
Owners: QA, Test Automation
|
||||
Task description:
|
||||
- Add or deepen retained Playwright coverage for every newly discovered release-confidence step so future iterations automatically recheck the same operator behavior.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Every newly discovered operator/release step is mapped to retained Playwright coverage or an explicit backlog gap.
|
||||
- [x] Retained coverage additions are organized by user journey, not only by route.
|
||||
- [x] The next aggregate run would exercise the newly discovered release-confidence path automatically.
|
||||
|
||||
### PLATFORM-RELEASE-CONFIDENCE-003 - Repair grouped release-confidence defects and retest
|
||||
Status: DONE
|
||||
Dependency: PLATFORM-RELEASE-CONFIDENCE-002
|
||||
Owners: 3rd line support, Architect, Developer
|
||||
Task description:
|
||||
- Diagnose the grouped failures exposed by the release-confidence journey, choose the clean product/architecture-conformant fix, implement it, add retained Playwright coverage for the new behavior when needed, and rerun the affected journeys plus the aggregate audit before committing.
|
||||
|
||||
Completion criteria:
|
||||
- [x] Root causes are recorded for the grouped failures.
|
||||
- [x] Fixes land with focused regression coverage and retained Playwright scenario updates where practical.
|
||||
- [x] The live stack is retested through the same release-confidence journeys before the iteration commit.
|
||||
|
||||
## Execution Log
|
||||
| Date (UTC) | Update | Owner |
|
||||
| --- | --- | --- |
|
||||
| 2026-03-15 | Sprint created immediately after local commit `4a5185121` closed the setup/admin operator journey. | QA |
|
||||
| 2026-03-15 | Executed the release-confidence operator path from releases overview through deployments, approval detail, decision capsules, triage, advisories/VEX, reachability, security reports, promotions, and hotfix creation before fixing anything. | QA |
|
||||
| 2026-03-15 | Converted the approval-detail operator handoffs into retained Playwright coverage in `live-release-confidence-journey.mjs`, including the real decision cockpit route and scope-preservation assertions for Reachability, Ops/Data, and gate-fix pivots. | Test Automation |
|
||||
| 2026-03-15 | Root-caused the release-confidence defect set to the approval detail route loading a legacy placeholder component and the real cockpit dropping active operator scope on handoff links. Fixed the canonical route, centralized handoff query-param preservation in `approval-detail-page.component.ts`, added focused Angular regression coverage, rebuilt the web bundle, redeployed, and reran the live release-confidence journey clean with `failedStepCount=0` and `runtimeIssueCount=0`. | QA / 3rd line support / Architect / Developer |
|
||||
|
||||
## Decisions & Risks
|
||||
- Decision: this iteration prioritizes release-confidence behavior over broad route counts.
|
||||
- Risk: several release-control surfaces already have route/action sweeps, but the full operator decision journey may still have hand-off gaps that only appear when used sequentially as a real user.
|
||||
- Decision: operator scope (`tenant`, `regions`, `environments`, `timeWindow`) must survive approval-detail pivots the same way it survives deployment-detail pivots; preserving that scope is part of the release-confidence contract, not optional UI state.
|
||||
- Root cause: `/releases/approvals/:id` still pointed at a legacy placeholder `approval-detail.component` while the actual decision cockpit lived in `approval-detail-page.component`; once the route was corrected, the retained journey exposed that cockpit handoffs generated canonical paths but discarded current operator scope because plain `routerLink` anchors and gate-fix links were not built from a shared scope-preserving query-param contract.
|
||||
|
||||
## Next Checkpoints
|
||||
- Define the exact release-confidence path before fixing anything.
|
||||
- Run the journey manually with Playwright, then convert newly discovered steps into retained coverage.
|
||||
@@ -443,6 +443,76 @@ async function main() {
|
||||
};
|
||||
});
|
||||
|
||||
await recordStep(page, report, '02b-approval-detail-decision-cockpit', async (issues) => {
|
||||
const detailUrl = buildScopedUrl('/releases/approvals/apr-001');
|
||||
|
||||
const verifyApprovalLink = async (tabName, linkName, expectedPath, options = {}) => {
|
||||
await page.goto(detailUrl, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30_000,
|
||||
});
|
||||
await settle(page, 2_000);
|
||||
|
||||
if (tabName !== 'Overview') {
|
||||
const tabButton = page.getByRole('button', { name: tabName, exact: true }).first();
|
||||
if (!(await tabButton.isVisible().catch(() => false))) {
|
||||
issues.push(`approval detail is missing the "${tabName}" tab`);
|
||||
return;
|
||||
}
|
||||
|
||||
await clickStable(page, tabButton);
|
||||
await settle(page, 1_000);
|
||||
}
|
||||
|
||||
if (typeof options.beforeLinkClick === 'function') {
|
||||
await options.beforeLinkClick();
|
||||
}
|
||||
|
||||
const target = page.getByRole('link', { name: linkName, exact: true }).first();
|
||||
if (!(await target.isVisible().catch(() => false))) {
|
||||
issues.push(`approval detail is missing the "${linkName}" handoff on ${tabName}`);
|
||||
return;
|
||||
}
|
||||
|
||||
await clickStable(page, target);
|
||||
await page.waitForURL((url) => url.pathname === expectedPath, { timeout: 20_000 }).catch(() => {});
|
||||
await settle(page, 1_500);
|
||||
|
||||
const finalPath = new URL(page.url()).pathname;
|
||||
if (finalPath !== expectedPath) {
|
||||
issues.push(`approval detail "${linkName}" landed on ${page.url()} instead of ${expectedPath}`);
|
||||
}
|
||||
issues.push(...scopeIssues(page.url(), `approval detail ${tabName} -> ${linkName}`));
|
||||
};
|
||||
|
||||
await page.goto(detailUrl, {
|
||||
waitUntil: 'domcontentloaded',
|
||||
timeout: 30_000,
|
||||
});
|
||||
await settle(page, 2_000);
|
||||
|
||||
await ensureHeading(page, /approval detail/i, issues, 'approval detail');
|
||||
const cockpitText = await bodyText(page);
|
||||
if (/coming soon/i.test(cockpitText) || !/decision case-file|reachability|ops\/data|replay\/verify/i.test(cockpitText)) {
|
||||
issues.push('approval detail is still rendering placeholder content instead of the decision cockpit');
|
||||
}
|
||||
|
||||
await verifyApprovalLink('Reachability', 'Open Reachability Ingest Health', '/ops/operations/data-integrity/reachability-ingest');
|
||||
await verifyApprovalLink('Reachability', 'Open Env Detail', '/setup/topology/environments');
|
||||
await verifyApprovalLink('Ops/Data', 'Open Data Integrity', '/ops/operations/data-integrity');
|
||||
await verifyApprovalLink('Ops/Data', 'Open Integrations', '/setup/integrations');
|
||||
await verifyApprovalLink('Gates', 'Trigger SBOM Scan', '/ops/operations/data-integrity/scan-pipeline', {
|
||||
beforeLinkClick: async () => {
|
||||
await clickStable(page, page.getByRole('button', { name: 'Gate detail trace', exact: true }).first());
|
||||
await settle(page, 750);
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
detailUrl,
|
||||
};
|
||||
});
|
||||
|
||||
await recordStep(page, report, '03-decision-capsules-search-and-detail', async (issues) => {
|
||||
await page.goto(buildScopedUrl('/evidence/capsules'), {
|
||||
waitUntil: 'domcontentloaded',
|
||||
|
||||
@@ -0,0 +1,121 @@
|
||||
import { ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { ActivatedRoute, convertToParamMap, provideRouter } from '@angular/router';
|
||||
import { of } from 'rxjs';
|
||||
|
||||
import { ApprovalDetailPageComponent } from './approval-detail-page.component';
|
||||
import { APPROVALS_ROUTES } from './approvals.routes';
|
||||
import { OPERATIONS_PATHS, dataIntegrityPath } from '../platform/ops/operations-paths';
|
||||
|
||||
describe('ApprovalDetailPageComponent', () => {
|
||||
let component: ApprovalDetailPageComponent;
|
||||
let fixture: ComponentFixture<ApprovalDetailPageComponent>;
|
||||
const scopeQueryParams = {
|
||||
tenant: 'demo-prod',
|
||||
regions: 'eu-west',
|
||||
environments: 'prod',
|
||||
timeWindow: '24h',
|
||||
};
|
||||
|
||||
beforeEach(async () => {
|
||||
await TestBed.configureTestingModule({
|
||||
imports: [ApprovalDetailPageComponent],
|
||||
providers: [
|
||||
provideRouter([]),
|
||||
{
|
||||
provide: ActivatedRoute,
|
||||
useValue: {
|
||||
params: of({ id: 'apr-001' }),
|
||||
queryParamMap: of(convertToParamMap(scopeQueryParams)),
|
||||
snapshot: {
|
||||
paramMap: convertToParamMap({ id: 'apr-001' }),
|
||||
queryParamMap: convertToParamMap(scopeQueryParams),
|
||||
queryParams: scopeQueryParams,
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}).compileComponents();
|
||||
|
||||
fixture = TestBed.createComponent(ApprovalDetailPageComponent);
|
||||
component = fixture.componentInstance;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
it('loads the decision cockpit for approval detail routes', async () => {
|
||||
const detailRoute = APPROVALS_ROUTES.find((route) => route.path === ':id');
|
||||
|
||||
expect(await detailRoute?.loadComponent?.()).toBe(ApprovalDetailPageComponent);
|
||||
});
|
||||
|
||||
it('uses canonical reachability handoff routes', () => {
|
||||
component.setActiveTab('reachability');
|
||||
fixture.detectChanges();
|
||||
|
||||
const hrefs = Array.from(
|
||||
(fixture.nativeElement as HTMLElement).querySelectorAll('.footer-links a'),
|
||||
).map((anchor) => (anchor as HTMLAnchorElement).getAttribute('href'));
|
||||
|
||||
expect(hrefs.some((href) => href?.startsWith(dataIntegrityPath('reachability-ingest')))).toBeTrue();
|
||||
expect(hrefs.some((href) => href?.startsWith('/setup/topology/environments'))).toBeTrue();
|
||||
});
|
||||
|
||||
it('uses canonical ops/data handoff routes', () => {
|
||||
component.setActiveTab('ops-data');
|
||||
fixture.detectChanges();
|
||||
|
||||
const hrefs = Array.from(
|
||||
(fixture.nativeElement as HTMLElement).querySelectorAll('.footer-links a'),
|
||||
).map((anchor) => (anchor as HTMLAnchorElement).getAttribute('href'));
|
||||
|
||||
expect(hrefs.some((href) => href?.startsWith(OPERATIONS_PATHS.dataIntegrity))).toBeTrue();
|
||||
expect(hrefs.some((href) => href?.startsWith('/setup/integrations'))).toBeTrue();
|
||||
expect(hrefs.some((href) => href?.startsWith(OPERATIONS_PATHS.schedulerRuns))).toBeTrue();
|
||||
expect(hrefs.some((href) => href?.startsWith(OPERATIONS_PATHS.deadLetter))).toBeTrue();
|
||||
});
|
||||
|
||||
it('uses canonical gate fix links for reachability blockers', () => {
|
||||
const reachabilityGate = component.gateTraceRows.find((row) => row.id === 'reachability');
|
||||
|
||||
expect(reachabilityGate?.fixLinks.map((link) => link.route)).toEqual([
|
||||
dataIntegrityPath('scan-pipeline'),
|
||||
'/security/findings',
|
||||
'/ops/policy/vex/exceptions',
|
||||
OPERATIONS_PATHS.dataIntegrity,
|
||||
]);
|
||||
});
|
||||
|
||||
it('preserves operator scope in decision context query params', () => {
|
||||
expect(component.decisioningContextParams({ create: '1' })).toEqual(
|
||||
jasmine.objectContaining({
|
||||
tenant: 'demo-prod',
|
||||
regions: 'eu-west',
|
||||
environments: 'prod',
|
||||
timeWindow: '24h',
|
||||
approvalId: 'apr-001',
|
||||
create: '1',
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
it('preserves operator scope in approval handoff links', () => {
|
||||
component.setActiveTab('ops-data');
|
||||
fixture.detectChanges();
|
||||
|
||||
const hrefs = Array.from(
|
||||
(fixture.nativeElement as HTMLElement).querySelectorAll('.footer-links a'),
|
||||
).map((anchor) => (anchor as HTMLAnchorElement).getAttribute('href') ?? '');
|
||||
|
||||
const dataIntegrityHref = hrefs.find((href) => href.startsWith(OPERATIONS_PATHS.dataIntegrity));
|
||||
const integrationsHref = hrefs.find((href) => href.startsWith('/setup/integrations'));
|
||||
|
||||
expect(dataIntegrityHref).toContain('tenant=demo-prod');
|
||||
expect(dataIntegrityHref).toContain('regions=eu-west');
|
||||
expect(dataIntegrityHref).toContain('environments=prod');
|
||||
expect(dataIntegrityHref).toContain('timeWindow=24h');
|
||||
|
||||
expect(integrationsHref).toContain('tenant=demo-prod');
|
||||
expect(integrationsHref).toContain('regions=eu-west');
|
||||
expect(integrationsHref).toContain('environments=prod');
|
||||
expect(integrationsHref).toContain('timeWindow=24h');
|
||||
});
|
||||
});
|
||||
@@ -1,10 +1,10 @@
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
import { ActivatedRoute, Router, RouterLink } from '@angular/router';
|
||||
import { ActivatedRoute, ParamMap, Router, RouterLink } from '@angular/router';
|
||||
|
||||
import { buildContextReturnTo } from '../../shared/ui/context-route-state/context-route-state';
|
||||
import { OPERATIONS_PATHS } from '../platform/ops/operations-paths';
|
||||
import { OPERATIONS_PATHS, dataIntegrityPath } from '../platform/ops/operations-paths';
|
||||
|
||||
type GateResult = 'PASS' | 'WARN' | 'BLOCK';
|
||||
type HealthStatus = 'OK' | 'WARN' | 'FAIL';
|
||||
@@ -97,7 +97,7 @@ interface HistoryEvent {
|
||||
template: `
|
||||
<div class="approval-detail-v2">
|
||||
<header class="decision-header">
|
||||
<a routerLink="/releases/approvals" class="back-link">Back to Approvals</a>
|
||||
<a routerLink="/releases/approvals" [queryParams]="scopeQueryParams()" class="back-link">Back to Approvals</a>
|
||||
|
||||
<div class="decision-header__title-row">
|
||||
<h1>Approval Detail</h1>
|
||||
@@ -232,7 +232,7 @@ interface HistoryEvent {
|
||||
@if (row.result === 'BLOCK') {
|
||||
<div class="fix-links">
|
||||
@for (link of row.fixLinks; track link.label) {
|
||||
<a [routerLink]="link.route" [queryParams]="link.queryParams || null">{{ link.label }}</a>
|
||||
<a [routerLink]="link.route" [queryParams]="handoffQueryParams(link.queryParams)">{{ link.label }}</a>
|
||||
}
|
||||
</div>
|
||||
}
|
||||
@@ -298,7 +298,7 @@ interface HistoryEvent {
|
||||
</table>
|
||||
|
||||
<div class="footer-links">
|
||||
<a routerLink="/security/findings">Open Findings (filtered)</a>
|
||||
<a routerLink="/security/findings" [queryParams]="scopeQueryParams()">Open Findings (filtered)</a>
|
||||
<a routerLink="/ops/policy/vex" [queryParams]="decisioningContextParams()">Open VEX Hub</a>
|
||||
<a routerLink="/ops/policy/vex/exceptions" [queryParams]="decisioningContextParams()">Open Exceptions</a>
|
||||
</div>
|
||||
@@ -339,8 +339,8 @@ interface HistoryEvent {
|
||||
</table>
|
||||
|
||||
<div class="footer-links">
|
||||
<a routerLink="/platform-ops/data-integrity/reachability-ingest">Open Reachability Ingest Health</a>
|
||||
<a routerLink="/topology/environments">Open Env Detail</a>
|
||||
<a [routerLink]="dataIntegrityPath('reachability-ingest')" [queryParams]="scopeQueryParams()">Open Reachability Ingest Health</a>
|
||||
<a [routerLink]="topologyEnvironmentsRoute" [queryParams]="scopeQueryParams()">Open Env Detail</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
@@ -398,10 +398,10 @@ interface HistoryEvent {
|
||||
</div>
|
||||
|
||||
<div class="footer-links">
|
||||
<a routerLink="/platform-ops/data-integrity">Open Data Integrity</a>
|
||||
<a routerLink="/integrations">Open Integrations</a>
|
||||
<a [routerLink]="OPERATIONS_PATHS.schedulerRuns">Open Scheduler Runs</a>
|
||||
<a [routerLink]="OPERATIONS_PATHS.deadLetter">Open DLQ</a>
|
||||
<a [routerLink]="OPERATIONS_PATHS.dataIntegrity" [queryParams]="scopeQueryParams()">Open Data Integrity</a>
|
||||
<a [routerLink]="integrationsRoute" [queryParams]="scopeQueryParams()">Open Integrations</a>
|
||||
<a [routerLink]="OPERATIONS_PATHS.schedulerRuns" [queryParams]="scopeQueryParams()">Open Scheduler Runs</a>
|
||||
<a [routerLink]="OPERATIONS_PATHS.deadLetter" [queryParams]="scopeQueryParams()">Open DLQ</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
@@ -420,8 +420,8 @@ interface HistoryEvent {
|
||||
<p>Signature status: DSSE signed, transparency log anchored, replay metadata present.</p>
|
||||
<div class="footer-links">
|
||||
<button type="button" class="link-btn" (click)="exportPacket()">Export Packet</button>
|
||||
<a routerLink="/evidence/exports">Open Export Center</a>
|
||||
<a routerLink="/evidence/capsules">Open Proof Chain</a>
|
||||
<a routerLink="/evidence/exports" [queryParams]="scopeQueryParams()">Open Export Center</a>
|
||||
<a routerLink="/evidence/capsules" [queryParams]="scopeQueryParams()">Open Proof Chain</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
@@ -447,7 +447,7 @@ interface HistoryEvent {
|
||||
</ul>
|
||||
|
||||
<div class="footer-links">
|
||||
<a routerLink="/evidence/verify-replay">Open canonical Replay/Verify</a>
|
||||
<a routerLink="/evidence/verify-replay" [queryParams]="scopeQueryParams()">Open canonical Replay/Verify</a>
|
||||
</div>
|
||||
</section>
|
||||
}
|
||||
@@ -788,12 +788,16 @@ interface HistoryEvent {
|
||||
})
|
||||
export class ApprovalDetailPageComponent implements OnInit {
|
||||
protected readonly OPERATIONS_PATHS = OPERATIONS_PATHS;
|
||||
protected readonly dataIntegrityPath = dataIntegrityPath;
|
||||
private readonly route = inject(ActivatedRoute);
|
||||
private readonly router = inject(Router);
|
||||
protected readonly integrationsRoute = '/setup/integrations';
|
||||
protected readonly topologyEnvironmentsRoute = '/setup/topology/environments';
|
||||
|
||||
readonly minDecisionReasonLength = 10;
|
||||
readonly activeTab = signal<ApprovalTabId>('overview');
|
||||
readonly expandedGateId = signal<string | null>(null);
|
||||
readonly scopeQueryParams = signal<Record<string, string>>({});
|
||||
|
||||
readonly approval = signal<ApprovalDetailState>({
|
||||
id: 'apr-001',
|
||||
@@ -846,14 +850,14 @@ export class ApprovalDetailPageComponent implements OnInit {
|
||||
timestamp: '2026-02-19T18:11:00Z',
|
||||
evidenceAge: '2h 11m',
|
||||
fixLinks: [
|
||||
{ label: 'Trigger SBOM Scan', route: '/platform-ops/data-integrity/scan-pipeline' },
|
||||
{ label: 'Trigger SBOM Scan', route: dataIntegrityPath('scan-pipeline') },
|
||||
{ label: 'Open Finding', route: '/security/findings' },
|
||||
{
|
||||
label: 'Request Exception',
|
||||
route: '/ops/policy/vex/exceptions',
|
||||
queryParams: this.decisioningContextParams({ create: '1' }),
|
||||
},
|
||||
{ label: 'Open Data Integrity', route: '/platform-ops/data-integrity' },
|
||||
{ label: 'Open Data Integrity', route: OPERATIONS_PATHS.dataIntegrity },
|
||||
],
|
||||
},
|
||||
{
|
||||
@@ -983,6 +987,10 @@ export class ApprovalDetailPageComponent implements OnInit {
|
||||
this.approval.update((state) => ({ ...state, id }));
|
||||
}
|
||||
});
|
||||
|
||||
this.route.queryParamMap.subscribe((queryParamMap) => {
|
||||
this.scopeQueryParams.set(this.mapQueryParams(queryParamMap));
|
||||
});
|
||||
}
|
||||
|
||||
setActiveTab(tab: ApprovalTabId): void {
|
||||
@@ -1050,17 +1058,34 @@ export class ApprovalDetailPageComponent implements OnInit {
|
||||
}));
|
||||
}
|
||||
|
||||
handoffQueryParams(extra: Record<string, string> = {}): Record<string, string> {
|
||||
return {
|
||||
...this.scopeQueryParams(),
|
||||
...extra,
|
||||
};
|
||||
}
|
||||
|
||||
decisioningContextParams(extra: Record<string, string> = {}): Record<string, string> {
|
||||
const approval = this.approval();
|
||||
const returnTo = buildContextReturnTo(this.router, ['/releases/approvals', approval.id]);
|
||||
|
||||
return {
|
||||
return this.handoffQueryParams({
|
||||
approvalId: approval.id,
|
||||
releaseId: approval.bundleVersion,
|
||||
environment: approval.targetEnvironment,
|
||||
artifact: approval.bundleDigest,
|
||||
returnTo,
|
||||
...extra,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
private mapQueryParams(queryParamMap: ParamMap): Record<string, string> {
|
||||
return queryParamMap.keys.reduce<Record<string, string>>((params, key) => {
|
||||
const value = queryParamMap.get(key);
|
||||
if (value !== null && value.length > 0) {
|
||||
params[key] = value;
|
||||
}
|
||||
return params;
|
||||
}, {});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -27,16 +27,14 @@ export const APPROVALS_ROUTES: Routes = [
|
||||
// A6-02 through A6-05 — Decision cockpit
|
||||
{
|
||||
path: ':id',
|
||||
title: 'Approval Decision',
|
||||
title: 'Approval Detail',
|
||||
data: {
|
||||
breadcrumb: 'Approval Decision',
|
||||
breadcrumb: 'Approval Detail',
|
||||
// Available tabs in the decision cockpit:
|
||||
// overview | gates | security | reachability | ops-data | evidence | replay | history
|
||||
decisionTabs: ['overview', 'gates', 'security', 'reachability', 'ops-data', 'evidence', 'replay', 'history'],
|
||||
},
|
||||
loadComponent: () =>
|
||||
import('../release-orchestrator/approvals/approval-detail/approval-detail.component').then(
|
||||
(m) => m.ApprovalDetailComponent
|
||||
),
|
||||
import('./approval-detail-page.component').then((m) => m.ApprovalDetailPageComponent),
|
||||
},
|
||||
];
|
||||
|
||||
@@ -13,6 +13,7 @@
|
||||
"src/app/core/auth/tenant-activation.service.spec.ts",
|
||||
"src/app/core/console/console-status.service.spec.ts",
|
||||
"src/app/features/change-trace/change-trace-viewer.component.spec.ts",
|
||||
"src/app/features/approvals/approval-detail-page.component.spec.ts",
|
||||
"src/app/features/admin-notifications/components/notification-rule-list.component.spec.ts",
|
||||
"src/app/features/admin-notifications/components/notification-dashboard.component.spec.ts",
|
||||
"src/app/features/admin-notifications/components/channel-management.component.spec.ts",
|
||||
|
||||
Reference in New Issue
Block a user