diff --git a/docs/implplan/SPRINT_20260307_010_FE_context_preferences_canonical_payload.md b/docs/implplan/SPRINT_20260307_010_FE_context_preferences_canonical_payload.md new file mode 100644 index 000000000..4b1eea817 --- /dev/null +++ b/docs/implplan/SPRINT_20260307_010_FE_context_preferences_canonical_payload.md @@ -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. diff --git a/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.spec.ts b/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.spec.ts new file mode 100644 index 000000000..e240bc7e0 --- /dev/null +++ b/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.spec.ts @@ -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(); + }); +}); diff --git a/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.ts b/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.ts index 8c19c18a2..a011321a2 100644 --- a/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.ts +++ b/src/Web/StellaOps.Web/src/app/core/context/platform-context.store.ts @@ -45,6 +45,12 @@ interface PlatformContextQueryState { stage: string; } +interface PlatformContextPreferencesRequestPayload { + regions: string[]; + environments: string[]; + timeWindow: string; +} + @Injectable({ providedIn: 'root' }) export class PlatformContextStore { private readonly http = inject(HttpClient); @@ -378,13 +384,7 @@ export class PlatformContextStore { return; } - const payload = { - tenantId: this.tenantId(), - regions: this.selectedRegions(), - environments: this.selectedEnvironments(), - timeWindow: this.timeWindow(), - stage: this.stage(), - }; + const payload = this.buildPreferencesPayload(); this.http .put('/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 { this.loading.set(false); this.initialized.set(true);