Emit canonical platform context preference payload
This commit is contained in:
@@ -0,0 +1,75 @@
|
|||||||
|
# Sprint 20260307-010 - FE Context Preferences Canonical Payload
|
||||||
|
|
||||||
|
## Topic & Scope
|
||||||
|
- Eliminate the live `Failed to persist global context preferences.` degradation triggered by Topology environment operator actions.
|
||||||
|
- Align the Web context persistence payload with the documented Platform v2 contract so environment-scoped navigation persists cleanly.
|
||||||
|
- Add focused Web regressions for canonical context payload emission and environment-only query-param persistence.
|
||||||
|
- Working directory: `src/Web/StellaOps.Web`.
|
||||||
|
- Expected evidence: focused Web unit tests, live Playwright verification on `https://stella-ops.local`, and sprint execution log updates.
|
||||||
|
|
||||||
|
## Dependencies & Concurrency
|
||||||
|
- Upstream live repro came from Playwright QA on `https://stella-ops.local/setup/topology/environments/dev/posture`.
|
||||||
|
- Safe parallelism: stay inside `src/Web/StellaOps.Web` plus sprint updates; do not edit unrelated settings/user-menu work already in progress from other agents.
|
||||||
|
- Contract dependency: Platform Service docs already define persisted global context as region/environment/time-window only.
|
||||||
|
|
||||||
|
## Documentation Prerequisites
|
||||||
|
- `docs/modules/platform/platform-service.md`
|
||||||
|
- `docs/modules/platform/architecture-overview.md`
|
||||||
|
- `src/Web/StellaOps.Web/AGENTS.md`
|
||||||
|
|
||||||
|
## Delivery Tracker
|
||||||
|
|
||||||
|
### FE-CTX-001 - Reproduce operator-action context persistence failure
|
||||||
|
Status: DONE
|
||||||
|
Dependency: none
|
||||||
|
Owners: QA
|
||||||
|
Task description:
|
||||||
|
- Use live authenticated Playwright to reproduce the degraded event banner after navigating from Topology environment detail into downstream operator actions.
|
||||||
|
- Capture the exact context persistence request shape that fails on the live stack.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Playwright reproduces the visible degraded banner from a real user path.
|
||||||
|
- [x] The failing request payload and response status are captured.
|
||||||
|
- [x] The issue is scoped to the frontend context persistence path.
|
||||||
|
|
||||||
|
### FE-CTX-002 - Emit canonical context preference payload from the Web store
|
||||||
|
Status: DONE
|
||||||
|
Dependency: FE-CTX-001
|
||||||
|
Owners: Developer
|
||||||
|
Task description:
|
||||||
|
- Update the global platform context store so persisted preferences match the Platform v2 endpoint contract instead of sending unsupported fields.
|
||||||
|
- Add focused Web unit coverage for canonical payload emission and the environment-only query-param navigation case.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Web context persistence emits only supported Platform v2 fields.
|
||||||
|
- [x] Focused unit tests cover canonical payload shape and environment-only query persistence.
|
||||||
|
- [x] The fix stays scoped to Web behavior; no undocumented backend contract changes are introduced.
|
||||||
|
|
||||||
|
### FE-CTX-003 - Replay live topology operator actions after the fix
|
||||||
|
Status: DONE
|
||||||
|
Dependency: FE-CTX-002
|
||||||
|
Owners: QA
|
||||||
|
Task description:
|
||||||
|
- Replay the Topology environment detail operator actions with live authenticated Playwright after the Web fix is built into the running stack.
|
||||||
|
- Confirm the degraded event banner is gone and the downstream pages remain functional.
|
||||||
|
|
||||||
|
Completion criteria:
|
||||||
|
- [x] Live Playwright confirms Topology operator actions no longer trigger the context persistence degradation banner.
|
||||||
|
- [x] Target downstream routes still load correctly after the context update.
|
||||||
|
|
||||||
|
## Execution Log
|
||||||
|
| Date (UTC) | Update | Owner |
|
||||||
|
| --- | --- | --- |
|
||||||
|
| 2026-03-07 | Sprint created after live Playwright on Topology environment operator actions reproduced a visible degraded-state banner tied to global context persistence. | QA |
|
||||||
|
| 2026-03-07 | Captured the failing browser request: `PUT /api/v2/context/preferences` with `{\"tenantId\":null,\"regions\":[],\"environments\":[\"dev\"],\"timeWindow\":\"24h\",\"stage\":\"all\"}` returning `400` from Platform while route navigation itself remained functional. | QA |
|
||||||
|
| 2026-03-07 | Updated the Web platform context store to emit only canonical Platform v2 fields and added focused unit coverage for canonical payload emission plus environment-only query persistence. | Developer |
|
||||||
|
| 2026-03-07 | Live replay after the Web patch proved the frontend contract issue was fixed but exposed a Router bridge defect: the request moved from gateway `404` to dispatched `400`, then cleared after the follow-up Router bridge remediation. Final live Playwright replay now shows `PUT /api/v2/context/preferences` returning `200` and no degraded banner across Topology operator actions. | QA |
|
||||||
|
|
||||||
|
## Decisions & Risks
|
||||||
|
- Decision: follow the documented Platform v2 context contract in `docs/modules/platform/platform-service.md` instead of expanding the backend surface during this Web-scoped iteration.
|
||||||
|
- Risk: `stage` is still a Web-only query-state concept and is not part of persisted Platform context, so this fix removes the live failure without changing the backend persistence schema.
|
||||||
|
- Decision: keep the frontend contract correction separate from the Router bridge remediation so future regressions can distinguish payload-shape failures from transport/dispatch failures.
|
||||||
|
- Residual risk: the persisted Platform environment list now contains the UI-facing `dev` identifier from live navigation, which is acceptable for the current client-ready flow but should be reviewed when deeper topology/integration validation begins.
|
||||||
|
|
||||||
|
## Next Checkpoints
|
||||||
|
- 2026-03-07: continue page/action QA sweeps with Playwright now that the obvious Topology context persistence failure is removed.
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { provideHttpClient, withInterceptorsFromDi } from '@angular/common/http';
|
||||||
|
import { HttpTestingController, provideHttpClientTesting } from '@angular/common/http/testing';
|
||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
|
||||||
|
import { PlatformContextStore } from './platform-context.store';
|
||||||
|
|
||||||
|
describe('PlatformContextStore', () => {
|
||||||
|
let store: PlatformContextStore;
|
||||||
|
let httpMock: HttpTestingController;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
PlatformContextStore,
|
||||||
|
provideHttpClient(withInterceptorsFromDi()),
|
||||||
|
provideHttpClientTesting(),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
store = TestBed.inject(PlatformContextStore);
|
||||||
|
httpMock = TestBed.inject(HttpTestingController);
|
||||||
|
|
||||||
|
(store as any).apiDisabled = false;
|
||||||
|
(store as any).persistPaused = false;
|
||||||
|
store.error.set(null);
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(() => {
|
||||||
|
httpMock.verify();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists only the canonical Platform context payload fields', () => {
|
||||||
|
store.selectedRegions.set(['us-east']);
|
||||||
|
store.selectedEnvironments.set(['dev']);
|
||||||
|
store.timeWindow.set('24h');
|
||||||
|
store.stage.set('all');
|
||||||
|
store.tenantId.set('demo-prod');
|
||||||
|
|
||||||
|
(store as any).persistPreferences();
|
||||||
|
|
||||||
|
const req = httpMock.expectOne('/api/v2/context/preferences');
|
||||||
|
expect(req.request.method).toBe('PUT');
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
regions: ['us-east'],
|
||||||
|
environments: ['dev'],
|
||||||
|
timeWindow: '24h',
|
||||||
|
});
|
||||||
|
expect(req.request.body.tenantId).toBeUndefined();
|
||||||
|
expect(req.request.body.stage).toBeUndefined();
|
||||||
|
|
||||||
|
req.flush({
|
||||||
|
tenantId: 'demo-prod',
|
||||||
|
actorId: 'context-tests',
|
||||||
|
regions: ['us-east'],
|
||||||
|
environments: ['dev'],
|
||||||
|
timeWindow: '24h',
|
||||||
|
updatedAt: '2026-03-07T00:00:00Z',
|
||||||
|
updatedBy: 'context-tests',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('persists environment-only query scope without unsupported fields', () => {
|
||||||
|
store.initialized.set(true);
|
||||||
|
store.environments.set([
|
||||||
|
{
|
||||||
|
environmentId: 'dev',
|
||||||
|
regionId: 'us-east',
|
||||||
|
environmentType: 'development',
|
||||||
|
displayName: 'Development',
|
||||||
|
sortOrder: 10,
|
||||||
|
enabled: true,
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
|
||||||
|
store.applyScopeQueryParams({ environment: 'dev' });
|
||||||
|
|
||||||
|
const req = httpMock.expectOne('/api/v2/context/preferences');
|
||||||
|
expect(req.request.method).toBe('PUT');
|
||||||
|
expect(req.request.body).toEqual({
|
||||||
|
regions: [],
|
||||||
|
environments: ['dev'],
|
||||||
|
timeWindow: '24h',
|
||||||
|
});
|
||||||
|
expect(req.request.body.tenantId).toBeUndefined();
|
||||||
|
expect(req.request.body.stage).toBeUndefined();
|
||||||
|
|
||||||
|
req.flush({
|
||||||
|
tenantId: 'demo-prod',
|
||||||
|
actorId: 'context-tests',
|
||||||
|
regions: ['us-east', 'eu-west', 'apac'],
|
||||||
|
environments: ['dev'],
|
||||||
|
timeWindow: '24h',
|
||||||
|
updatedAt: '2026-03-07T00:00:00Z',
|
||||||
|
updatedBy: 'context-tests',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.selectedEnvironments()).toEqual(['dev']);
|
||||||
|
expect(store.error()).toBeNull();
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -45,6 +45,12 @@ interface PlatformContextQueryState {
|
|||||||
stage: string;
|
stage: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
interface PlatformContextPreferencesRequestPayload {
|
||||||
|
regions: string[];
|
||||||
|
environments: string[];
|
||||||
|
timeWindow: string;
|
||||||
|
}
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
export class PlatformContextStore {
|
export class PlatformContextStore {
|
||||||
private readonly http = inject(HttpClient);
|
private readonly http = inject(HttpClient);
|
||||||
@@ -378,13 +384,7 @@ export class PlatformContextStore {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const payload = {
|
const payload = this.buildPreferencesPayload();
|
||||||
tenantId: this.tenantId(),
|
|
||||||
regions: this.selectedRegions(),
|
|
||||||
environments: this.selectedEnvironments(),
|
|
||||||
timeWindow: this.timeWindow(),
|
|
||||||
stage: this.stage(),
|
|
||||||
};
|
|
||||||
|
|
||||||
this.http
|
this.http
|
||||||
.put<PlatformContextPreferences>('/api/v2/context/preferences', payload)
|
.put<PlatformContextPreferences>('/api/v2/context/preferences', payload)
|
||||||
@@ -396,6 +396,14 @@ export class PlatformContextStore {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private buildPreferencesPayload(): PlatformContextPreferencesRequestPayload {
|
||||||
|
return {
|
||||||
|
regions: this.selectedRegions(),
|
||||||
|
environments: this.selectedEnvironments(),
|
||||||
|
timeWindow: this.timeWindow(),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private finishInitialization(): void {
|
private finishInitialization(): void {
|
||||||
this.loading.set(false);
|
this.loading.set(false);
|
||||||
this.initialized.set(true);
|
this.initialized.set(true);
|
||||||
|
|||||||
Reference in New Issue
Block a user