From 58e53d254bd4a229974bba8908a78d2e8a65fa93 Mon Sep 17 00:00:00 2001 From: chen21019 Date: Thu, 1 Oct 2026 11:19:01 +0800 Subject: [PATCH 1/2] 1.6.162: gate Host container creation and translate Secrets headers --- README.md | 14 +++ app/host/controller.js | 6 ++ app/host/template.hbs | 4 +- app/secrets/index/controller.js | 4 + ...ass-replacement.node24-ignore-scripts.json | 4 +- docs/releases/web-console-1.6.162.md | 41 +++++++++ package-lock.json | 4 +- package.json | 2 +- scripts/check-modernization-blockers | 4 +- tests/integration/scoped-desktop-ui-test.js | 89 +++++++++++++++++++ tests/unit/host/controller-test.js | 80 +++++++++++++++++ tests/unit/secrets/index/controller-test.js | 71 +++++++++++++++ 12 files changed, 315 insertions(+), 8 deletions(-) create mode 100644 docs/releases/web-console-1.6.162.md create mode 100644 tests/integration/scoped-desktop-ui-test.js create mode 100644 tests/unit/host/controller-test.js create mode 100644 tests/unit/secrets/index/controller-test.js diff --git a/README.md b/README.md index 600ee8f74a..19c08deb46 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,20 @@ PastureStack is an independent community effort to preserve, audit, and moderniz ## Project status +Release `1.6.162` is being prepared for two narrow desktop fixes. The Secrets +table headers use the existing generic translations while retaining their +sorting, search and QA mapping fields. Host details show Add Container only +when the current environment's loaded container schema permits creation; +schema reloads and environment changes cannot reuse stale create access. +Container-list and Host-card capability checks and the direct Add route remain +unchanged. The permission gate uses schema capabilities, not role-name checks. +Focused source validation passed Secrets 2/2 and Host 8/8 tests (four new Host +cases plus four adjacent existing cases). Official CI, publication and packaged +browser acceptance are still pending. Responsive/mobile Secrets labels and +all-language layout acceptance are not claimed. Earlier HOLD evidence remains +HOLD and the broader resource/role matrix remains INCOMPLETE. See the +[preparation note](docs/releases/web-console-1.6.162.md). + Published release `1.6.161` fixes existing Certificate metadata edits blocked by the masked private key. Name/description-only edits omit certificate material from the PUT body; new certificates and material replacements keep diff --git a/app/host/controller.js b/app/host/controller.js index a61071f6cd..24aa351e3c 100644 --- a/app/host/controller.js +++ b/app/host/controller.js @@ -1,10 +1,16 @@ import { alias } from '@ember/object/computed'; import Controller, { inject as controller } from '@ember/controller'; +import { service } from '@ember/service'; export default Controller.extend({ application: controller(), + projects: service(), host: alias('model.host'), + canCreateContainer: function() { + return this.get('projects').canCreateResource('container'); + }.property('projects.current.id', 'projects.schemaProjectId', 'projects.schemaLoadGeneration'), + actions: { changeHost(host) { this.get('router').transitionTo('host', host.get('id')); diff --git a/app/host/template.hbs b/app/host/template.hbs index 873745efff..45cd72aa44 100644 --- a/app/host/template.hbs +++ b/app/host/template.hbs @@ -7,7 +7,9 @@
{{#action-menu model=this.host size="sm" classNames="r-ml10 pull-right"}} - {{t 'hostsPage.hostPage.addContainer.linkTo'}} + {{#if this.canCreateContainer}} + {{t 'hostsPage.hostPage.addContainer.linkTo'}} + {{/if}} {{/action-menu}} {{header-state model=this.host classNames="pull-right"}} diff --git a/app/secrets/index/controller.js b/app/secrets/index/controller.js index ea0357e943..5a77c9143d 100644 --- a/app/secrets/index/controller.js +++ b/app/secrets/index/controller.js @@ -49,6 +49,7 @@ export default Controller.extend({ headers: [ { displayName: 'State', + translationKey: 'generic.state', name: 'stateSort', sort: ['stateSort','name','id'], type: 'string', @@ -58,18 +59,21 @@ export default Controller.extend({ }, { displayName: 'Name', + translationKey: 'generic.name', name: 'name', sort: ['name','id'], type: 'string', }, { displayName: 'Description', + translationKey: 'generic.description', name: 'description', sort: ['description','name','id'], type: 'string', }, { displayName: 'Created', + translationKey: 'generic.created', name: 'created', sort: ['primaryHost.displayName','name','id'], searchField: false, 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 937d12900b..b9ee3ee126 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.161", + "version": "1.6.162", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.161", + "version": "1.6.162", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/docs/releases/web-console-1.6.162.md b/docs/releases/web-console-1.6.162.md new file mode 100644 index 0000000000..339660022f --- /dev/null +++ b/docs/releases/web-console-1.6.162.md @@ -0,0 +1,41 @@ +# Web Console 1.6.162 + +Preparation only. Official CI, publication and packaged browser acceptance are +pending; this note does not declare an artifact or deployment ready. + +## Narrow desktop fixes + +The Secrets table's State, Name, Description and Created desktop headers use +the existing generic translation keys. Their fallback display names, column +names, sorting, search fields, widths and existing QA mappings are unchanged. +No new translations or shared table-component changes are introduced. + +The Host-detail Add Container link now follows the current environment's +loaded container-create capability. Missing or stale schema readiness and a +schema reload that removes POST permission hide the link. Roles whose current +container schema permits POST retain the same link and selected Host query. +The existing Container-list, Host-card and direct Add-route capability checks +are unchanged. This is not a role-name rule or a backend authorization change. + +## Source validation and remaining acceptance + +Focused native Chrome 153 source validation passed Secrets 2/2 tests and Host +8/8 tests. The Host filter executed four new cases plus four adjacent existing +cases, not only the four new cases. Tests cover schema readiness, create +capability, environment switching, same-environment schema generations and +preservation of the Secrets table's legacy fields. + +Two additional narrow rendering checks use the actual Host template and the +actual desktop table-header translation chain. Their results are recorded +separately from the preceding controller tests and from packaged acceptance. + +Responsive/mobile Secrets data-title labels still use the shared component's +legacy displayName mechanism and are outside this fix. No mobile or +all-language layout acceptance is claimed. The Host template rendering test +checks link visibility and query binding, not server write authorization. + +No API, authentication, session, OIDC, MFA, database, stored-data, runtime-host +or VM contract changes are included. The earlier HOLD receipts remain HOLD; +the broader resource/role matrix remains INCOMPLETE. Packaged desktop checks +on the subsequent Server artifact remain required. No company-site deployment +is authorized. diff --git a/package-lock.json b/package-lock.json index 937d12900b..b9ee3ee126 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.161", + "version": "1.6.162", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.161", + "version": "1.6.162", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package.json b/package.json index c711f68631..f000ce2db4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.161", + "version": "1.6.162", "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 8defb663b2..55b0f7776d 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.161" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.161" +if [[ "$version" != "1.6.162" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.162" failures=$((failures + 1)) fi diff --git a/tests/integration/scoped-desktop-ui-test.js b/tests/integration/scoped-desktop-ui-test.js new file mode 100644 index 0000000000..7ced6b6358 --- /dev/null +++ b/tests/integration/scoped-desktop-ui-test.js @@ -0,0 +1,89 @@ +import EmberObject from '@ember/object'; +import { A } from '@ember/array'; +import { alias } from '@ember/object/computed'; +import Component from '@ember/component'; +import { helper } from '@ember/component/helper'; +import Service from '@ember/service'; +import { run } from '@ember/runloop'; +import { precompileTemplate } from '@ember/template-compilation'; +import { find, findAll, render, settled, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers'; +import { module, test } from 'qunit'; + +import HostTemplate from 'ui/host/template'; +import SecretsIndexController from 'ui/secrets/index/controller'; +import { initialize as initializeIntl } from 'ui/instance-initializers/intl'; +import resolver from '../helpers/resolver'; + +module('Integration | UI | scoped desktop fixes', function(hooks) { + hooks.beforeEach(async function() { + this.testRoot = document.createElement('div'); + this.testRoot.id = 'ember-testing'; + document.body.appendChild(this.testRoot); + await setupContext(this, {resolver}); + initializeIntl(this.owner); + this.intl = this.owner.lookup('service:intl'); + for (let locale of ['en-us', 'zh-tw']) { + this.intl.addTranslations(locale, await (await fetch(`/translations/${locale}.json`)).json()); + } + this.owner.register('service:prefs', Service.extend({tablePerPage: 25})); + await setupRenderingContext(this); + }); + + hooks.afterEach(async function() { + if (this.secretsController) { + run(() => this.secretsController.destroy()); + } + await teardownContext(this); + this.testRoot.remove(); + }); + + test('the real Host template hides denied Add Container and preserves an allowed host query', async function(assert) { + // Only unrelated Host widgets and routing are inert. The production Host + // template still supplies the capability conditional and exact LinkTo args. + const Yielding = Component.extend({tagName: '', layout: precompileTemplate('{{yield}}')}); + this.owner.register('component:action-menu', Yielding); + this.owner.register('component:link-to', Component.extend({ + tagName: 'a', + layout: precompileTemplate('{{yield}}'), + attributeBindings: ['route:data-route', 'hostId:data-host-id'], + hostId: alias('query.hostId'), + })); + for (let name of ['power-select', 'select-dot', 'header-state', 'copy-ip', 'info-multi-stats']) { + this.owner.register(`component:${name}`, Component.extend({tagName: 'span'})); + } + this.owner.register('helper:outlet', helper(() => null)); + this.host = EmberObject.create({id: 'synthetic-host', info: {osInfo: {kernelVersion: 'test'}}}); + this.model = {host: this.host, all: A([this.host])}; + this.actions = {changeHost() {}}; + this.intl.setLocale(['zh-tw', 'en-us']); + this.set('canCreateContainer', false); + + await render(HostTemplate); + assert.strictEqual(findAll('.header [data-route="containers.new"]').length, 0, 'denied or unloaded capability has no Add link'); + this.set('canCreateContainer', true); + await settled(); + let add = find('.header [data-route="containers.new"]'); + assert.ok(add, 'the actual template exposes the creator entry'); + assert.strictEqual(add.getAttribute('data-host-id'), 'synthetic-host', 'LinkTo receives the exact selected Host ID'); + assert.strictEqual(add.textContent.trim(), this.intl.t('hostsPage.hostPage.addContainer.linkTo')); + this.set('canCreateContainer', false); + await settled(); + assert.strictEqual(findAll('.header [data-route="containers.new"]').length, 0, 'revoked capability removes an already-rendered entry'); + run(() => this.host.destroy()); + }); + + test('real desktop table headers render the Secrets generic keys and follow locale changes', async function(assert) { + this.secretsController = SecretsIndexController.create(); + this.headers = this.secretsController.get('headers'); + this.rows = A([]); + this.intl.setLocale(['en-us']); + + await render(precompileTemplate('{{sortable-table headers=this.headers body=this.rows sortBy="name" paging=false bulkActions=false search=false}}')); + let labels = () => findAll('thead tr.fixed-header > th').filter(node => node.getAttribute('data-column-role') !== 'actions').map(node => node.textContent.replace(/\s+/g, ' ').trim()); + assert.deepEqual(labels(), ['State', 'Name', 'Description', 'Created'], 'the real table retains English desktop labels'); + this.intl.setLocale(['zh-tw', 'en-us']); + await settled(); + assert.deepEqual(labels(), ['狀態', '名稱', '描述', '建立'], 'the same real table headers react to Traditional Chinese'); + assert.strictEqual(findAll('thead tr.fixed-header > th[data-column-role="actions"]').length, 1, 'the unchanged actions column remains present'); + }); +}); diff --git a/tests/unit/host/controller-test.js b/tests/unit/host/controller-test.js new file mode 100644 index 0000000000..3fcee62117 --- /dev/null +++ b/tests/unit/host/controller-test.js @@ -0,0 +1,80 @@ +import EmberObject from '@ember/object'; +import { module, test } from 'qunit'; +import HostController from 'ui/host/controller'; +import ProjectsService from 'ui/services/projects'; +import { createOwned, destroyOwned } from '../../helpers/owned-subject'; + +module('Unit | Controller | host'); + +function createSubject(assert, schemaProjectId = '1a2540') { + let project = EmberObject.create({id: '1a2540'}); + let schema = EmberObject.create({collectionMethods: ['GET', 'POST']}); + let projects = createOwned(ProjectsService, { + current: project, + schemaProjectId, + store: { + canCreate(type) { + assert.strictEqual(type, 'container', 'checks the container schema, not host permissions'); + return schema.get('collectionMethods').includes('POST'); + }, + }, + }, 'service'); + let controller = createOwned(HostController, {projects}, 'controller'); + + return { + controller, + project, + projects, + schema, + destroy() { + destroyOwned(controller); + destroyOwned(projects); + destroyOwned(project); + destroyOwned(schema); + }, + }; +} + +test('add-container stays hidden until the current project schema is ready', function(assert) { + let subject = createSubject(assert, null); + + assert.false(subject.controller.get('canCreateContainer'), 'schema loading cannot expose the action'); + subject.projects.set('schemaProjectId', '1a2540'); + assert.true(subject.controller.get('canCreateContainer'), 'a matching loaded POST schema exposes the action'); + subject.destroy(); +}); + +test('add-container follows the container POST capability', function(assert) { + let subject = createSubject(assert); + + subject.schema.set('collectionMethods', ['GET']); + assert.false(subject.controller.get('canCreateContainer'), 'a read-only container schema hides the action'); + subject.schema.set('collectionMethods', ['GET', 'POST']); + subject.projects.incrementProperty('schemaLoadGeneration'); + assert.true(subject.controller.get('canCreateContainer'), 'a creator schema exposes the action without role-name checks'); + subject.destroy(); +}); + +test('a project switch cannot reuse a stale container capability', function(assert) { + let subject = createSubject(assert); + + assert.true(subject.controller.get('canCreateContainer'), 'starts with an authorized loaded schema'); + subject.project.set('id', '1a-other'); + assert.false(subject.controller.get('canCreateContainer'), 'the old project schema cannot grant access'); + subject.projects.set('schemaProjectId', '1a-other'); + assert.true(subject.controller.get('canCreateContainer'), 'the new matching schema may grant access'); + subject.destroy(); +}); + +test('same-project schema generations revoke cached container creation', function(assert) { + let subject = createSubject(assert); + + assert.true(subject.controller.get('canCreateContainer'), 'starts with the current POST capability'); + subject.schema.set('collectionMethods', ['GET']); + subject.projects.incrementProperty('schemaLoadGeneration'); + assert.false(subject.controller.get('canCreateContainer'), 'a same-project schema reload revokes the cached action'); + subject.schema.set('collectionMethods', ['GET', 'POST']); + subject.projects.incrementProperty('schemaLoadGeneration'); + assert.true(subject.controller.get('canCreateContainer'), 'a later schema generation can restore the action'); + subject.destroy(); +}); diff --git a/tests/unit/secrets/index/controller-test.js b/tests/unit/secrets/index/controller-test.js new file mode 100644 index 0000000000..d7b263fd28 --- /dev/null +++ b/tests/unit/secrets/index/controller-test.js @@ -0,0 +1,71 @@ +import { run } from '@ember/runloop'; +import { module, test } from 'qunit'; + +import SecretsIndexController from 'ui/secrets/index/controller'; + +module('Unit | Controller | secrets/index | desktop headers'); + +test('desktop headers use existing generic translation keys', function(assert) { + let controller = SecretsIndexController.create(); + let headers = controller.get('headers'); + + assert.deepEqual(headers.filter((header) => !header.isActions).map((header) => header.translationKey), [ + 'generic.state', + 'generic.name', + 'generic.description', + 'generic.created', + ]); + assert.strictEqual(headers[4].translationKey, undefined, 'actions header remains unchanged and visually suppressed'); + + run(() => controller.destroy()); +}); + +test('translation keys preserve all legacy table and QA mapping fields', function(assert) { + let controller = SecretsIndexController.create(); + let legacyHeaders = controller.get('headers').map((header) => { + let legacy = {...header}; + + delete legacy.translationKey; + return legacy; + }); + + assert.deepEqual(legacyHeaders, [ + { + displayName: 'State', + name: 'stateSort', + sort: ['stateSort', 'name', 'id'], + type: 'string', + searchField: 'displayState', + classNames: '', + width: '125px', + }, + { + displayName: 'Name', + name: 'name', + sort: ['name', 'id'], + type: 'string', + }, + { + displayName: 'Description', + name: 'description', + sort: ['description', 'name', 'id'], + type: 'string', + }, + { + displayName: 'Created', + name: 'created', + sort: ['primaryHost.displayName', 'name', 'id'], + searchField: false, + type: 'string', + }, + { + displayName: 'Actions', + isActions: true, + width: '110px', + }, + ], 'only four translationKey properties are added; names, sorting, searching, widths and fallback labels stay intact'); + assert.equal(controller.get('sortBy'), 'name'); + assert.deepEqual(controller.get('queryParams'), ['sortBy']); + + run(() => controller.destroy()); +}); From 46501e31071b3d74595aea91908876eec32b7fd6 Mon Sep 17 00:00:00 2001 From: chen21019 Date: Thu, 1 Oct 2026 11:26:38 +0800 Subject: [PATCH 2/2] Sync current Web162 CI version gates --- scripts/check-ui-console-workspace | 2 +- scripts/check-ui-critical-high-dependencies | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index 40987082d5..ecf01b28f9 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.161 browser-session broker-broadcast + 1.6.162 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index ea03e020a5..9ab62f208c 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.161": +if package.get("version") != "1.6.162": 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')}")