diff --git a/README.md b/README.md index 8e1ab051d7..0483ad77b9 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,15 @@ PastureStack is an independent community effort to preserve, audit, and moderniz ## Project status +Candidate `1.6.173` repairs empty environment-template choice labels. These +choices are native `ProjectTemplate` resources, not Catalog templates: their +authoritative label is `name`, and selection remains bound to the template ID. +The focused regression covers the actual model, rendered choices, renaming and +selection. Official validation, immutable publication, Server packaging and +packaged native acceptance are pending; this candidate does not promote any +historical HOLD or claim the complete permission matrix is finished. See the +[release note](docs/releases/web-console-1.6.173.md). + Published `1.6.172` preserves API-key create-only first delivery when a redacted subscribe model arrives before POST/201. Only the API-key editor opts into a request-private, Schema-bound delivery to its detached clone; newer canonical diff --git a/app/components/view-edit-project/component.js b/app/components/view-edit-project/component.js index 4ce2c49f4b..07f50a298a 100644 --- a/app/components/view-edit-project/component.js +++ b/app/components/view-edit-project/component.js @@ -103,7 +103,7 @@ export default Component.extend(NewOrEdit, Sortable, { var active = this.get('project.projectTemplateId'); var choices = this.get('projectTemplates').map((tpl) => { - return {id: tpl.id, name: tpl.get('localizedName'), image: tpl.get('orchestrationIcon')}; + return {id: tpl.id, name: tpl.get('name'), image: tpl.get('orchestrationIcon')}; }); if ( !choices.length ) { 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 1d8631e649..70f107e71c 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.172", + "version": "1.6.173", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.172", + "version": "1.6.173", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/docs/releases/web-console-1.6.173.md b/docs/releases/web-console-1.6.173.md new file mode 100644 index 0000000000..5808e39b6d --- /dev/null +++ b/docs/releases/web-console-1.6.173.md @@ -0,0 +1,30 @@ +# Web Console 1.6.173 + +Candidate; official validation and publication are pending. + +## Root cause and minimal repair + +The environment-create form maps native `ProjectTemplate` resources to choice +cards. It incorrectly read `localizedName`, a Catalog Template property which +the native model does not implement, leaving labels empty. Read the native +resource's `name` instead. Preserve the existing name-dependent sorting and +selection by exact template ID; do not translate user-defined names or add a +fallback which conceals a missing model contract. + +The bounded consumer review found this incorrect native-model lookup only in +`app/components/view-edit-project/component.js`. Catalog templates legitimately +use `localizedName` and are unchanged. No API, permission, membership, save-hook, +OIDC, MFA, session-generation or WebSocket contract changes. + +## Verification and release boundaries + +Regressions must use the real native model without an invented `localizedName`, +verify rendered labels and sorting, rename reactivity and exact-ID card +selection. Existing environment permission tests remain applicable. Packaged +native creation, owner/member Host acceptance and the fresh API-key delivery +acceptance remain separate gates. Historical HOLD receipts remain HOLD and +the full permission/resource/locale matrix remains INCOMPLETE. + +Use a new immutable numeric component tag and a new immutable Server patch +image. Preserve existing Compose options, volumes, HTTPS origin and prior image +for rollback. Do not deploy the company instance as part of this QA repair. diff --git a/package-lock.json b/package-lock.json index 1d8631e649..70f107e71c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.172", + "version": "1.6.173", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.172", + "version": "1.6.173", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package.json b/package.json index 22851ccf29..9ed5e6b3ee 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.172", + "version": "1.6.173", "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 40caa864f2..5252a4815d 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.172" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.172" +if [[ "$version" != "1.6.173" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.173" failures=$((failures + 1)) fi diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index 35d05d92c9..19e8ab7bc4 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.172 browser-session broker-broadcast + 1.6.173 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index 0b2c02ef9b..6d93fa02fe 100755 --- a/scripts/check-ui-critical-high-dependencies +++ b/scripts/check-ui-critical-high-dependencies @@ -70,7 +70,7 @@ if lock_bytes != baseline_bytes: lock = json.loads(lock_bytes) packages = lock.get("packages", {}) root = packages.get("", {}) -if package.get("version") != "1.6.172": +if package.get("version") != "1.6.173": 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/integration/components/view-edit-project-permissions-test.js b/tests/integration/components/view-edit-project-permissions-test.js index 55d48aac83..bcca300866 100644 --- a/tests/integration/components/view-edit-project-permissions-test.js +++ b/tests/integration/components/view-edit-project-permissions-test.js @@ -7,7 +7,9 @@ import { click, find, findAll, render, settled, setupContext, setupRenderingCont import { module, test } from 'qunit'; import { initialize as initializePodLayouts } from 'ui/initializers/pod-component-layouts'; +import ProjectTemplate from 'ui/models/projecttemplate'; import Router from 'ui/router'; +import { destroyOwned } from '../../helpers/owned-subject'; import resolver from '../../helpers/resolver'; module('Integration | Component | view edit project permissions', function(hooks) { @@ -61,9 +63,65 @@ module('Integration | Component | view edit project permissions', function(hooks hooks.afterEach(async function() { await teardownContext(this); + (this.nativeTemplates || []).forEach((template) => destroyOwned(template)); this.testRoot.remove(); }); + test('new environment cards render real native names and select the same ID after a rename', async function(assert) { + this.app = EmberObject.create({baseAssets: '/'}); + let zulu = ProjectTemplate.create({id: '1pt-zulu', name: 'Zulu environment', stacks: A([]), app: this.app}); + let alpha = ProjectTemplate.create({id: '1pt-alpha', name: 'alpha environment', stacks: A([]), app: this.app}); + this.nativeTemplates = [zulu, alpha]; + this.projectTemplates = A(this.nativeTemplates); + this.project.setProperties({id: null, projectTemplateId: zulu.id}); + assert.strictEqual(alpha.get('localizedName'), undefined, 'the real native model supplies no catalog name alias'); + + await render(precompileTemplate(`{{view-edit-project + project=this.project originalProject=this.originalProject projectTemplates=this.projectTemplates + app=this.app network=this.network policyManager=this.policyManager userStore=this.userStore + showEdit=true editing=false + }}`)); + + assert.deepEqual(findAll('.orchestration-driver .clip').map((label) => label.textContent.trim()), + ['alpha environment', 'Zulu environment'], 'native user-defined labels render in case-insensitive order'); + assert.strictEqual(findAll('.orchestration-driver.active').length, 1, 'one native ID is selected'); + assert.strictEqual(find('.orchestration-driver.active .clip').textContent.trim(), 'Zulu environment'); + + await click(findAll('.orchestration-driver')[0]); + assert.strictEqual(this.project.get('projectTemplateId'), alpha.id, 'the real card action stores its exact model ID'); + assert.strictEqual(find('.orchestration-driver.active .clip').textContent.trim(), 'alpha environment'); + + alpha.set('name', 'zz renamed environment'); + await settled(); + assert.deepEqual(findAll('.orchestration-driver .clip').map((label) => label.textContent.trim()), + ['Zulu environment', 'zz renamed environment'], 'renaming updates the rendered label and card order'); + assert.strictEqual(this.project.get('projectTemplateId'), alpha.id, 'the selected ID survives reordering'); + assert.strictEqual(findAll('.orchestration-driver.active').length, 1); + assert.strictEqual(find('.orchestration-driver.active .clip').textContent.trim(), 'zz renamed environment'); + }); + + test('new environment renders and selects the None card for an empty native template list', async function(assert) { + this.app = EmberObject.create({baseAssets: '/'}); + this.projectTemplates = A([]); + this.project.setProperties({id: null, projectTemplateId: '1pt-stale'}); + + await render(precompileTemplate(`{{view-edit-project + project=this.project originalProject=this.originalProject projectTemplates=this.projectTemplates + app=this.app network=this.network policyManager=this.policyManager userStore=this.userStore + showEdit=true editing=false + }}`)); + + assert.strictEqual(findAll('.orchestration-driver').length, 1); + assert.strictEqual(find('.orchestration-driver .clip').textContent.trim(), 'None'); + assert.strictEqual(find('.orchestration-driver img').getAttribute('src'), '/assets/images/logos/pasturestack-mark.svg'); + assert.strictEqual(findAll('.orchestration-driver.active').length, 0, 'a stale ID is not selected'); + assert.ok(find('.well .text-center').textContent.includes('viewEditProject.nativeEngine'), 'the empty-list summary stays native'); + + await click('.orchestration-driver'); + assert.strictEqual(this.project.get('projectTemplateId'), null, 'the native fallback action clears the stale ID'); + assert.strictEqual(findAll('.orchestration-driver.active').length, 1, 'the null-ID card becomes selected'); + }); + test('direct edit URL follows member, metadata, and network links independently', async function(assert) { let cancelled = 0; this.cancel = () => { cancelled++; }; diff --git a/tests/unit/components/view-edit-project-permissions-test.js b/tests/unit/components/view-edit-project-permissions-test.js index 3afc96563e..ce77e9afab 100644 --- a/tests/unit/components/view-edit-project-permissions-test.js +++ b/tests/unit/components/view-edit-project-permissions-test.js @@ -4,6 +4,7 @@ import { resolve } from 'rsvp'; import { module, test } from 'qunit'; import ViewEditProject from 'ui/components/view-edit-project/component'; +import ProjectTemplate from 'ui/models/projecttemplate'; import inertRenderer from '../../helpers/inert-renderer'; import { createOwned, destroyOwned } from '../../helpers/owned-subject'; @@ -25,6 +26,62 @@ function makeComponent(project, network, options = {}) { }, 'component'); } +test('native project template choices use names, keep IDs, and react to renames', function(assert) { + let app = EmberObject.create({baseAssets: '/'}); + let zulu = ProjectTemplate.create({id: '1pt-zulu', name: 'Zulu environment', stacks: A([]), app}); + let alpha = ProjectTemplate.create({id: '1pt-alpha', name: 'alpha environment', stacks: A([]), app}); + let project = EmberObject.create({id: null, projectTemplateId: zulu.id, projectMembers: A([])}); + let component = makeComponent(project, null, {app, editing: false, projectTemplates: A([zulu, alpha])}); + + try { + assert.strictEqual(zulu.get('localizedName'), undefined, 'the real native model has no catalog localizedName'); + assert.strictEqual(alpha.get('localizedName'), undefined, 'no fixture alias hides a missing native name'); + assert.deepEqual(component.get('templateChoices').map(({id, name, image}) => ({id, name, image})), [ + {id: alpha.id, name: 'alpha environment', image: alpha.get('orchestrationIcon')}, + {id: zulu.id, name: 'Zulu environment', image: zulu.get('orchestrationIcon')}, + ], 'user-defined names are sorted case-insensitively without losing IDs or icons'); + + component.send('selectTemplate', alpha.id); + assert.strictEqual(project.get('projectTemplateId'), alpha.id, 'the action stores the selected native ID, not its name'); + assert.strictEqual(component.get('selectedProjectTemplate'), alpha, 'the selection resolves the actual native model'); + + alpha.set('name', 'zz renamed environment'); + assert.deepEqual(component.get('templateChoices').map(({id, name}) => ({id, name})), [ + {id: zulu.id, name: 'Zulu environment'}, + {id: alpha.id, name: 'zz renamed environment'}, + ], 'the existing name dependency refreshes both labels and order'); + assert.strictEqual(project.get('projectTemplateId'), alpha.id, 'renaming does not replace the selected ID'); + } finally { + destroyOwned(component); + destroyOwned(zulu); + destroyOwned(alpha); + } +}); + +test('an empty native project template list keeps the None choice', function(assert) { + let project = EmberObject.create({id: null, projectTemplateId: '1pt-stale', projectMembers: A([])}); + let component = makeComponent(project, null, { + app: EmberObject.create({baseAssets: '/'}), + editing: false, + projectTemplates: A([]), + }); + + try { + let choices = component.get('templateChoices'); + assert.strictEqual(choices.length, 1); + assert.deepEqual({id: choices[0].id, name: choices[0].name, image: choices[0].image}, { + id: null, + name: 'None', + image: '/assets/images/logos/pasturestack-mark.svg', + }, 'the no-template fallback remains a null-ID native choice'); + component.send('selectTemplate', choices[0].id); + assert.strictEqual(project.get('projectTemplateId'), null, 'the fallback clears a stale template ID'); + assert.strictEqual(component.get('selectedProjectTemplate'), undefined); + } finally { + destroyOwned(component); + } +}); + test('existing environment writes only resources advertised by their own capabilities', async function(assert) { let cases = [ {name: 'readonly', project: false, members: false, network: false},