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 recordStep(page, report, '03-decision-capsules-search-and-detail', async (issues) => {
|
||||||
await page.goto(buildScopedUrl('/evidence/capsules'), {
|
await page.goto(buildScopedUrl('/evidence/capsules'), {
|
||||||
waitUntil: 'domcontentloaded',
|
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 { CommonModule } from '@angular/common';
|
||||||
import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, computed, inject, OnInit, signal } from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
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 { 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 GateResult = 'PASS' | 'WARN' | 'BLOCK';
|
||||||
type HealthStatus = 'OK' | 'WARN' | 'FAIL';
|
type HealthStatus = 'OK' | 'WARN' | 'FAIL';
|
||||||
@@ -97,7 +97,7 @@ interface HistoryEvent {
|
|||||||
template: `
|
template: `
|
||||||
<div class="approval-detail-v2">
|
<div class="approval-detail-v2">
|
||||||
<header class="decision-header">
|
<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">
|
<div class="decision-header__title-row">
|
||||||
<h1>Approval Detail</h1>
|
<h1>Approval Detail</h1>
|
||||||
@@ -232,7 +232,7 @@ interface HistoryEvent {
|
|||||||
@if (row.result === 'BLOCK') {
|
@if (row.result === 'BLOCK') {
|
||||||
<div class="fix-links">
|
<div class="fix-links">
|
||||||
@for (link of row.fixLinks; track link.label) {
|
@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>
|
</div>
|
||||||
}
|
}
|
||||||
@@ -298,7 +298,7 @@ interface HistoryEvent {
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="footer-links">
|
<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" [queryParams]="decisioningContextParams()">Open VEX Hub</a>
|
||||||
<a routerLink="/ops/policy/vex/exceptions" [queryParams]="decisioningContextParams()">Open Exceptions</a>
|
<a routerLink="/ops/policy/vex/exceptions" [queryParams]="decisioningContextParams()">Open Exceptions</a>
|
||||||
</div>
|
</div>
|
||||||
@@ -339,8 +339,8 @@ interface HistoryEvent {
|
|||||||
</table>
|
</table>
|
||||||
|
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a routerLink="/platform-ops/data-integrity/reachability-ingest">Open Reachability Ingest Health</a>
|
<a [routerLink]="dataIntegrityPath('reachability-ingest')" [queryParams]="scopeQueryParams()">Open Reachability Ingest Health</a>
|
||||||
<a routerLink="/topology/environments">Open Env Detail</a>
|
<a [routerLink]="topologyEnvironmentsRoute" [queryParams]="scopeQueryParams()">Open Env Detail</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
@@ -398,10 +398,10 @@ interface HistoryEvent {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<a routerLink="/platform-ops/data-integrity">Open Data Integrity</a>
|
<a [routerLink]="OPERATIONS_PATHS.dataIntegrity" [queryParams]="scopeQueryParams()">Open Data Integrity</a>
|
||||||
<a routerLink="/integrations">Open Integrations</a>
|
<a [routerLink]="integrationsRoute" [queryParams]="scopeQueryParams()">Open Integrations</a>
|
||||||
<a [routerLink]="OPERATIONS_PATHS.schedulerRuns">Open Scheduler Runs</a>
|
<a [routerLink]="OPERATIONS_PATHS.schedulerRuns" [queryParams]="scopeQueryParams()">Open Scheduler Runs</a>
|
||||||
<a [routerLink]="OPERATIONS_PATHS.deadLetter">Open DLQ</a>
|
<a [routerLink]="OPERATIONS_PATHS.deadLetter" [queryParams]="scopeQueryParams()">Open DLQ</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
@@ -420,8 +420,8 @@ interface HistoryEvent {
|
|||||||
<p>Signature status: DSSE signed, transparency log anchored, replay metadata present.</p>
|
<p>Signature status: DSSE signed, transparency log anchored, replay metadata present.</p>
|
||||||
<div class="footer-links">
|
<div class="footer-links">
|
||||||
<button type="button" class="link-btn" (click)="exportPacket()">Export Packet</button>
|
<button type="button" class="link-btn" (click)="exportPacket()">Export Packet</button>
|
||||||
<a routerLink="/evidence/exports">Open Export Center</a>
|
<a routerLink="/evidence/exports" [queryParams]="scopeQueryParams()">Open Export Center</a>
|
||||||
<a routerLink="/evidence/capsules">Open Proof Chain</a>
|
<a routerLink="/evidence/capsules" [queryParams]="scopeQueryParams()">Open Proof Chain</a>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
@@ -447,7 +447,7 @@ interface HistoryEvent {
|
|||||||
</ul>
|
</ul>
|
||||||
|
|
||||||
<div class="footer-links">
|
<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>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
}
|
}
|
||||||
@@ -788,12 +788,16 @@ interface HistoryEvent {
|
|||||||
})
|
})
|
||||||
export class ApprovalDetailPageComponent implements OnInit {
|
export class ApprovalDetailPageComponent implements OnInit {
|
||||||
protected readonly OPERATIONS_PATHS = OPERATIONS_PATHS;
|
protected readonly OPERATIONS_PATHS = OPERATIONS_PATHS;
|
||||||
|
protected readonly dataIntegrityPath = dataIntegrityPath;
|
||||||
private readonly route = inject(ActivatedRoute);
|
private readonly route = inject(ActivatedRoute);
|
||||||
private readonly router = inject(Router);
|
private readonly router = inject(Router);
|
||||||
|
protected readonly integrationsRoute = '/setup/integrations';
|
||||||
|
protected readonly topologyEnvironmentsRoute = '/setup/topology/environments';
|
||||||
|
|
||||||
readonly minDecisionReasonLength = 10;
|
readonly minDecisionReasonLength = 10;
|
||||||
readonly activeTab = signal<ApprovalTabId>('overview');
|
readonly activeTab = signal<ApprovalTabId>('overview');
|
||||||
readonly expandedGateId = signal<string | null>(null);
|
readonly expandedGateId = signal<string | null>(null);
|
||||||
|
readonly scopeQueryParams = signal<Record<string, string>>({});
|
||||||
|
|
||||||
readonly approval = signal<ApprovalDetailState>({
|
readonly approval = signal<ApprovalDetailState>({
|
||||||
id: 'apr-001',
|
id: 'apr-001',
|
||||||
@@ -846,14 +850,14 @@ export class ApprovalDetailPageComponent implements OnInit {
|
|||||||
timestamp: '2026-02-19T18:11:00Z',
|
timestamp: '2026-02-19T18:11:00Z',
|
||||||
evidenceAge: '2h 11m',
|
evidenceAge: '2h 11m',
|
||||||
fixLinks: [
|
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: 'Open Finding', route: '/security/findings' },
|
||||||
{
|
{
|
||||||
label: 'Request Exception',
|
label: 'Request Exception',
|
||||||
route: '/ops/policy/vex/exceptions',
|
route: '/ops/policy/vex/exceptions',
|
||||||
queryParams: this.decisioningContextParams({ create: '1' }),
|
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.approval.update((state) => ({ ...state, id }));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.route.queryParamMap.subscribe((queryParamMap) => {
|
||||||
|
this.scopeQueryParams.set(this.mapQueryParams(queryParamMap));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
setActiveTab(tab: ApprovalTabId): void {
|
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> {
|
decisioningContextParams(extra: Record<string, string> = {}): Record<string, string> {
|
||||||
const approval = this.approval();
|
const approval = this.approval();
|
||||||
const returnTo = buildContextReturnTo(this.router, ['/releases/approvals', approval.id]);
|
const returnTo = buildContextReturnTo(this.router, ['/releases/approvals', approval.id]);
|
||||||
|
|
||||||
return {
|
return this.handoffQueryParams({
|
||||||
approvalId: approval.id,
|
approvalId: approval.id,
|
||||||
releaseId: approval.bundleVersion,
|
releaseId: approval.bundleVersion,
|
||||||
environment: approval.targetEnvironment,
|
environment: approval.targetEnvironment,
|
||||||
artifact: approval.bundleDigest,
|
artifact: approval.bundleDigest,
|
||||||
returnTo,
|
returnTo,
|
||||||
...extra,
|
...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
|
// A6-02 through A6-05 — Decision cockpit
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
title: 'Approval Decision',
|
title: 'Approval Detail',
|
||||||
data: {
|
data: {
|
||||||
breadcrumb: 'Approval Decision',
|
breadcrumb: 'Approval Detail',
|
||||||
// Available tabs in the decision cockpit:
|
// Available tabs in the decision cockpit:
|
||||||
// overview | gates | security | reachability | ops-data | evidence | replay | history
|
// overview | gates | security | reachability | ops-data | evidence | replay | history
|
||||||
decisionTabs: ['overview', 'gates', 'security', 'reachability', 'ops-data', 'evidence', 'replay', 'history'],
|
decisionTabs: ['overview', 'gates', 'security', 'reachability', 'ops-data', 'evidence', 'replay', 'history'],
|
||||||
},
|
},
|
||||||
loadComponent: () =>
|
loadComponent: () =>
|
||||||
import('../release-orchestrator/approvals/approval-detail/approval-detail.component').then(
|
import('./approval-detail-page.component').then((m) => m.ApprovalDetailPageComponent),
|
||||||
(m) => m.ApprovalDetailComponent
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -13,6 +13,7 @@
|
|||||||
"src/app/core/auth/tenant-activation.service.spec.ts",
|
"src/app/core/auth/tenant-activation.service.spec.ts",
|
||||||
"src/app/core/console/console-status.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/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-rule-list.component.spec.ts",
|
||||||
"src/app/features/admin-notifications/components/notification-dashboard.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",
|
"src/app/features/admin-notifications/components/channel-management.component.spec.ts",
|
||||||
|
|||||||
Reference in New Issue
Block a user