diff --git a/README.md b/README.md
index 949093b497..3a3eee125b 100644
--- a/README.md
+++ b/README.md
@@ -8,7 +8,7 @@ PastureStack is an independent community effort to preserve, audit, and moderniz
## Project status
-The current compatibility release is `1.6.103`. It retains the existing Node 24, Ember, Sass,
+The current compatibility release is `1.6.104`. It retains the existing Node 24, Ember, Sass,
dependency, browser-smoke, terminal, console, and test-harness modernization.
It adds a provider-neutral OpenID Connect administration and sign-in flow with
PKCE S256, staged configuration validation, a real test login before
@@ -16,6 +16,11 @@ activation, and local-authentication recovery. Product-owned names, logos,
icons, package metadata, and visible text use PastureStack branding. API
models and protocol fields remain compatible.
+Release `1.6.104` keeps the authenticated browser-session and OIDC corrections
+from `1.6.103`. It also keeps the init-process checkbox inside its own resource
+grid column, with the launch-configuration binding unchanged, so the control no
+longer touches the adjacent process-limit input on create or upgrade forms.
+
Release `1.6.103` keeps an authenticated browser session when a non-auth API
request fails during startup, displays the nested OIDC/API explanation instead
of an empty alert, and activates a newly verified provider in unrestricted mode
diff --git a/SECURITY.md b/SECURITY.md
index 4766d55511..1aa6f87e3f 100644
--- a/SECURITY.md
+++ b/SECURITY.md
@@ -2,7 +2,7 @@
## Supported state
-The maintained compatibility release is the pure numeric `1.6.103` line used
+The maintained compatibility release is the pure numeric `1.6.104` line used
by the current PastureStack Server release. Earlier branded coordinates are
historical records and are not current release or deployment targets.
Authentication-provider combinations must still be validated by an
diff --git a/app/components/form-resources/component.js b/app/components/form-resources/component.js
index 871480c6af..4864cc9a4a 100644
--- a/app/components/form-resources/component.js
+++ b/app/components/form-resources/component.js
@@ -118,6 +118,7 @@ export default Component.extend({
let callback = this.get('setRequestedHost');
if ( typeof callback === 'function' ) { callback(hostId); } else { this.set('instance.requestedHostId', hostId); }
},
+ setBoolean(field, event) { this.set(`instance.${field}`, !!event.target.checked); },
setField(field, event) { this.set(`instance.${field}`, event.target.value || null); },
setShm(value) {
updateHardwareDraft(this.get('instance'), {shmError: null});
diff --git a/app/components/form-resources/template.hbs b/app/components/form-resources/template.hbs
index 66cc51fbfd..96c8eed377 100644
--- a/app/components/form-resources/template.hbs
+++ b/app/components/form-resources/template.hbs
@@ -148,10 +148,10 @@
{{/input-or-display}}
-
-
{{t 'formResources.init'}}
+
+ {{t 'formResources.init'}}
{{#input-or-display editable=this.editing value=this.instance.runInit}}
- {{input type='checkbox' checked=this.instance.runInit}} {{t 'formResources.init'}}
+ {{t 'formResources.init'}}
{{/input-or-display}}
diff --git a/app/styles/components/_form-resources.scss b/app/styles/components/_form-resources.scss
index 6694279039..330703f71a 100644
--- a/app/styles/components/_form-resources.scss
+++ b/app/styles/components/_form-resources.scss
@@ -166,6 +166,20 @@ $well-bg: #f5f5f5 !default;
grid-column: span 4;
}
+ .resource-inline-checkbox {
+ align-items: center;
+ display: flex;
+ gap: 8px;
+ min-height: 43px;
+ margin: 0;
+
+ input[type="checkbox"] {
+ flex: 0 0 auto;
+ margin: 0;
+ position: static;
+ }
+ }
+
.resource-advanced-group {
background: $well-bg;
border: 1px solid $table-border-color;
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 b8810dbbc7..e619770b93 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.103",
+ "version": "1.6.104",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
- "version": "1.6.103",
+ "version": "1.6.104",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
diff --git a/package-lock.json b/package-lock.json
index b8810dbbc7..e619770b93 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
- "version": "1.6.103",
+ "version": "1.6.104",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
- "version": "1.6.103",
+ "version": "1.6.104",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
diff --git a/package.json b/package.json
index 8ac8915566..18d8e01e39 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/web-console",
- "version": "1.6.103",
+ "version": "1.6.104",
"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 0fa9fdc51f..1f52ff5c1c 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.103" ]]; then
- echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.103"
+if [[ "$version" != "1.6.104" ]]; then
+ echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.104"
failures=$((failures + 1))
fi
diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace
index 48fecd08e2..4ccc507cf6 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.103 browser-session broker-broadcast
+ 1.6.104 browser-session broker-broadcast
diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies
index 3f8017d3c8..c29a5281ce 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.103":
+if package.get("version") != "1.6.104":
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/scripts/check-ui-ember-api-store-fetch-upgrade b/scripts/check-ui-ember-api-store-fetch-upgrade
index c8d00e0555..2681ab2f0d 100755
--- a/scripts/check-ui-ember-api-store-fetch-upgrade
+++ b/scripts/check-ui-ember-api-store-fetch-upgrade
@@ -322,7 +322,7 @@ storage_template = (repo / "app/host/storage/template.hbs").read_text(encoding="
if 'pageSizeChanged=(action "storagePageSizeChanged")' not in storage_template:
fail("storage page-size callback is missing from the host storage table")
-if template_action_count != 586:
+if template_action_count != 587:
fail(f"unexpected template action count: {template_action_count}")
if power_select_count != 15:
fail(f"unexpected modern PowerSelect count: {power_select_count}")
diff --git a/scripts/check-ui-test-harness-blockers b/scripts/check-ui-test-harness-blockers
index 5f7f125bd4..5d05e131ec 100755
--- a/scripts/check-ui-test-harness-blockers
+++ b/scripts/check-ui-test-harness-blockers
@@ -51,8 +51,8 @@ if module_for_count != 0:
fail("MODULE_FOR_USAGE_COUNT_UNEXPECTED", actual=module_for_count, expected=0)
if module_for_component_count != 0:
fail("MODULE_FOR_COMPONENT_USAGE_UNEXPECTED", actual=module_for_component_count, expected=0)
-if direct_qunit_import_count != 105:
- fail("DIRECT_QUNIT_IMPORT_COUNT_UNEXPECTED", actual=direct_qunit_import_count, expected=105)
+if direct_qunit_import_count != 106:
+ fail("DIRECT_QUNIT_IMPORT_COUNT_UNEXPECTED", actual=direct_qunit_import_count, expected=106)
volatile_computed_count = 0
for path in Path("app").rglob("*.js"):
diff --git a/tests/integration/components/form-resources-test.js b/tests/integration/components/form-resources-test.js
index 8ad151f517..6da5eb0887 100644
--- a/tests/integration/components/form-resources-test.js
+++ b/tests/integration/components/form-resources-test.js
@@ -74,6 +74,23 @@ module('Integration | Component | hardware resources', function(hooks) {
assert.ok(unit.getBoundingClientRect().width < input.getBoundingClientRect().width, 'the unit is compact and the value gets useful space');
assert.ok(find('.resource-field-host select[id$="-host"]'), 'host inventory is prominent in the hardware section');
assert.ok(find('.resource-advanced-toggle[aria-expanded="false"]'), 'rare settings remain discoverable without overwhelming the common path');
+ await click('.resource-advanced-toggle');
+ let pids = find('[data-hardware="pids"]');
+ let init = find('[data-hardware="init"]');
+ let pidsRect = pids.getBoundingClientRect();
+ let initRect = init.getBoundingClientRect();
+ let initFieldRect = init.closest('.resource-init-field').getBoundingClientRect();
+ assert.ok(initRect.left >= initFieldRect.left,
+ 'the init checkbox stays inside its own grid column instead of protruding into the previous control');
+ if ( initRect.top < pidsRect.bottom && initRect.bottom > pidsRect.top ) {
+ assert.ok(initRect.left - pidsRect.right >= 16,
+ 'same-row PID and init controls retain a visible grid gap');
+ } else {
+ assert.ok(initRect.top >= pidsRect.bottom, 'responsive rows remain vertically separated');
+ }
+ assert.equal(getComputedStyle(init).position, 'static', 'the checkbox does not use the legacy negative-offset layout');
+ await click(init);
+ assert.true(this.instance.get('runInit'), 'the aligned control still writes the launch configuration');
});
test('switching primary and sidekick resets drafts and advanced maps without rewriting either config', async function(assert) {
diff --git a/tests/unit/components/new-container-hardware-payload-test.js b/tests/unit/components/new-container-hardware-payload-test.js
new file mode 100644
index 0000000000..a3a3070547
--- /dev/null
+++ b/tests/unit/components/new-container-hardware-payload-test.js
@@ -0,0 +1,120 @@
+import { A } from '@ember/array';
+import EmberObject from '@ember/object';
+import { run } from '@ember/runloop';
+import { module, test } from 'qunit';
+
+import NewContainerComponent from 'ui/components/new-container/component';
+import inertRenderer from '../../helpers/inert-renderer';
+import { createOwned, destroyOwned } from '../../helpers/owned-subject';
+
+module('Unit | Component | new container hardware payload');
+
+function hardwareLaunchConfig() {
+ return EmberObject.create({
+ labels: {},
+ ports: A(),
+ secrets: A(),
+ shmSize: 2147483648,
+ ipcMode: 'private',
+ runtime: 'nvidia',
+ runInit: true,
+ pidsLimit: 512,
+ cpuQuota: 200000,
+ cpuPeriod: 100000,
+ groupAdd: ['993'],
+ tmpfs: {'/run': 'rw,noexec,nosuid,size=64m'},
+ sysctls: {'net.core.somaxconn': '1024'},
+ ulimits: [{name: 'memlock', soft: -1, hard: -1}],
+ devices: ['/dev/dri/renderD128:/dev/dri/renderD128:rw'],
+ deviceRequests: [{driver: 'nvidia', count: 1, capabilities: [['gpu']]}],
+ requestedHostId: '1h1',
+ });
+}
+
+function createComponent(service, launchConfig) {
+ let component;
+
+ run(() => {
+ component = createOwned(NewContainerComponent, {
+ renderer: inertRenderer(),
+ intl: EmberObject.create({t(key) { return key; }}),
+ launchConfig,
+ service,
+ primaryResource: service,
+ primaryService: service,
+ isService: true,
+ }, 'component');
+ });
+
+ return component;
+}
+
+function hardwareSnapshot(config) {
+ return {
+ shmSize: config.get('shmSize'),
+ ipcMode: config.get('ipcMode'),
+ runtime: config.get('runtime'),
+ runInit: config.get('runInit'),
+ pidsLimit: config.get('pidsLimit'),
+ cpuQuota: config.get('cpuQuota'),
+ cpuPeriod: config.get('cpuPeriod'),
+ groupAdd: config.get('groupAdd'),
+ tmpfs: config.get('tmpfs'),
+ sysctls: config.get('sysctls'),
+ ulimits: config.get('ulimits'),
+ devices: config.get('devices'),
+ deviceRequests: config.get('deviceRequests'),
+ requestedHostId: config.get('requestedHostId'),
+ };
+}
+
+test('service creation retains every resource and hardware launch field', async function(assert) {
+ let launchConfig = hardwareLaunchConfig();
+ let saved;
+ let service = EmberObject.create({
+ launchConfig,
+ secondaryLaunchConfigs: A(),
+ save() {
+ saved = hardwareSnapshot(this.get('launchConfig'));
+ return Promise.resolve(this);
+ },
+ });
+ let component = createComponent(service, launchConfig);
+
+ await component.doSave();
+
+ assert.deepEqual(saved, hardwareSnapshot(launchConfig), 'create payload keeps all configured fields');
+ destroyOwned(component);
+});
+
+test('service upgrade sends every resource and hardware field in the upgrade strategy', async function(assert) {
+ let launchConfig = hardwareLaunchConfig();
+ let action;
+ let payload;
+ let service = EmberObject.create({
+ launchConfig,
+ secondaryLaunchConfigs: A(),
+ save() { return Promise.resolve(this); },
+ waitForAction(name) {
+ assert.equal(name, 'upgrade', 'waits for the upgrade action');
+ return Promise.resolve();
+ },
+ doAction(name, value) {
+ action = name;
+ payload = value;
+ return Promise.resolve();
+ },
+ });
+ let component = createComponent(service, launchConfig);
+
+ run(() => component.setProperties({
+ isUpgrade: true,
+ upgradeOptions: {batchSize: 1, intervalMillis: 2000, startFirst: false},
+ }));
+ await component.doSave();
+
+ assert.equal(action, 'upgrade');
+ assert.deepEqual(hardwareSnapshot(payload.inServiceStrategy.launchConfig), hardwareSnapshot(launchConfig),
+ 'upgrade payload keeps all configured fields');
+ destroyOwned(component);
+});