diff --git a/README.md b/README.md index 1b96150c9b..8ec3faa28c 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ PastureStack is an independent community effort to preserve, audit, and moderniz ## Project status -The current compatibility release is `1.6.106`. It retains the existing Node 24, Ember, Sass, +The current compatibility release is `1.6.107`. It retains the existing Node 24, Ember, Sass, dependency, browser-smoke, terminal, console, and test-harness modernization. It adds a provider-neutral OpenID Connect administration and sign-in flow with PKCE S256, staged configuration validation, a real test login before @@ -16,6 +16,13 @@ activation, and local-authentication recovery. Product-owned names, logos, icons, package metadata, and visible text use PastureStack branding. API models and protocol fields remain compatible. +Release `1.6.107` closes the successful first-create navigation race found by +real-host acceptance testing. Completion now treats the stack query parameter +as immutable route authority and never dereferences the API's mutable saved +resource merely to decide where to navigate. Empty service links still avoid +the redundant action, while non-empty links remain persisted before leaving +the form. + Release `1.6.106` fixes the real first-service creation completion path. An empty service-link set no longer sends a redundant action after the service is already persisted, and a non-empty link update preserves the stack route diff --git a/SECURITY.md b/SECURITY.md index 8dee9a1748..9eed387a30 100644 --- a/SECURITY.md +++ b/SECURITY.md @@ -2,7 +2,7 @@ ## Supported state -The maintained compatibility release is the pure numeric `1.6.106` line used +The maintained compatibility release is the pure numeric `1.6.107` line used by the current PastureStack Server release. Earlier branded coordinates are historical records and are not current release or deployment targets. Authentication-provider combinations must still be validated by an diff --git a/app/components/new-container/component.js b/app/components/new-container/component.js index 52b19b523b..1c4569088e 100644 --- a/app/components/new-container/component.js +++ b/app/components/new-container/component.js @@ -297,7 +297,7 @@ export default Component.extend(NewOrEdit, SelectTab, { 'primaryService.stackId', 'service.stackId', function() { - return this.get('primaryService.stackId') || this.get('service.stackId') || null; + return read(this.get('primaryService'), 'stackId') || read(this.get('service'), 'stackId') || null; } ), @@ -558,28 +558,11 @@ export default Component.extend(NewOrEdit, SelectTab, { if ( this.get('isService') ) { let service = savedResource || this.get('service'); - let stackId = service && (typeof service.get === 'function' ? service.get('stackId') : service.stackId); - - stackId = stackId || this.get('preflightStackId'); - - // A partial setservicelinks response can replace fields on the saved - // service. Preserve the route identity captured before that action so - // a successful create cannot remain on the form and invite a duplicate. - return this.setServiceLinks(service).then(() => { - if ( service && stackId ) { - let current = typeof service.get === 'function' ? service.get('stackId') : service.stackId; - - if ( !current ) { - if ( typeof service.set === 'function' ) { - service.set('stackId', stackId); - } else { - service.stackId = stackId; - } - } - } - return service; - }); + // The saved API resource is completion data, not route authority. A + // partial response may no longer be readable after persistence, so keep + // it in the chain without dereferencing it during navigation. + return this.setServiceLinks(service).then(() => service); } return savedResource; diff --git a/app/service/new/controller.js b/app/service/new/controller.js index 2934bbfcc3..0e3cbd3f1e 100644 --- a/app/service/new/controller.js +++ b/app/service/new/controller.js @@ -8,18 +8,16 @@ export default Controller.extend({ upgrade: null, actions: { - done(savedService) { + done() { if ( this.get('upgrade') ) { this.send('goToPrevious','stacks'); } else { - let model = this.get('model'); - let modelStackId = model && (typeof model.get === 'function' ? model.get('stackId') : model.stackId); - let modelService = model && (typeof model.get === 'function' ? model.get('service') : model.service); - let stackId = savedService && (typeof savedService.get === 'function' ? savedService.get('stackId') : savedService.stackId); + let stackId = this.get('stackId'); - stackId = stackId || modelStackId || (modelService && (typeof modelService.get === 'function' ? modelService.get('stackId') : modelService.stackId)) || this.get('stackId'); - - return this.get('router').transitionTo('stack', stackId); + // The query parameter is the immutable route input. Do not derive + // navigation from a resource that the successful create response may + // replace or partially hydrate. + return stackId ? this.get('router').transitionTo('stack', stackId) : this.get('router').transitionTo('stacks'); } }, diff --git a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json index 665840733c..37d2621e61 100644 --- a/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json +++ b/docs/baselines/npm-package-lock.sass-replacement.node24-ignore-scripts.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.106", + "version": "1.6.107", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.106", + "version": "1.6.107", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package-lock.json b/package-lock.json index 665840733c..37d2621e61 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.106", + "version": "1.6.107", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.106", + "version": "1.6.107", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package.json b/package.json index 31dfe32819..c7036f2d4b 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.106", + "version": "1.6.107", "private": true, "description": "PastureStack browser console for the compatible control platform.", "repository": { diff --git a/scripts/check-modernization-blockers b/scripts/check-modernization-blockers index 788a14c9f0..47462819f3 100755 --- a/scripts/check-modernization-blockers +++ b/scripts/check-modernization-blockers @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f: print(json.load(f).get('version', '')) PY ) -if [[ "$version" != "1.6.106" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.106" +if [[ "$version" != "1.6.107" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.107" failures=$((failures + 1)) fi diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index d4a0b55d2a..05134b4134 100755 --- a/scripts/check-ui-console-workspace +++ b/scripts/check-ui-console-workspace @@ -141,4 +141,4 @@ if [[ -n ${PASTURESTACK_PRIVATE_MARKER:-} ]] && grep -RInF -- "$PASTURESTACK_PRI fi printf 'UI_CONSOLE_WORKSPACE_OK version=%s persistence=%s cross_tab=%s\n' \ - 1.6.106 browser-session broker-broadcast + 1.6.107 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index e2e7223936..ade46636ba 100755 --- a/scripts/check-ui-critical-high-dependencies +++ b/scripts/check-ui-critical-high-dependencies @@ -66,7 +66,7 @@ if lock_bytes != baseline_bytes: lock = json.loads(lock_bytes) packages = lock.get("packages", {}) root = packages.get("", {}) -if package.get("version") != "1.6.106": +if package.get("version") != "1.6.107": fail(f"unexpected Web Console version: {package.get('version')}") if root.get("version") != package.get("version"): fail(f"lock root version differs: {root.get('version')}") diff --git a/tests/unit/components/new-container-hardware-payload-test.js b/tests/unit/components/new-container-hardware-payload-test.js index f17c3c760e..b8f3dae20b 100644 --- a/tests/unit/components/new-container-hardware-payload-test.js +++ b/tests/unit/components/new-container-hardware-payload-test.js @@ -122,27 +122,28 @@ test('first service creation keeps the saved service through links and navigatio destroyOwned(component); }); -test('service link persistence cannot erase the saved stack route identity', async function(assert) { +test('service link persistence keeps the saved service without reading response route fields', async function(assert) { let launchConfig = hardwareLaunchConfig(); let service = EmberObject.create({ - id: '1s-new', - stackId: '1st-new', launchConfig, secondaryLaunchConfigs: A(), save() { return Promise.resolve(this); }, + }); + let savedResource = { + get() { + throw new Error('completion must not read fields from the saved response'); + }, doAction(name) { assert.equal(name, 'setservicelinks'); - this.set('stackId', undefined); return Promise.resolve(); }, - }); + }; let component = createComponent(service, launchConfig); run(() => component.set('serviceLinksArray', A([{serviceId: '1s-linked', name: 'db'}]))); - let linked = await component.didSave(service); + let linked = await component.didSave(savedResource); - assert.strictEqual(linked, service, 'the persisted service stays in the completion chain'); - assert.equal(service.get('stackId'), '1st-new', 'the stack route identity is restored after a partial action response'); + assert.strictEqual(linked, savedResource, 'the persisted service stays in the completion chain'); destroyOwned(component); }); diff --git a/tests/unit/service/new/controller-test.js b/tests/unit/service/new/controller-test.js index 130afc3808..cdc480f840 100644 --- a/tests/unit/service/new/controller-test.js +++ b/tests/unit/service/new/controller-test.js @@ -10,7 +10,7 @@ import NewVirtualMachineController from 'ui/service/new-virtualmachine/controlle module('Unit | Controller | service | new'); -test('first-create navigation uses the persisted service and has a safe query fallback', function(assert) { +test('first-create navigation uses only the immutable stack route input', function(assert) { let transitions = []; let controller = NewServiceController.create({ stackId: '1st-query', @@ -22,14 +22,14 @@ test('first-create navigation uses the persisted service and has a safe query fa }, }); - controller.actions.done.call(controller, EmberObject.create({stackId: '1st-saved'})); - run(() => controller.set('model', EmberObject.create({stackId: '1st-model', service: EmberObject.create({})}))); - controller.actions.done.call(controller); + controller.actions.done.call(controller, {get() { throw new Error('saved response must not be read'); }}); + run(() => controller.set('stackId', null)); + controller.actions.done.call(controller, {get() { throw new Error('missing-route fallback must not read response'); }}); assert.deepEqual(transitions, [ - {route: 'stack', stackId: '1st-saved'}, - {route: 'stack', stackId: '1st-model'}, - ], 'navigation uses the immutable route identity even if a response clears the service field'); + {route: 'stack', stackId: '1st-query'}, + {route: 'stacks', stackId: undefined}, + ], 'navigation cannot be broken by a partial or unreadable saved resource'); run(() => controller.destroy()); });