Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions COMPATIBILITY.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,16 @@ 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.

The `1.6.163` source candidate localizes state badges only from seven recognized
`model.displayState` labels using existing translation keys. Unknown labels and
health/connection display overrides retain their original text; a machine state
must not replace the model's display semantics. State, icons, colors, API payloads
and authorization remain unchanged. Relative dates recompute on locale changes
and use the existing language-to-Moment mapping on each instance, without
mutating the global Moment locale during formatting. Focused local source tests
passed 12/12; publication, packaged acceptance and full-language acceptance are
not claimed.

Web Console `1.6.161` preserves an existing Certificate's masked key when only
name/description are changed. Explicit update-validation options apply only to
persisted records and explicitly omitted fields; other required and supplied
Expand Down
17 changes: 15 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,19 @@ PastureStack is an independent community effort to preserve, audit, and moderniz

## Project status

Release `1.6.163` is being prepared for two shared display-locale fixes.
Relative dates recompute when the selected language changes and use a Moment
instance locale, without changing the global locale during formatting. State
badges translate seven known display labels through existing translations;
unknown labels and model health/connection overrides such as Disconnected
remain unchanged. Icons, colors, model state and API behavior are preserved.
Focused native Chrome 153 source validation passed 12/12 tests: seven rendering
checks, three relative-date/helper checks and two existing user-language checks.
Official validation, deterministic candidate packaging, publication and packaged
browser acceptance are still pending. This is not full-language or resource/role
matrix acceptance; earlier HOLD evidence remains HOLD. See the
[preparation note](docs/releases/web-console-1.6.163.md).

Published release `1.6.162` contains 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
Expand All @@ -18,8 +31,8 @@ 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), plus two desktop rendering checks.
Official CI passed 746/746 tests and produced two byte-identical archives.
Current Server packaging remains `v1.6.495` / Web Console `1.6.161`; packaged
browser acceptance of `1.6.162` is still pending. Responsive/mobile Secrets
At component publication, Server packaging remained `v1.6.495` / Web Console
`1.6.161`; packaged browser acceptance of `1.6.162` was 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
[release note](docs/releases/web-console-1.6.162.md).
Expand Down
16 changes: 16 additions & 0 deletions app/components/badge-state/component.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,23 @@
import Component from '@ember/component';
import { computed } from '@ember/object';
import { service } from '@ember/service';

const TRANSLATED_STATES = ['active', 'running', 'stopped', 'stopping', 'created', 'exited', 'error'];

export default Component.extend({
intl: service(),
tagName: 'SPAN',
classNames: ['state', 'badge'],
classNameBindings: ['model.stateColor', 'model.stateBackground'],

displayState: computed('model.displayState', 'intl._locale', function() {
let original = this.get('model.displayState');
let state = String(original || '').toLowerCase();

if ( TRANSLATED_STATES.includes(state) ) {
return this.get('intl').t(`formPorts.preflight.state.${state}`);
}

return original;
}),
});
2 changes: 1 addition & 1 deletion app/components/badge-state/template.hbs
Original file line number Diff line number Diff line change
@@ -1 +1 @@
<i class="{{this.model.stateIcon}}"></i> {{this.model.displayState}}
<i class="{{this.model.stateIcon}}"></i> {{this.displayState}}
27 changes: 23 additions & 4 deletions app/helpers/date-from-now.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,26 @@
import { helper as buildHelper } from '@ember/component/helper';
import Helper from '@ember/component/helper';
import { observer } from '@ember/object';
import { service } from '@ember/service';
import { momentLocale } from 'ui/services/user-language';

export function dateFromNow(params) {
return moment(params[0]).fromNow();
export function dateFromNow(params, locale) {
let date = moment(params[0]);

if ( locale ) {
date.locale(momentLocale(locale));
}

return date.fromNow();
}

export default buildHelper(dateFromNow);
export default Helper.extend({
intl: service(),

localeChanged: observer('intl._locale', function() {
this.recompute();
}),

compute(params) {
return dateFromNow(params, this.get('intl.primaryLocale'));
},
});
9 changes: 7 additions & 2 deletions app/services/user-language.js
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,12 @@ const MOMENT_LOCALES = {
'zh-tw': 'zh-tw',
};

export function momentLocale(lang) {
const normalized = (lang || C.LANGUAGE.DEFAULT).toLowerCase();

return MOMENT_LOCALES[normalized] || 'en';
}

export default Service.extend({
prefs : service(),
session : service(),
Expand Down Expand Up @@ -101,8 +107,7 @@ export default Service.extend({
},

setMomentLocale(lang) {
const normalized = this.normalizeLang(lang || C.LANGUAGE.DEFAULT);
moment.locale(MOMENT_LOCALES[normalized] || 'en');
moment.locale(momentLocale(lang));
},

setDocumentLanguage(lang) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.162",
"version": "1.6.163",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
"version": "1.6.162",
"version": "1.6.163",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
Expand Down
47 changes: 47 additions & 0 deletions docs/releases/web-console-1.6.163.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Web Console 1.6.163

Source preparation only. Official validation, deterministic candidate packaging,
public release and packaged browser acceptance are pending. No `1.6.163` release
asset or Server deployment is claimed.

## Narrow shared display-locale fixes

`date-from-now` recomputes when the selected Intl locale changes, even if the
timestamp is unchanged. Each Moment instance uses the existing user-language
locale mapping; formatting does not mutate the global Moment locale. The named
helper function retains its previous global-locale behavior when no locale is
supplied.

`badge-state` translates the seven recognized display labels Active, Running,
Stopped, Stopping, Created, Exited and Error through existing
`formPorts.preflight.state` translations. It uses `model.displayState`, not the
machine state: an active resource displaying Disconnected must remain
Disconnected. Unknown display labels retain their original text. No translation
keys, model state, icon/color bindings, sorting, filtering or API contracts change.

## Focused source evidence and pending acceptance

Native Chrome 153 local source validation passed 12/12 tests, with zero failures,
skips or todos:

- Seven real DOM rendering tests use the actual Intl service and built
translation JSON. They cover unchanged-timestamp `en-us` to `zh-tw` to `en-us`
switching, instance-only Moment locale, the existing Simplified Chinese locale
mapping, all seven known display labels, unknown fallback, label changes,
retained icons/colors and health/connection override semantics.
- Three helper tests preserve the existing locale mapping, explicit instance
formatting, unknown-language English fallback and the named helper contract.
- Two existing user-language tests retain document language/direction and
English fallback loading behavior.

These local tests used an existing dependency installation. They are not a fresh
locked CI run or packaged acceptance. The subsequent version/documentation-only
preparation does not change the tested locale semantics. The normal
`validate.yml` workflow dispatch remains the required formal source validation:
it runs `scripts/ci`, builds twice and compares the deterministic candidate
archives. The disabled historical `scripts/build-static` path is not used.

No full-language, responsive/mobile layout, backend-write, authentication, MFA,
database, runtime-host or VM acceptance is claimed. Earlier HOLD evidence remains
HOLD, and the broader resource/role matrix remains INCOMPLETE. No Server packaging,
production or deployment changes are included.
4 changes: 2 additions & 2 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/web-console",
"version": "1.6.162",
"version": "1.6.163",
"private": true,
"description": "PastureStack browser console for the compatible control platform.",
"repository": {
Expand Down
4 changes: 2 additions & 2 deletions scripts/check-modernization-blockers
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ with open('package.json', encoding='utf-8') as f:
print(json.load(f).get('version', ''))
PY
)
if [[ "$version" != "1.6.162" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.162"
if [[ "$version" != "1.6.163" ]]; then
echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.163"
failures=$((failures + 1))
fi

Expand Down
2 changes: 1 addition & 1 deletion scripts/check-ui-console-workspace
Original file line number Diff line number Diff line change
Expand Up @@ -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.162 browser-session broker-broadcast
1.6.163 browser-session broker-broadcast
2 changes: 1 addition & 1 deletion scripts/check-ui-critical-high-dependencies
Original file line number Diff line number Diff line change
Expand Up @@ -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.162":
if package.get("version") != "1.6.163":
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')}")
Expand Down
140 changes: 140 additions & 0 deletions tests/integration/state-date-locale-test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,140 @@
import { module, test } from 'qunit';
import { render, find, settled, setupContext, setupRenderingContext, teardownContext } from '@ember/test-helpers';
import { precompileTemplate } from '@ember/template-compilation';
import EmberObject from '@ember/object';
import { run } from '@ember/runloop';
import resolver from '../helpers/resolver';
import { initialize as initializePodLayouts } from 'ui/initializers/pod-component-layouts';
import { initialize as initializeIntl } from 'ui/instance-initializers/intl';

module('Integration | Locale | state and relative date', 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});
initializePodLayouts();
initializeIntl(this.owner);
this.intl = this.owner.lookup('service:intl');

// Read the build's real translation JSON rather than replacing the intl service.
for (let locale of ['en-us', 'zh-tw']) {
let response = await fetch(`/translations/${locale}.json`);
if (!response.ok) {
throw new Error(`Local translation fixture failed: ${locale} ${response.status}`);
}
this.intl.addTranslations(locale, await response.json());
}
this.intl.setLocale(['en-us']);
this.previousMomentLocale = moment.locale();
this.previousMomentNow = moment.now;
moment.locale('en');
moment.now = () => Date.UTC(2026, 9, 1, 12);
await setupRenderingContext(this);
});

hooks.afterEach(async function() {
await teardownContext(this);
moment.now = this.previousMomentNow;
moment.locale(this.previousMomentLocale);
this.testRoot.remove();
});

test('relative date rerenders en to zh-tw to en without changing the date or global Moment locale', async function(assert) {
this.created = Date.UTC(2026, 9, 1, 8);
await render(precompileTemplate('<span class="relative-date">{{date-from-now this.created}}</span>'));
assert.strictEqual(find('.relative-date').textContent, '4 hours ago');

run(() => this.intl.setLocale(['zh-tw', 'en-us']));
await settled();
assert.strictEqual(find('.relative-date').textContent, '4 小時前', 'the unchanged timestamp is recomputed in Traditional Chinese');
assert.strictEqual(moment.locale(), 'en', 'the helper uses an instance locale');

run(() => this.intl.setLocale(['en-us']));
await settled();
assert.strictEqual(find('.relative-date').textContent, '4 hours ago', 'switching back does not leave a stale Chinese result');
});

test('relative date follows the existing zh-hans and English locale mapping', async function(assert) {
this.created = Date.UTC(2026, 9, 1, 8);
run(() => this.intl.setLocale(['zh-hans', 'en-us']));
await render(precompileTemplate('<span class="relative-date">{{date-from-now this.created}}</span>'));
assert.strictEqual(find('.relative-date').textContent, '4 小时前');
assert.strictEqual(moment.locale(), 'en', 'the mapped locale remains instance-local');
});

test('all seven known resource states use the existing translations and rerender on locale change', async function(assert) {
this.models = [
['active', 'Active', '使用中'],
['running', 'Running', '執行中'],
['stopped', 'Stopped', '已停止'],
['stopping', 'Stopping', '停止中'],
['created', 'Created', '已建立'],
['exited', 'Exited', '已結束'],
['error', 'Error', '發生錯誤'],
].map(([state, displayState, chinese]) => EmberObject.create({state, displayState, chinese}));
await render(precompileTemplate('{{#each this.models as |model|}}{{badge-state model=model}}{{/each}}'));

let labels = () => [...this.testRoot.querySelectorAll('.badge')].map((badge) => badge.textContent.trim());
assert.deepEqual(labels(), this.models.map((model) => model.displayState));
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
await settled();
assert.deepEqual(labels(), this.models.map((model) => model.chinese));
run(() => this.intl.setLocale(['en-us']));
await settled();
assert.deepEqual(labels(), this.models.map((model) => model.displayState));
});

test('unknown states retain the original display text across locale changes', async function(assert) {
this.model = EmberObject.create({state: 'vendor-special', displayState: 'Vendor-Special (custom)'});
await render(precompileTemplate('{{badge-state model=this.model}}'));
assert.strictEqual(find('.badge').textContent.trim(), 'Vendor-Special (custom)');
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
await settled();
assert.strictEqual(find('.badge').textContent.trim(), 'Vendor-Special (custom)');
run(() => this.model.set('displayState', 'Vendor updated text'));
await settled();
assert.strictEqual(find('.badge').textContent.trim(), 'Vendor updated text');
});

test('the model display state retains precedence and changes retain icon and color bindings', async function(assert) {
this.model = EmberObject.create({
relevantState: 'running', state: 'active', displayState: 'Running',
stateIcon: 'icon-check', stateColor: 'text-success', stateBackground: 'bg-success',
});
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
await render(precompileTemplate('{{badge-state model=this.model}}'));
assert.strictEqual(find('.badge').textContent.trim(), '執行中');
assert.ok(find('.badge.text-success.bg-success'), 'existing status styling is unchanged');
assert.ok(find('.badge i.icon-check'), 'existing status icon is unchanged');

run(() => this.model.setProperties({relevantState: 'stopped', displayState: 'Stopped'}));
await settled();
assert.strictEqual(find('.badge').textContent.trim(), '已停止');
run(() => this.model.setProperties({relevantState: 'custom-state', displayState: 'Custom-State'}));
await settled();
assert.strictEqual(find('.badge').textContent.trim(), 'Custom-State');
});

test('active resources with a health or connection display override retain that original text', async function(assert) {
this.model = EmberObject.create({state: 'active', relevantState: 'active', displayState: 'Disconnected'});
await render(precompileTemplate('{{badge-state model=this.model}}'));
assert.strictEqual(find('.badge').textContent.trim(), 'Disconnected');
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
await settled();
assert.strictEqual(find('.badge').textContent.trim(), 'Disconnected', 'a machine state does not override the model display semantics');
run(() => this.model.set('displayState', 'Active'));
await settled();
assert.strictEqual(find('.badge').textContent.trim(), '使用中', 'a known English display state is translated once the override clears');
});

test('unknown prototype-like states and missing state do not invent a translation', async function(assert) {
this.models = ['__proto__', 'constructor', ''].map((state) => EmberObject.create({state, displayState: `original:${state}`}));
run(() => this.intl.setLocale(['zh-tw', 'en-us']));
await render(precompileTemplate('{{#each this.models as |model|}}{{badge-state model=model}}{{/each}}'));
assert.deepEqual(
[...this.testRoot.querySelectorAll('.badge')].map((badge) => badge.textContent.trim()),
this.models.map((model) => model.displayState)
);
});
});
Loading
Loading