From 2a11503d6e3f56592841d59d69a88d0ddecc4f39 Mon Sep 17 00:00:00 2001 From: "Cheng-Chen, Chen" Date: Mon, 5 Oct 2026 14:24:36 +0800 Subject: [PATCH 1/2] web: handle inactive environment details without forbidden scoped reads --- COMPATIBILITY.md | 12 +++ README.md | 13 +++ app/components/view-edit-project/component.js | 5 +- app/components/view-edit-project/template.hbs | 6 +- app/settings/projects/detail/route.js | 17 +++- app/settings/projects/detail/template.hbs | 1 + ...ass-replacement.node24-ignore-scripts.json | 4 +- docs/releases/web-console-1.6.178.md | 37 +++++++++ package-lock.json | 4 +- package.json | 2 +- scripts/check-modernization-blockers | 4 +- scripts/check-ui-console-workspace | 2 +- scripts/check-ui-critical-high-dependencies | 2 +- .../view-edit-project-permissions-test.js | 62 +++++++++++++- .../view-edit-project-permissions-test.js | 31 +++++++ .../settings/projects/detail/route-test.js | 82 +++++++++++++++++++ translations/en-us.yaml | 1 + translations/zh-tw.yaml | 1 + 18 files changed, 269 insertions(+), 17 deletions(-) create mode 100644 docs/releases/web-console-1.6.178.md diff --git a/COMPATIBILITY.md b/COMPATIBILITY.md index 1b737c7ec7..5ed9b6100c 100644 --- a/COMPATIBILITY.md +++ b/COMPATIBILITY.md @@ -4,6 +4,18 @@ Web Console preserves compatible API paths, schema and resource names, action na Visible branding, product-owned assets, icon identifiers, package metadata, and operator documentation use PastureStack. Historical identifiers remain only where they are server data or protocol contracts and must not be mechanically replaced. +Candidate `1.6.178` handles only the exact `inactive` environment state specially, +after global project and member authorization succeeds. Network and +policy-manager values remain unavailable (`null`), with a state-specific +localized explanation; they are not invented empty resource collections. +Project metadata, membership and removal capabilities remain sourced from the +API. A stale editable network cannot be persisted from that inactive form. +All other states retain the existing scoped reads and error semantics. Backend +authorization, session generation/mutex, MFA, OIDC, workspace lifecycle and +dependencies are unchanged. Formal publication and deployed native acceptance +are pending; historical HOLDs and the incomplete full matrix remain unchanged. +See the [candidate release note](docs/releases/web-console-1.6.178.md). + Published `1.6.177` treats an ended workspace entry as terminal across both logs and terminal components. Late responses and queued socket/timer callbacks are bound to the original entry, never an explicitly opened replacement. Existing diff --git a/README.md b/README.md index de64d5c5d9..f4ba6d654a 100644 --- a/README.md +++ b/README.md @@ -8,6 +8,19 @@ PastureStack is an independent community effort to preserve, audit, and moderniz ## Project status +Candidate `1.6.178` fixes viewing and editing an inactive environment. Once the +globally authorized project and members have loaded, the page does not request +network or policy-manager data scoped to that inactive environment: the API +correctly denies those requests. The existing form explains that network +settings are unavailable until activation, in English and Traditional Chinese. +Metadata and membership controls still follow their original API action links; +cached network data cannot enable a network save. Other states and authorization +failures retain the established error handling. This candidate changes no +backend, authentication, session ownership or dependency graph. Formal +publication and deployed native acceptance remain pending; it does not promote +earlier HOLD results or claim completion of the full matrix. See the +[candidate release note](docs/releases/web-console-1.6.178.md). + Published `1.6.177` prevents ended log and terminal workspace entries from reconnecting after remount/reload or from late access-ticket, broker, socket and timer callbacks. Both components share one lifecycle boundary; asynchronous diff --git a/app/components/view-edit-project/component.js b/app/components/view-edit-project/component.js index 07f50a298a..ed7d3ff9fc 100644 --- a/app/components/view-edit-project/component.js +++ b/app/components/view-edit-project/component.js @@ -20,6 +20,7 @@ export default Component.extend(NewOrEdit, Sortable, { originalProject: null, allProjects: null, policyManager: null, + networkUnavailableForInactiveProject: false, editing: false, tab: 'access', @@ -150,8 +151,8 @@ export default Component.extend(NewOrEdit, Sortable, { }.property('project.id', 'project.actionLinks.setmembers'), canEditNetwork: function() { - return !!this.get('network.actionLinks.update') && !this.get('missingManager') && !this.get('hasUnsupportedPolicy'); - }.property('network.actionLinks.update', 'missingManager', 'hasUnsupportedPolicy'), + return !this.get('networkUnavailableForInactiveProject') && !!this.get('network.actionLinks.update') && !this.get('missingManager') && !this.get('hasUnsupportedPolicy'); + }.property('networkUnavailableForInactiveProject', 'network.actionLinks.update', 'missingManager', 'hasUnsupportedPolicy'), canSave: function() { return this.get('canEditProject') || diff --git a/app/components/view-edit-project/template.hbs b/app/components/view-edit-project/template.hbs index 8a3548107a..4c6c636167 100644 --- a/app/components/view-edit-project/template.hbs +++ b/app/components/view-edit-project/template.hbs @@ -137,7 +137,11 @@ -{{#if this.network}} +{{#if this.networkUnavailableForInactiveProject}} +

+ {{t 'viewEditProject.networkPolicy.inactive'}} +

+{{else if this.network}}

{{t 'viewEditProject.networkPolicy.label'}}


diff --git a/app/settings/projects/detail/route.js b/app/settings/projects/detail/route.js index 332728df15..60a4a8f273 100644 --- a/app/settings/projects/detail/route.js +++ b/app/settings/projects/detail/route.js @@ -50,7 +50,12 @@ export default Route.extend(PromiseToCb, { } ); })], - networks: ['project', this.toCb(() => { + networks: ['importMembers', this.toCb((results) => { + // Inactive environments remain globally visible to their owners, + // but the API correctly rejects requests scoped to them. + if ( results.project.get('state') === 'inactive' ) { + return null; + } return userStore.find('network', null, { filter: {accountId: params.project_id}, headers: {[C.HEADER.PROJECT_ID]: params.project_id}, @@ -58,7 +63,10 @@ export default Route.extend(PromiseToCb, { throw this.environmentLoadError(err, 'viewEditProject.error.relatedUnavailable'); }); })], - policyManagers: ['project', this.toCb(() => { + policyManagers: ['importMembers', this.toCb((results) => { + if ( results.project.get('state') === 'inactive' ) { + return null; + } return userStore.find('stack', null, policyManagerOpt).then(null, (err) => { throw this.environmentLoadError(err, 'viewEditProject.error.relatedUnavailable'); }); @@ -75,7 +83,7 @@ export default Route.extend(PromiseToCb, { }, 'Load all the things'); return promise.then((hash) => { - let network = hash.networks.find((x) => C.PROJECT.SUPPORTS_NETWORK_POLICY.includes(x.get('name'))); + let network = hash.networks ? hash.networks.find((x) => C.PROJECT.SUPPORTS_NETWORK_POLICY.includes(x.get('name'))) : null; if ( network ) { network = network.clone(); @@ -106,7 +114,8 @@ export default Route.extend(PromiseToCb, { let out = EmberObject.create({ all: hash.allProjects, network: network, - policyManager: hash.policyManagers.objectAt(0), + policyManager: hash.policyManagers ? hash.policyManagers.objectAt(0) : null, + networkUnavailableForInactiveProject: hash.project.get('state') === 'inactive', }); if ( params.editing ) { diff --git a/app/settings/projects/detail/template.hbs b/app/settings/projects/detail/template.hbs index d2cb7f2848..4d63521636 100644 --- a/app/settings/projects/detail/template.hbs +++ b/app/settings/projects/detail/template.hbs @@ -6,6 +6,7 @@ initialStacks=this.model.stacks serviceChoices=this.model.serviceChoices policyManager=this.model.policyManager + networkUnavailableForInactiveProject=this.model.networkUnavailableForInactiveProject showEdit=this.editing editing=true tab=this.tab 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 82eb97fd86..5c2278190e 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.177", + "version": "1.6.178", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.177", + "version": "1.6.178", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/docs/releases/web-console-1.6.178.md b/docs/releases/web-console-1.6.178.md new file mode 100644 index 0000000000..299284ee50 --- /dev/null +++ b/docs/releases/web-console-1.6.178.md @@ -0,0 +1,37 @@ +# Web Console 1.6.178 — inactive environment details + +Status: source candidate; formal publication and deployed native acceptance +are pending. No earlier HOLD is promoted to PASS. + +## Cause and correction + +An inactive environment remains globally visible to an authorized owner, but +the engine rejects API requests scoped to that inactive environment. The +details route unconditionally requested networks and policy-manager stacks; +their 403 responses prevented its model from committing and obscured otherwise +authorized metadata and membership information. + +The route now waits for globally authorized project and membership reads, +then skips only those two inapplicable scoped reads when the project state is +exactly `inactive`. It returns `null` for the unavailable data and a dedicated +reason flag. The existing details component displays an English or Traditional +Chinese explanation and prevents a cached network from enabling network saves. +Metadata, member and removal action links are unchanged. Active and transitional +states, denied global data, expired sessions and other failures keep the +existing authorization and error behavior. + +## Change boundary and verification + +Changes are confined to the details route/template, shared details component, +two translations and their three focused test modules. Release-version metadata +and the matching reviewed lock baseline move together; dependencies do not +change. No API, engine, provider, HAProxy, authentication or session contract is +modified. The existing ended log/terminal and cross-tab session tests are +retained. + +Added regressions cover inactive view/edit, globally denied project/member +reads, non-inactive scoped denial, the actual details-template reason flag and +stale-network write prevention. Formal exact-source tests, reproducible static +archives, immutable publication and isolated native view/edit/reload/removal +acceptance must be recorded separately. Full role/resource/locale acceptance +remains incomplete. diff --git a/package-lock.json b/package-lock.json index 82eb97fd86..5c2278190e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "@pasturestack/web-console", - "version": "1.6.177", + "version": "1.6.178", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "@pasturestack/web-console", - "version": "1.6.177", + "version": "1.6.178", "license": "Apache-2.0", "dependencies": { "sass": "1.103.1" diff --git a/package.json b/package.json index fa0af56534..648adb0516 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@pasturestack/web-console", - "version": "1.6.177", + "version": "1.6.178", "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 c7e84ecc97..69984e687a 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.177" ]]; then - echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.177" +if [[ "$version" != "1.6.178" ]]; then + echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.178" failures=$((failures + 1)) fi diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace index 8aca861389..c5eb40c6e2 100755 --- a/scripts/check-ui-console-workspace +++ b/scripts/check-ui-console-workspace @@ -143,4 +143,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.177 browser-session broker-broadcast + 1.6.178 browser-session broker-broadcast diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies index 2d6af6ee44..3caafd7317 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.177": +if package.get("version") != "1.6.178": 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 e1d4ac0839..ecdcfa5d9a 100644 --- a/tests/integration/components/view-edit-project-permissions-test.js +++ b/tests/integration/components/view-edit-project-permissions-test.js @@ -1,14 +1,17 @@ import { A } from '@ember/array'; import Component from '@ember/component'; +import EmberRouter from '@ember/routing/router'; import EmberObject, { get } from '@ember/object'; -import Service from '@ember/service'; +import Service, { service } from '@ember/service'; import { precompileTemplate } from '@ember/template-compilation'; import { click, find, findAll, render, select, settled, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers'; import { module, test } from 'qunit'; import { initialize as initializePodLayouts } from 'ui/initializers/pod-component-layouts'; +import ViewEditProject from 'ui/components/view-edit-project/component'; import ProjectTemplate from 'ui/models/projecttemplate'; import Router from 'ui/router'; +import ProjectDetailTemplate from 'ui/settings/projects/detail/template'; import { destroyOwned } from '../../helpers/owned-subject'; import resolver from '../../helpers/resolver'; @@ -218,6 +221,63 @@ module('Integration | Component | view edit project permissions', function(hooks assert.strictEqual(findAll('.footer-actions button').length, 2); }); + for (let editing of [false, true]) { + test(`inactive detail ${editing ? 'edit' : 'view'} shows its state reason without scoped controls`, async function(assert) { + // Use an isolated route map; the application's extension registry is + // intentionally consumed only once by its native Router. + let DetailRouter = EmberRouter.extend({location: 'none'}); + DetailRouter.map(function() { + this.route('settings', function() { + this.route('projects', {path: '/env'}, function() { + this.route('detail', {path: '/:project_id'}); + }); + }); + }); + this.owner.register('router:main', DetailRouter); + this.owner.lookup('router:main').setupRouter(); + this.owner.register('component:action-menu', Component.extend({ + layout: precompileTemplate('{{this.model.actionLinks.remove}}'), + })); + this.owner.register('component:header-state', Component.extend({ + layout: precompileTemplate('{{this.model.state}}'), + })); + this.owner.register('component:power-select', Component.extend({ + layout: precompileTemplate(''), + })); + let actionLinks = {update: '/projects/1a21', setmembers: '/projects/1a21?action=setmembers', remove: '/projects/1a21'}; + this.project.setProperties({state: 'inactive', actionLinks}); + this.network.set('actionLinks', {update: '/networks/1n1'}); + this.model = EmberObject.create({ + project: this.project, originalProject: this.originalProject, all: A([this.project]), + network: this.network, policyManager: this.policyManager, networkUnavailableForInactiveProject: true, + }); + this.editing = editing; + this.done = () => {}; + this.cancel = () => {}; + this.owner.register('service:user-store', Service.extend(this.userStore)); + // Rendering owners do not run the application's store initializer. + // Supply its real service injection without replacing component logic. + this.owner.register('component:view-edit-project', ViewEditProject.extend({userStore: service('user-store')})); + // Render the actual detail template so its reason flag forwarding is tested. + await render(ProjectDetailTemplate); + + assert.ok(find('[data-test-inactive-network-notice]').textContent.includes('viewEditProject.networkPolicy.inactive'), 'a state-specific translated explanation is visible'); + assert.ok(find('[data-test-member-name]'), 'global membership data stays visible'); + assert.strictEqual(findAll('.radio input').length, 0, 'cached network data cannot expose policy controls'); + assert.notOk(find('[data-test-network-only-edit]'), 'no scoped network edit link is introduced'); + assert.strictEqual(this.project.get('actionLinks'), actionLinks, 'existing global capabilities are untouched'); + if ( editing ) { + assert.false(find('input[type="text"]').disabled, 'global metadata capability still enables its fields'); + assert.ok(find('[data-test-member-add]'), 'global member capability remains available'); + assert.strictEqual(findAll('table.grid select').length, 1, 'member roles remain editable'); + assert.strictEqual(findAll('.footer-actions button').length, 2, 'global save and cancel remain available'); + } else { + assert.strictEqual(find('[data-test-header-state]').textContent.trim(), 'inactive', 'the environment state remains visible'); + assert.strictEqual(find('[data-test-action-menu]').textContent.trim(), actionLinks.remove, 'the existing action menu receives the original global remove link'); + } + }); + } + test('network-only environment can reach its edit form from the detail header', async function(assert) { this.owner.register('router:main', Router); this.owner.register('component:action-menu', Component.extend({ diff --git a/tests/unit/components/view-edit-project-permissions-test.js b/tests/unit/components/view-edit-project-permissions-test.js index ce77e9afab..e4a2aee261 100644 --- a/tests/unit/components/view-edit-project-permissions-test.js +++ b/tests/unit/components/view-edit-project-permissions-test.js @@ -163,6 +163,37 @@ test('member actions cannot mutate a readonly membership list', function(assert) destroyOwned(component); }); +test('inactive network unavailability preserves global capabilities and prevents stale network saves', async function(assert) { + for (let canEdit of [false, true]) { + let writes = {project: 0, members: 0, network: 0}; + let actionLinks = canEdit ? {update: '/projects/1a21', setmembers: '/projects/1a21?action=setmembers', remove: '/projects/1a21'} : {}; + let project = EmberObject.create({ + id: '1a21', state: 'inactive', actionLinks, + projectMembers: A([EmberObject.create({externalIdType: 'oidc_user', externalId: 'owner-1', role: 'owner'})]), + validationErrors() { return A([]); }, + save() { writes.project++; return resolve(this); }, + doAction(action) { assert.strictEqual(action, 'setmembers'); writes.members++; return resolve(); }, + }); + let network = EmberObject.create({ + actionLinks: {update: '/networks/1n1'}, policy: A([]), + save() { writes.network++; return resolve(this); }, + }); + let component = makeComponent(project, network, {networkUnavailableForInactiveProject: true}); + + try { + assert.strictEqual(component.get('canEditProject'), canEdit, 'metadata still follows the global project link'); + assert.strictEqual(component.get('canEditMembers'), canEdit, 'members still follow the global setmembers link'); + assert.false(component.get('canEditNetwork'), 'even a stale editable network cannot enable scoped persistence'); + assert.strictEqual(component.get('canSave'), canEdit, 'the notice does not grant a save capability'); + await component.get('actions').save.call(component); + assert.deepEqual(writes, {project: Number(canEdit), members: Number(canEdit), network: 0}, 'only the advertised global operations are saved'); + assert.strictEqual(project.get('actionLinks'), actionLinks, 'remove and other action links are not rewritten'); + } finally { + destroyOwned(component); + } + } +}); + test('member validation errors use translated messages', function(assert) { let owner = EmberObject.create({externalIdType: 'oidc_user', externalId: 'owner-1', role: 'owner'}); let project = EmberObject.create({ diff --git a/tests/unit/settings/projects/detail/route-test.js b/tests/unit/settings/projects/detail/route-test.js index 133b943302..fed8132abb 100644 --- a/tests/unit/settings/projects/detail/route-test.js +++ b/tests/unit/settings/projects/detail/route-test.js @@ -13,6 +13,7 @@ function fixture(failureAt, failure = new Error(`${failureAt} failed`)) { let project = EmberObject.create({ id: '1a21', name: 'QA project', + state: 'active', projectMembers: null, followLink(name) { if ( failureAt === 'membersSync' ) { @@ -30,6 +31,8 @@ function fixture(failureAt, failure = new Error(`${failureAt} failed`)) { return EmberObject.create({ id: this.get('id'), name: this.get('name'), + state: this.get('state'), + actionLinks: this.get('actionLinks'), projectMembers: this.get('projectMembers'), }); }, @@ -66,6 +69,83 @@ function fixture(failureAt, failure = new Error(`${failureAt} failed`)) { return {failure, members, policyManager, project, store, relatedCalls}; } +for (let editing of [false, true]) { + test(`inactive environment ${editing ? 'edit' : 'view'} preserves global data without scoped resource reads`, async function(assert) { + let data = fixture(); + let actionLinks = {update: '/projects/1a21', setmembers: '/projects/1a21?action=setmembers', remove: '/projects/1a21'}; + data.project.setProperties({state: 'inactive', actionLinks}); + let route = ProjectDetailRoute.create({userStore: data.store}); + + try { + let model = await route.model({project_id: '1a21', editing}); + assert.deepEqual(data.relatedCalls, [], 'neither network nor policy-manager API is requested'); + assert.strictEqual(model.get('network'), null, 'network data is unavailable, not an empty collection'); + assert.strictEqual(model.get('policyManager'), null, 'no policy manager is invented'); + assert.true(model.get('networkUnavailableForInactiveProject'), 'the model exposes the exact state reason'); + assert.strictEqual(model.get('all').objectAt(0), data.project, 'the global project list is retained'); + assert.strictEqual(model.get('project.projectMembers'), data.members, 'authorized global memberships are retained'); + assert.strictEqual(model.get('project.actionLinks'), actionLinks, 'the API remains the source of metadata, member, and remove capabilities'); + assert.strictEqual(model.get('originalProject'), editing ? data.project : null); + assert.strictEqual(model.get('project') === data.project, !editing, 'only the edit form clones the project'); + } finally { + run(() => route.destroy()); + } + }); +} + +test('inactive global project and member failures remain errors before any scoped reads', async function(assert) { + let intl = EmberObject.create({t(key) { return key; }}); + for (let task of ['allProjects', 'project', 'members']) { + for (let status of [401, 403, 404, 503]) { + let failure = {status, message: 'Raw API message'}; + let data = fixture(task, failure); + data.project.set('state', 'inactive'); + let route = ProjectDetailRoute.create({userStore: data.store, intl}); + + try { + await route.model({project_id: '1a21', editing: false}).then( + () => assert.ok(false, `${task} ${status} must reject`), + (error) => { + assert.strictEqual(error.status, status === 403 || status === 404 ? 404 : status, + `${task} ${status} keeps the established error classification`); + if ( status === 401 ) { + assert.strictEqual(error, failure, 'expired-session handling is unchanged'); + } + } + ); + assert.deepEqual(data.relatedCalls, [], `${task} ${status} cannot start scoped reads`); + } finally { + run(() => route.destroy()); + } + } + } +}); + +test('only exact inactive skips scoped reads; other states still reject related 403s', async function(assert) { + let intl = EmberObject.create({t(key) { return key; }}); + for (let state of ['active', 'deactivating', 'activating', 'removed', 'upgrading', 'updating-active', 'unknown', null, undefined]) { + for (let task of ['networks', 'policyManagers']) { + let data = fixture(task, {status: 403, message: 'Forbidden'}); + data.project.set('state', state); + let route = ProjectDetailRoute.create({userStore: data.store, intl}); + + try { + await route.model({project_id: '1a21', editing: false}).then( + () => assert.ok(false, `${state} ${task} 403 must reject`), + (error) => { + assert.strictEqual(error.status, 404, 'the existing denied-resource error is preserved'); + assert.strictEqual(error.messageKey, 'viewEditProject.error.relatedUnavailable'); + } + ); + assert.ok(data.relatedCalls.includes(task === 'networks' ? 'network' : 'stack'), 'the applicable request is still sent'); + assert.strictEqual(data.project.get('projectMembers'), data.members, 'global members were loaded first'); + } finally { + run(() => route.destroy()); + } + } + } +}); + test('a denied project cannot start unrelated network or policy-manager reads', async function(assert) { let data = fixture('project', {status: 404, message: 'Environment unavailable'}); let route = ProjectDetailRoute.create({userStore: data.store, intl: EmberObject.create({ @@ -89,6 +169,8 @@ test('loads project members through the supported link contract before cloning f assert.notStrictEqual(model.get('project'), data.project, 'editing uses a clone'); assert.strictEqual(model.get('project.projectMembers'), data.members, 'the imported members reach the editable clone'); assert.strictEqual(model.get('policyManager'), data.policyManager, 'the policy manager is loaded'); + assert.false(model.get('networkUnavailableForInactiveProject'), 'an active environment has no inactive notice'); + assert.deepEqual(data.relatedCalls, ['network', 'stack'], 'both active-environment reads still run'); assert.strictEqual(data.store.get('networkOptions.filter.accountId'), '1a21', 'the network lookup stays in the selected project'); assert.strictEqual(data.store.get('networkOptions.headers.X-Api-Project-Id'), '1a21', 'the network lookup uses its project policy'); assert.strictEqual(data.store.get('policyManagerOptions.headers.X-Api-Project-Id'), '1a21', 'policy manager lookup is scoped to the project'); diff --git a/translations/en-us.yaml b/translations/en-us.yaml index 0302b7e84e..f5ef42be6b 100644 --- a/translations/en-us.yaml +++ b/translations/en-us.yaml @@ -4213,6 +4213,7 @@ viewEditProject: noMembers: Add one or more members who can use this environment. networkPolicy: label: Network Policy + inactive: This environment is deactivated; network settings are temporarily unavailable. Activate the environment to view or edit them. description: Control what containers are allowed to communicate with each other over the Managed Network. unsupported: This environment has network policy rules which are not yet supported by the UI. Please use the API to manage the policy. noManager: Network Policies are available only for environments using the PastureStack orchestration engine with the Network Policy Manager infrastructure template deployed. diff --git a/translations/zh-tw.yaml b/translations/zh-tw.yaml index 0666aedc19..bdd975ab5d 100644 --- a/translations/zh-tw.yaml +++ b/translations/zh-tw.yaml @@ -4020,6 +4020,7 @@ viewEditProject: noMembers: 新增一個或多個成員能夠使用此環境的成員 networkPolicy: label: 網路策略 + inactive: 環境已停用;網路設定暫不可用。啟用後可檢視/修改。 description: 控制受管網路中哪些容器彼此之間能夠進行通訊 unsupported: 此環境中的網路策略設定暫不支援透過 UI 進行設定,請使用 API 管理網路策略。 noManager: 網路策略功能僅適用於使用 PastureStack 容器編排服務,且已部署網路策略管理基礎架構範本的環境。 From 60a494e943150ecd300d1aa653ee397f590b575b Mon Sep 17 00:00:00 2001 From: "Cheng-Chen, Chen" Date: Mon, 5 Oct 2026 14:35:37 +0800 Subject: [PATCH 2/2] web: complete inactive environment explanation in all packaged locales --- README.md | 2 +- docs/releases/web-console-1.6.178.md | 6 +++--- translations/de-de.yaml | 1 + translations/fa-ir.yaml | 1 + translations/fil-ph.yaml | 1 + translations/fr-fr.yaml | 1 + translations/hu-hu.yaml | 1 + translations/ja-jp.yaml | 1 + translations/ko-kr.yaml | 1 + translations/pt-br.yaml | 1 + translations/ru-ru.yaml | 1 + translations/uk-ua.yaml | 1 + translations/zh-hans.yaml | 1 + 13 files changed, 15 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index f4ba6d654a..251c99fc88 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ Candidate `1.6.178` fixes viewing and editing an inactive environment. Once the globally authorized project and members have loaded, the page does not request network or policy-manager data scoped to that inactive environment: the API correctly denies those requests. The existing form explains that network -settings are unavailable until activation, in English and Traditional Chinese. +settings are unavailable until activation, in all thirteen packaged locales. Metadata and membership controls still follow their original API action links; cached network data cannot enable a network save. Other states and authorization failures retain the established error handling. This candidate changes no diff --git a/docs/releases/web-console-1.6.178.md b/docs/releases/web-console-1.6.178.md index 299284ee50..4d7fffbee9 100644 --- a/docs/releases/web-console-1.6.178.md +++ b/docs/releases/web-console-1.6.178.md @@ -14,8 +14,8 @@ authorized metadata and membership information. The route now waits for globally authorized project and membership reads, then skips only those two inapplicable scoped reads when the project state is exactly `inactive`. It returns `null` for the unavailable data and a dedicated -reason flag. The existing details component displays an English or Traditional -Chinese explanation and prevents a cached network from enabling network saves. +reason flag. The existing details component displays a translated explanation +in all thirteen packaged locales and prevents a cached network from enabling network saves. Metadata, member and removal action links are unchanged. Active and transitional states, denied global data, expired sessions and other failures keep the existing authorization and error behavior. @@ -23,7 +23,7 @@ existing authorization and error behavior. ## Change boundary and verification Changes are confined to the details route/template, shared details component, -two translations and their three focused test modules. Release-version metadata +thirteen translations and their three focused test modules. Release-version metadata and the matching reviewed lock baseline move together; dependencies do not change. No API, engine, provider, HAProxy, authentication or session contract is modified. The existing ended log/terminal and cross-tab session tests are diff --git a/translations/de-de.yaml b/translations/de-de.yaml index b13b473c46..11284ed9ee 100644 --- a/translations/de-de.yaml +++ b/translations/de-de.yaml @@ -3840,6 +3840,7 @@ viewEditProject: noMembers: 'Fügen Sie ein oder mehrere Mitglieder hinzu, die diese Umgebung nutzen können.' networkPolicy: label: Netzwerkrichtlinie + inactive: Diese Umgebung ist deaktiviert; die Netzwerkeinstellungen sind vorübergehend nicht verfügbar. Aktivieren Sie die Umgebung, um sie anzuzeigen oder zu bearbeiten. description: 'Steuern Sie, welche Container über das verwaltete Netzwerk miteinander kommunizieren dürfen.' unsupported: >- Diese Umgebung verfügt über Netzwerkrichtlinienregeln, die von UI noch nicht unterstützt werden. Bitte verwenden diff --git a/translations/fa-ir.yaml b/translations/fa-ir.yaml index 7649462fd2..c8092def89 100644 --- a/translations/fa-ir.yaml +++ b/translations/fa-ir.yaml @@ -3729,6 +3729,7 @@ viewEditProject: noMembers: افزودن اعضایی که می توانند از این محیط استفاده کنند. networkPolicy: label: خط مشی شبکه + inactive: این محیط غیرفعال است؛ تنظیمات شبکه موقتاً در دسترس نیستند. برای مشاهده یا ویرایش آن‌ها، محیط را فعال کنید. description: کنترل کنید که چه کانتینرهایی مجاز به برقراری ارتباط با یکدیگر از طریق شبکه مدیریت شده هستند. unsupported: >- این محیط دارای قوانین خط مشی شبکه است که هنوز توسط UI پشتیبانی نمی شود. لطفاً از API برای مدیریت خط مشی استفاده diff --git a/translations/fil-ph.yaml b/translations/fil-ph.yaml index 81daf2abf4..d08aace7d6 100644 --- a/translations/fil-ph.yaml +++ b/translations/fil-ph.yaml @@ -3786,6 +3786,7 @@ viewEditProject: noMembers: Magdagdag ng isa o higit pang miyembro na maaaring gumamit ng environment na ito. networkPolicy: label: Patakaran sa Network + inactive: Naka-deactivate ang environment na ito; pansamantalang hindi available ang mga setting ng network. I-activate ang environment upang makita o i-edit ang mga ito. description: Kontrolin kung anong mga container ang pinapayagang makipag-ugnayan sa isa't isa sa Pamamahala ng Network. unsupported: >- Ang kapaligirang ito ay may mga panuntunan sa patakaran sa network na hindi pa sinusuportahan ng UI. Pakigamit diff --git a/translations/fr-fr.yaml b/translations/fr-fr.yaml index 4d0145d346..623e9a6e8e 100644 --- a/translations/fr-fr.yaml +++ b/translations/fr-fr.yaml @@ -3785,6 +3785,7 @@ viewEditProject: noMembers: Ajouter un ou plusieurs membres qui peuvent utiliser cet environnement. networkPolicy: label: Politique de réseau + inactive: Cet environnement est désactivé ; les paramètres réseau sont temporairement indisponibles. Activez l’environnement pour les consulter ou les modifier. description: Contrôlez quels conteneurs sont autorisés à communiquer entre eux sur le réseau géré. unsupported: >- Cet environnement possède des règles de politique réseau qui ne sont pas encore prises en charge par UI. Veuillez diff --git a/translations/hu-hu.yaml b/translations/hu-hu.yaml index 0eaebccfea..f027a8aff1 100644 --- a/translations/hu-hu.yaml +++ b/translations/hu-hu.yaml @@ -3803,6 +3803,7 @@ viewEditProject: noMembers: Egy vagy több tag hozzáadása ehhez a környezethez. networkPolicy: label: Hálózati házirend + inactive: Ez a környezet inaktív; a hálózati beállítások átmenetileg nem érhetők el. A megtekintésükhöz vagy módosításukhoz aktiválja a környezetet. description: 'Szabályozhatod, hogy mely konténerek kommunikálhatnak egymással a kezelt hálózaton keresztül.' unsupported: >- Ez a környezet hálózati házirend-szabályokkal rendelkezik, amelyeket a UI még nem támogat. Kérjük, használja a diff --git a/translations/ja-jp.yaml b/translations/ja-jp.yaml index c3249d29c2..b354e8d0ed 100644 --- a/translations/ja-jp.yaml +++ b/translations/ja-jp.yaml @@ -3991,6 +3991,7 @@ viewEditProject: noMembers: この環境を利用することができる 1 つまたは複数のメンバーを追加します。 networkPolicy: label: ネットワークポリシー + inactive: この環境は無効です。ネットワーク設定は一時的に利用できません。表示または編集するには環境を有効にしてください。 description: 管理ネットワーク経由での相互通信を許可するコンテナをコントロールします。 unsupported: この環境には UI ではサポートされていない ネットワークポリシー ルール が存在します。ポリシーを管理するには API を利用してください。 noManager: ネットワークポリシーは Network Policy Manager テンプレートがデプロイされており PastureStack オーケストレーションエンジンを利用している環境下でのみ利用できます。 diff --git a/translations/ko-kr.yaml b/translations/ko-kr.yaml index 999b2efd3d..4f4b85f773 100644 --- a/translations/ko-kr.yaml +++ b/translations/ko-kr.yaml @@ -3643,6 +3643,7 @@ viewEditProject: noMembers: 이 환경을 사용할 수 있는 구성원을 한 명 이상 추가하세요. networkPolicy: label: 네트워크 정책 + inactive: 이 환경은 비활성 상태이며 네트워크 설정을 일시적으로 사용할 수 없습니다. 설정을 보거나 수정하려면 환경을 활성화하세요. description: 관리형 네트워크를 통해 서로 통신할 수 있는 컨테이너를 제어합니다. unsupported: 이 환경에는 UI에서 아직 지원되지 않는 네트워크 정책 규칙이 있습니다. 정책을 관리하려면 API를 이용하세요. noManager: 네트워크 정책은 네트워크 정책 관리자 인프라 템플릿이 배포된 PastureStack 오케스트레이션 엔진을 사용하는 환경에서만 사용할 수 있습니다. diff --git a/translations/pt-br.yaml b/translations/pt-br.yaml index c8f97e87db..0edfbf4bb4 100644 --- a/translations/pt-br.yaml +++ b/translations/pt-br.yaml @@ -3789,6 +3789,7 @@ viewEditProject: noMembers: Adicione um ou mais membros que possam usar este ambiente. networkPolicy: label: Política de Rede + inactive: Este ambiente está desativado; as configurações de rede estão temporariamente indisponíveis. Ative o ambiente para visualizá-las ou editá-las. description: Controle quais contêineres podem se comunicar entre si pela Rede Gerenciada. unsupported: >- Este ambiente possui regras de política de rede que ainda não são suportadas pelo UI. Use o API para gerenciar a diff --git a/translations/ru-ru.yaml b/translations/ru-ru.yaml index 14d5d81aa1..6656cba54c 100644 --- a/translations/ru-ru.yaml +++ b/translations/ru-ru.yaml @@ -3811,6 +3811,7 @@ viewEditProject: noMembers: 'Добавьте одиного или несколько участников, которые могут использовать эту среду.' networkPolicy: label: Сетевая политика + inactive: Эта среда отключена; настройки сети временно недоступны. Активируйте среду, чтобы просмотреть или изменить их. description: 'Контролируйте, каким контейнерам разрешено взаимодействовать друг с другом через управляемую сеть.' unsupported: >- В этой среде есть правила сетевой политики, которые еще не поддерживаются UI. Пожалуйста, используйте API для diff --git a/translations/uk-ua.yaml b/translations/uk-ua.yaml index cad66e5dfe..3d6dc9c7c3 100644 --- a/translations/uk-ua.yaml +++ b/translations/uk-ua.yaml @@ -3848,6 +3848,7 @@ viewEditProject: noMembers: 'Додайте одного або декілька учасників, які можуть використовувати це середовище.' networkPolicy: label: Політика мережі + inactive: Це середовище вимкнено; налаштування мережі тимчасово недоступні. Активуйте середовище, щоб переглянути або змінити їх. description: 'Ви можете контролювати, контейнери які можуть зв''язуватися один з одним через керовану мережу.' unsupported: >- Це середовище має правила мережевої політики, які ще не підтримуються UI. Для керування політикою використовуйте diff --git a/translations/zh-hans.yaml b/translations/zh-hans.yaml index 99b8f64242..70d077f736 100644 --- a/translations/zh-hans.yaml +++ b/translations/zh-hans.yaml @@ -3686,6 +3686,7 @@ viewEditProject: noMembers: 添加一个或者多个成员能够使用此环境的成员 networkPolicy: label: 网络策略 + inactive: 此环境已停用;网络设置暂不可用。启用环境后可查看或修改。 description: 控制受管网络中哪些容器彼此之间能够进行通讯 unsupported: 此环境中的网络策略设置暂不支持通过UI进行设置,请使用API管理网络策略。 noManager: 网络策略功能仅在使用 PastureStack 编排引擎且部署了Network Policy Manager基础设施模板的环境中可用。