Align mission control with shared context scope
This commit is contained in:
@@ -0,0 +1,59 @@
|
|||||||
|
# Sprint 20260310-023 - FE Mission Control Shared Context Scope Alignment
|
||||||
|
|
||||||
|
## Topic & Scope
|
||||||
|
- Align Mission Control board and alert drilldowns with the shared Platform context so scoped sessions only surface matching environments and preserve tenant/region/environment state in downstream routes.
|
||||||
|
- Keep the work inside the Mission Control feature and its focused regression specs, with only sprint coordination outside the Web working directory.
|
||||||
|
- Working directory: `src/Web/StellaOps.Web`.
|
||||||
|
- Allowed coordination edits: `docs/implplan/SPRINT_20260310_023_FE_mission_control_shared_context_scope_alignment.md`.
|
||||||
|
- Expected evidence: focused Angular tests, a live authenticated Mission Control Playwright sweep, and rebuilt Web assets already synced into the running stack.
|
||||||
|
|
||||||
|
## Dependencies & Concurrency
|
||||||
|
- Depends on the authenticated local stack being reachable through `https://stella-ops.local`.
|
||||||
|
- Depends on the frontdoor evidence routing fix in `SPRINT_20260310_022_Router_platform_v2_evidence_frontdoor_mapping.md` so downstream posture pages do not emit false Mission Control runtime failures.
|
||||||
|
- Safe parallelism: do not mix unrelated layout/search cleanup into this commit.
|
||||||
|
|
||||||
|
## Documentation Prerequisites
|
||||||
|
- `AGENTS.md`
|
||||||
|
- `docs/qa/feature-checks/FLOW.md`
|
||||||
|
- `docs/implplan/SPRINT_20260310_003_FE_mission_control_live_action_sweep.md`
|
||||||
|
|
||||||
|
## Delivery Tracker
|
||||||
|
|
||||||
|
### FE-MISSION-SCOPE-001 - Align board environment selection with shared context
|
||||||
|
Status: DONE
|
||||||
|
Dependency: none
|
||||||
|
Owners: QA, Developer, Product Manager
|
||||||
|
Task description:
|
||||||
|
- Remove the Mission Control board's drift from the shared Platform context so scoped sessions only show the matching environment cards and downstream topology/findings links inherit the correct region and environment.
|
||||||
|
- Keep the implementation centered on the shared context store instead of duplicating local filter state inside the board component.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Mission Control board environment cards reflect the active `PlatformContextStore` region scope.
|
||||||
|
- [x] Scoped downstream links from the board preserve the expected region and environment query state.
|
||||||
|
- [x] Focused regression specs cover the shared-context behavior.
|
||||||
|
|
||||||
|
### FE-MISSION-SCOPE-002 - Preserve alert drilldown scope when leaving Mission Control
|
||||||
|
Status: DONE
|
||||||
|
Dependency: FE-MISSION-SCOPE-001
|
||||||
|
Owners: QA, Developer
|
||||||
|
Task description:
|
||||||
|
- Ensure Mission Control alert drilldowns merge the active query scope so Watchlist and adjacent surfaces do not drop the current tenant/region/environment window when the user pivots from alerts.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Mission Control alert links use merged query params where needed.
|
||||||
|
- [x] Live Mission Control action sweep completes with zero failed actions and zero runtime issues.
|
||||||
|
|
||||||
|
## Execution Log
|
||||||
|
| Date (UTC) | Update | Owner |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-03-10 | Sprint created after live Mission Control QA showed stage drilldowns resolving against the wrong region because the board was not honoring the active shared context scope. | Developer |
|
||||||
|
| 2026-03-10 | Updated the board and alert drilldowns to rely on shared query scope, refreshed the focused specs, rebuilt/synced the Web assets, and reran `scripts/live-mission-control-action-sweep.mjs` cleanly with `failedActionCount=0` and `runtimeIssueCount=0`. | QA |
|
||||||
|
|
||||||
|
## Decisions & Risks
|
||||||
|
- Decision: Mission Control should consume the global context store instead of keeping a separate board-local region/window state. That avoids dual sources of truth and keeps deep links aligned with the rest of the shell.
|
||||||
|
- Risk: other mission surfaces could still rely on stale local scope defaults.
|
||||||
|
- Mitigation: keep expanding the live action sweeps for Mission Control-adjacent pages and add focused spec coverage whenever a new scope regression is found.
|
||||||
|
|
||||||
|
## Next Checkpoints
|
||||||
|
- Commit the Mission Control scope repair as its own FE iteration.
|
||||||
|
- Continue into the next page-family action sweep on the rebuilt stack.
|
||||||
@@ -6,33 +6,39 @@ import { DashboardV3Component } from '../../features/dashboard-v3/dashboard-v3.c
|
|||||||
|
|
||||||
describe('DashboardV3Component', () => {
|
describe('DashboardV3Component', () => {
|
||||||
it('builds canonical topology posture targets for environment cards', () => {
|
it('builds canonical topology posture targets for environment cards', () => {
|
||||||
|
const contextStore = {
|
||||||
|
initialize: () => undefined,
|
||||||
|
selectedRegions: () => [],
|
||||||
|
timeWindow: () => '24h',
|
||||||
|
setRegions: () => undefined,
|
||||||
|
setTimeWindow: () => undefined,
|
||||||
|
regions: () => [
|
||||||
|
{ regionId: 'eu-west', displayName: 'EU West' },
|
||||||
|
{ regionId: 'us-east', displayName: 'US East' },
|
||||||
|
],
|
||||||
|
environments: () => [
|
||||||
|
{
|
||||||
|
environmentId: 'dev',
|
||||||
|
regionId: 'eu-west',
|
||||||
|
environmentType: 'development',
|
||||||
|
displayName: 'Development EU West',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
environmentId: 'prod-us-east',
|
||||||
|
regionId: 'us-east',
|
||||||
|
environmentType: 'production',
|
||||||
|
displayName: 'Production US East',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [DashboardV3Component],
|
imports: [DashboardV3Component],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{
|
{
|
||||||
provide: PlatformContextStore,
|
provide: PlatformContextStore,
|
||||||
useValue: {
|
useValue: contextStore,
|
||||||
initialize: () => undefined,
|
|
||||||
regions: () => [
|
|
||||||
{ regionId: 'eu-west', displayName: 'EU West' },
|
|
||||||
{ regionId: 'us-east', displayName: 'US East' },
|
|
||||||
],
|
|
||||||
environments: () => [
|
|
||||||
{
|
|
||||||
environmentId: 'dev',
|
|
||||||
regionId: 'eu-west',
|
|
||||||
environmentType: 'development',
|
|
||||||
displayName: 'Development EU West',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
environmentId: 'prod-us-east',
|
|
||||||
regionId: 'us-east',
|
|
||||||
environmentType: 'production',
|
|
||||||
displayName: 'Production US East',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -55,33 +61,39 @@ describe('DashboardV3Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('builds canonical findings scope for downstream pages instead of synthetic dashboard ids', () => {
|
it('builds canonical findings scope for downstream pages instead of synthetic dashboard ids', () => {
|
||||||
|
const contextStore = {
|
||||||
|
initialize: () => undefined,
|
||||||
|
selectedRegions: () => [],
|
||||||
|
timeWindow: () => '24h',
|
||||||
|
setRegions: () => undefined,
|
||||||
|
setTimeWindow: () => undefined,
|
||||||
|
regions: () => [
|
||||||
|
{ regionId: 'eu-west', displayName: 'EU West' },
|
||||||
|
{ regionId: 'us-east', displayName: 'US East' },
|
||||||
|
],
|
||||||
|
environments: () => [
|
||||||
|
{
|
||||||
|
environmentId: 'dev',
|
||||||
|
regionId: 'eu-west',
|
||||||
|
environmentType: 'development',
|
||||||
|
displayName: 'Development EU West',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
environmentId: 'prod',
|
||||||
|
regionId: 'us-east',
|
||||||
|
environmentType: 'production',
|
||||||
|
displayName: 'Production US East',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [DashboardV3Component],
|
imports: [DashboardV3Component],
|
||||||
providers: [
|
providers: [
|
||||||
provideRouter([]),
|
provideRouter([]),
|
||||||
{
|
{
|
||||||
provide: PlatformContextStore,
|
provide: PlatformContextStore,
|
||||||
useValue: {
|
useValue: contextStore,
|
||||||
initialize: () => undefined,
|
|
||||||
regions: () => [
|
|
||||||
{ regionId: 'eu-west', displayName: 'EU West' },
|
|
||||||
{ regionId: 'us-east', displayName: 'US East' },
|
|
||||||
],
|
|
||||||
environments: () => [
|
|
||||||
{
|
|
||||||
environmentId: 'dev',
|
|
||||||
regionId: 'eu-west',
|
|
||||||
environmentType: 'development',
|
|
||||||
displayName: 'Development EU West',
|
|
||||||
},
|
|
||||||
{
|
|
||||||
environmentId: 'prod',
|
|
||||||
regionId: 'us-east',
|
|
||||||
environmentType: 'production',
|
|
||||||
displayName: 'Production US East',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -101,4 +113,50 @@ describe('DashboardV3Component', () => {
|
|||||||
jasmine.objectContaining({ env: 'prod-us-east' }),
|
jasmine.objectContaining({ env: 'prod-us-east' }),
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('filters mission board environments from the active context scope', () => {
|
||||||
|
const contextStore = {
|
||||||
|
initialize: () => undefined,
|
||||||
|
selectedRegions: () => ['us-east'],
|
||||||
|
timeWindow: () => '7d',
|
||||||
|
regions: () => [
|
||||||
|
{ regionId: 'eu-west', displayName: 'EU West' },
|
||||||
|
{ regionId: 'us-east', displayName: 'US East' },
|
||||||
|
],
|
||||||
|
environments: () => [
|
||||||
|
{
|
||||||
|
environmentId: 'stage',
|
||||||
|
regionId: 'eu-west',
|
||||||
|
environmentType: 'staging',
|
||||||
|
displayName: 'Staging EU West',
|
||||||
|
},
|
||||||
|
{
|
||||||
|
environmentId: 'stage',
|
||||||
|
regionId: 'us-east',
|
||||||
|
environmentType: 'staging',
|
||||||
|
displayName: 'Staging US East',
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [DashboardV3Component],
|
||||||
|
providers: [
|
||||||
|
provideRouter([]),
|
||||||
|
{ provide: PlatformContextStore, useValue: contextStore },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
const fixture = TestBed.createComponent(DashboardV3Component);
|
||||||
|
const component = fixture.componentInstance;
|
||||||
|
|
||||||
|
expect(component.filteredEnvironments().map((environment) => environment.regionId)).toEqual(['us-east']);
|
||||||
|
expect(component.filteredEnvironments().map((environment) => environment.environmentId)).toEqual(['stage']);
|
||||||
|
expect(component.environmentScopeQuery(component.filteredEnvironments()[0])).toEqual({
|
||||||
|
region: 'us-east',
|
||||||
|
regions: 'us-east',
|
||||||
|
environment: 'stage',
|
||||||
|
environments: 'stage',
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,6 +23,10 @@ describe('Mission scope-preserving links', () => {
|
|||||||
provide: PlatformContextStore,
|
provide: PlatformContextStore,
|
||||||
useValue: {
|
useValue: {
|
||||||
initialize: () => undefined,
|
initialize: () => undefined,
|
||||||
|
selectedRegions: () => ['us-east'],
|
||||||
|
timeWindow: () => '7d',
|
||||||
|
setRegions: () => undefined,
|
||||||
|
setTimeWindow: () => undefined,
|
||||||
regions: () => [
|
regions: () => [
|
||||||
{ regionId: 'eu-west', displayName: 'EU West' },
|
{ regionId: 'eu-west', displayName: 'EU West' },
|
||||||
{ regionId: 'us-east', displayName: 'US East' },
|
{ regionId: 'us-east', displayName: 'US East' },
|
||||||
@@ -50,7 +54,7 @@ describe('Mission scope-preserving links', () => {
|
|||||||
it('marks every mission alerts link to merge the active query scope', () => {
|
it('marks every mission alerts link to merge the active query scope', () => {
|
||||||
const links = routerLinksFor(MissionAlertsPageComponent);
|
const links = routerLinksFor(MissionAlertsPageComponent);
|
||||||
|
|
||||||
expect(links.length).toBe(3);
|
expect(links.length).toBeGreaterThanOrEqual(3);
|
||||||
expect(links.every((link) => link.queryParamsHandling === 'merge')).toBeTrue();
|
expect(links.every((link) => link.queryParamsHandling === 'merge')).toBeTrue();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -63,37 +63,6 @@ interface MissionSummary {
|
|||||||
<p class="board-subtitle">Mission board for release health across regions and environments</p>
|
<p class="board-subtitle">Mission board for release health across regions and environments</p>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="header-controls">
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label" for="regionFilter">Region</label>
|
|
||||||
<select
|
|
||||||
id="regionFilter"
|
|
||||||
class="control-select"
|
|
||||||
[value]="selectedRegion()"
|
|
||||||
(change)="onRegionChange($event)"
|
|
||||||
>
|
|
||||||
<option value="all">All Regions</option>
|
|
||||||
@for (region of availableRegions(); track region.value) {
|
|
||||||
<option [value]="region.value">{{ region.label }}</option>
|
|
||||||
}
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="control-group">
|
|
||||||
<label class="control-label" for="timeWindow">Time Window</label>
|
|
||||||
<select
|
|
||||||
id="timeWindow"
|
|
||||||
class="control-select"
|
|
||||||
[value]="selectedTimeWindow()"
|
|
||||||
(change)="onTimeWindowChange($event)"
|
|
||||||
>
|
|
||||||
<option value="1h">Last 1h</option>
|
|
||||||
<option value="24h">Last 24h</option>
|
|
||||||
<option value="7d">Last 7d</option>
|
|
||||||
<option value="30d">Last 30d</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<!-- Mission Summary Strip -->
|
<!-- Mission Summary Strip -->
|
||||||
@@ -449,34 +418,6 @@ interface MissionSummary {
|
|||||||
margin: 0.25rem 0 0;
|
margin: 0.25rem 0 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
.header-controls {
|
|
||||||
display: flex;
|
|
||||||
gap: 1rem;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-group {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 0.25rem;
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-label {
|
|
||||||
font-size: 0.75rem;
|
|
||||||
color: var(--color-text-secondary);
|
|
||||||
font-weight: var(--font-weight-medium);
|
|
||||||
}
|
|
||||||
|
|
||||||
.control-select {
|
|
||||||
padding: 0.4rem 0.75rem;
|
|
||||||
border: 1px solid var(--color-border-primary);
|
|
||||||
border-radius: var(--radius-sm);
|
|
||||||
background: var(--color-surface-primary);
|
|
||||||
color: var(--color-text-primary);
|
|
||||||
font-size: 0.9rem;
|
|
||||||
min-width: 140px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Mission Summary Strip */
|
/* Mission Summary Strip */
|
||||||
.mission-summary {
|
.mission-summary {
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -1020,14 +961,6 @@ interface MissionSummary {
|
|||||||
})
|
})
|
||||||
export class DashboardV3Component {
|
export class DashboardV3Component {
|
||||||
private readonly context = inject(PlatformContextStore);
|
private readonly context = inject(PlatformContextStore);
|
||||||
readonly selectedRegion = signal<string>('all');
|
|
||||||
readonly selectedTimeWindow = signal<string>('24h');
|
|
||||||
|
|
||||||
private readonly fallbackRegionOptions = [
|
|
||||||
{ value: 'eu-west', label: 'EU West' },
|
|
||||||
{ value: 'us-east', label: 'US East' },
|
|
||||||
{ value: 'apac', label: 'APAC' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// Placeholder mission summary data
|
// Placeholder mission summary data
|
||||||
readonly summary = signal<MissionSummary>({
|
readonly summary = signal<MissionSummary>({
|
||||||
@@ -1114,18 +1047,6 @@ export class DashboardV3Component {
|
|||||||
this.context.initialize();
|
this.context.initialize();
|
||||||
}
|
}
|
||||||
|
|
||||||
readonly availableRegions = computed(() => {
|
|
||||||
const regions = this.context.regions();
|
|
||||||
if (regions.length === 0) {
|
|
||||||
return this.fallbackRegionOptions;
|
|
||||||
}
|
|
||||||
|
|
||||||
return regions.map((region) => ({
|
|
||||||
value: region.regionId,
|
|
||||||
label: region.displayName,
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
readonly allEnvironments = computed(() => {
|
readonly allEnvironments = computed(() => {
|
||||||
const environments = this.context.environments();
|
const environments = this.context.environments();
|
||||||
if (environments.length === 0) {
|
if (environments.length === 0) {
|
||||||
@@ -1136,10 +1057,14 @@ export class DashboardV3Component {
|
|||||||
});
|
});
|
||||||
|
|
||||||
readonly filteredEnvironments = computed(() => {
|
readonly filteredEnvironments = computed(() => {
|
||||||
const region = this.selectedRegion();
|
|
||||||
const environments = this.allEnvironments();
|
const environments = this.allEnvironments();
|
||||||
if (region === 'all') return environments;
|
const selectedRegions = this.context.selectedRegions();
|
||||||
return environments.filter((environment) => environment.regionId === region);
|
if (selectedRegions.length === 0) {
|
||||||
|
return environments;
|
||||||
|
}
|
||||||
|
|
||||||
|
const allowed = new Set(selectedRegions);
|
||||||
|
return environments.filter((environment) => allowed.has(environment.regionId));
|
||||||
});
|
});
|
||||||
|
|
||||||
readonly riskEnvironments = computed(() =>
|
readonly riskEnvironments = computed(() =>
|
||||||
@@ -1198,16 +1123,6 @@ export class DashboardV3Component {
|
|||||||
},
|
},
|
||||||
]);
|
]);
|
||||||
|
|
||||||
onRegionChange(event: Event): void {
|
|
||||||
const select = event.target as HTMLSelectElement;
|
|
||||||
this.selectedRegion.set(select.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
onTimeWindowChange(event: Event): void {
|
|
||||||
const select = event.target as HTMLSelectElement;
|
|
||||||
this.selectedTimeWindow.set(select.value);
|
|
||||||
}
|
|
||||||
|
|
||||||
environmentPostureRoute(env: EnvironmentCard): string[] {
|
environmentPostureRoute(env: EnvironmentCard): string[] {
|
||||||
return ['/setup/topology/environments', env.environmentId, 'posture'];
|
return ['/setup/topology/environments', env.environmentId, 'posture'];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,6 +19,7 @@ import { RouterLink } from '@angular/router';
|
|||||||
<a
|
<a
|
||||||
routerLink="/setup/trust-signing/watchlist/alerts"
|
routerLink="/setup/trust-signing/watchlist/alerts"
|
||||||
[queryParams]="{ alertId: 'alert-001', returnTo: '/mission-control/alerts', scope: 'tenant', tab: 'alerts' }"
|
[queryParams]="{ alertId: 'alert-001', returnTo: '/mission-control/alerts', scope: 'tenant', tab: 'alerts' }"
|
||||||
|
queryParamsHandling="merge"
|
||||||
>
|
>
|
||||||
Identity watchlist alert requires signer review
|
Identity watchlist alert requires signer review
|
||||||
</a>
|
</a>
|
||||||
|
|||||||
Reference in New Issue
Block a user