diff --git a/README.md b/README.md
index 22285578c8..2ed77beb5b 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 source compatibility target is `1.6.155`. It retains the existing Node 24, Ember, Sass,
+The current source compatibility target is `1.6.156`. 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,12 +16,17 @@ 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.155` positions global notices below the navigation bar using the
-existing navbar height and keeps them inside narrow viewports. This corrects
-the mobile layout overlap found during `1.6.154` acceptance without changing
-authorization or API behavior. See the
-[release note](docs/releases/web-console-1.6.155.md) for focused visual tests;
-packaged Server/browser acceptance must be checked against a new image.
+Release `1.6.156` places authenticated-page notices in the normal layout flow
+between the navigation and main content, so an error cannot cover the page
+title or right-side actions. Login/MFA notices keep their existing body host.
+See the [release note](docs/releases/web-console-1.6.156.md) for focused tests;
+packaged Server/browser acceptance remains a separate gate.
+
+Release `1.6.155` moved global notices below the navigation bar and bounded
+their width. QA 8080 browser acceptance subsequently found that the fixed
+notice still covered the mobile page title and desktop header actions. It is
+not the accepted layout; see the
+[1.6.155 release note](docs/releases/web-console-1.6.155.md).
Release `1.6.154` shows a localized, persistent permission error when a direct
Stack or Service creation URL is denied, then returns to the Stacks list.
diff --git a/app/authenticated/route.js b/app/authenticated/route.js
index 9c9c860f4f..746f79e0bb 100644
--- a/app/authenticated/route.js
+++ b/app/authenticated/route.js
@@ -38,6 +38,7 @@ export function projectIdFromTransition(transition) {
export default Route.extend(Subscribe, PromiseToCb, {
catalog : service(),
+ growl : service(),
prefs : service(),
projects : service(),
settings : service(),
@@ -158,6 +159,12 @@ export default Route.extend(Subscribe, PromiseToCb, {
let app = this.controllerFor('application');
this._super();
+ scheduleOnce('afterRender', this, function() {
+ let mount = document.getElementById('growl-mount');
+ if (mount && mount.isConnected) {
+ this.get('growl').placeContainer(mount);
+ }
+ });
if ( !this.controllerFor('application').get('isPopup') && this.get('projects.current') )
{
this.connectSubscribe();
@@ -177,6 +184,9 @@ export default Route.extend(Subscribe, PromiseToCb, {
},
deactivate() {
+ // Keep the plugin's container alive when Ember removes this route's mount.
+ this.get('growl').placeContainer(document.body);
+
this._super();
this.disconnectSubscribe();
cancel(this.get('testTimer'));
diff --git a/app/authenticated/template.hbs b/app/authenticated/template.hbs
index 52862e75c2..4006c97478 100644
--- a/app/authenticated/template.hbs
+++ b/app/authenticated/template.hbs
@@ -7,6 +7,7 @@
goToPrevious="goToPrevious"
}}
{{/unless}}
+
{{#if (and this.forceUpgrade this.projects.current.needsUpgrade)}}
{{project-upgrade model=this.projects.current}}
diff --git a/app/services/growl.js b/app/services/growl.js
index 043a705253..07d68be4d7 100644
--- a/app/services/growl.js
+++ b/app/services/growl.js
@@ -12,8 +12,20 @@ export default Service.extend({
$.jGrowl.defaults.closerTemplate = '';
},
+ placeContainer(target) {
+ let destination = target || document.getElementById('growl-mount') || document.body;
+ let container = document.getElementById('jGrowl');
+
+ if (container && container.parentNode !== destination) {
+ destination.appendChild(container);
+ }
+
+ return destination;
+ },
+
raw: function(title, body, opt) {
opt = opt || {};
+ opt.appendTo = this.placeContainer();
if ( title )
{
diff --git a/app/styles/components/_growl.scss b/app/styles/components/_growl.scss
index 5dbfbff5bf..c2d8505219 100644
--- a/app/styles/components/_growl.scss
+++ b/app/styles/components/_growl.scss
@@ -7,6 +7,15 @@ $navbar-height: null !default;
z-index: 10;
}
+#growl-mount .jGrowl.top-right {
+ // The authenticated layout places this between the navbar and main.
+ // Keep notices in flow so page titles and actions start below them.
+ position: static;
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+}
+
.jGrowl {
.jGrowl-close {
position: absolute;
@@ -23,6 +32,12 @@ $navbar-height: null !default;
color: white;
}
+ .jGrowl-notification {
+ // Anchor each dismiss control to its own notice, including in the
+ // in-flow mobile layout and when several notices are visible.
+ position: relative;
+ }
+
.jGrowl-notification .jGrowl-header {
font-size: 1em;
}
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 b106cb37d7..1ea9515f9c 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.155",
+ "version": "1.6.156",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
- "version": "1.6.155",
+ "version": "1.6.156",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
diff --git a/docs/releases/web-console-1.6.155.md b/docs/releases/web-console-1.6.155.md
index 3f9fa783c0..876021032f 100644
--- a/docs/releases/web-console-1.6.155.md
+++ b/docs/releases/web-console-1.6.155.md
@@ -17,3 +17,10 @@ The focused Chrome QUnit layout test covers both themes, both directions and
fit, dismiss-control placement and opacity. Local production/development
build and the focused test passed. Official release CI and packaged 8080
browser acceptance are separate gates and must be recorded after publication.
+
+The subsequent isolated v1.6.489 / 1.6.155 packaged-browser run confirmed
+navigation clearance but found that the fixed notice still obscured the
+375px page title and 1440px right-side sort controls. Thus the layout was not
+accepted despite the original focused runner reporting visibility PASS.
+Web Console 1.6.156 moves authenticated notices into document flow and adds
+content-overlap checks; this note retains the actual 1.6.155 outcome.
diff --git a/docs/releases/web-console-1.6.156.md b/docs/releases/web-console-1.6.156.md
new file mode 100644
index 0000000000..60855d28c8
--- /dev/null
+++ b/docs/releases/web-console-1.6.156.md
@@ -0,0 +1,25 @@
+# Web Console 1.6.156
+
+Server v1.6.489 packaged Web Console 1.6.155. Its isolated QA 8080 browser
+run confirmed that the permission notice was visible and no longer covered
+the 45px navigation bar, but screenshots revealed a remaining defect: at
+375px the fixed notice covered the page title; at 1440px it covered the
+right-side sort controls. The earlier viewport-only test had incorrectly
+reported those cases as passing. This release addresses that shared layout
+cause, not the Stack or Service authorization rules.
+
+On authenticated pages the existing jGrowl container is moved into a mount
+between the navigation and ``. It stays in the normal document flow and
+is right-aligned, so visible notices push page titles and header actions down
+at desktop and mobile widths. The same container and plugin instance move
+back to the body when leaving the authenticated route; login and MFA notices
+retain their existing behavior. A deferred mount operation checks that the
+destination still belongs to the document before moving the container.
+
+Focused Chrome QUnit tests cover the shared notification service and layout
+at 1440px, 375px, and 280px, including both themes and text directions.
+Local Playwright screenshots with compiled CSS verify that the notice clears
+the navigation, title, and header actions at those widths. These are source
+and local-browser checks; official CI, the immutable Server image, and a new
+QA 8080 packaged-browser receipt must be verified separately. No API,
+Engine, permission, or request-payload contract changes in this release.
diff --git a/package-lock.json b/package-lock.json
index b106cb37d7..1ea9515f9c 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -1,12 +1,12 @@
{
"name": "@pasturestack/web-console",
- "version": "1.6.155",
+ "version": "1.6.156",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@pasturestack/web-console",
- "version": "1.6.155",
+ "version": "1.6.156",
"license": "Apache-2.0",
"dependencies": {
"sass": "1.103.1"
diff --git a/package.json b/package.json
index d1493b989f..84d5510bfe 100644
--- a/package.json
+++ b/package.json
@@ -1,6 +1,6 @@
{
"name": "@pasturestack/web-console",
- "version": "1.6.155",
+ "version": "1.6.156",
"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 f6d8b9531e..7eca9d616c 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.155" ]]; then
- echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.155"
+if [[ "$version" != "1.6.156" ]]; then
+ echo "UNEXPECTED_UI_ARTIFACT_VERSION version=$version expected=1.6.156"
failures=$((failures + 1))
fi
diff --git a/scripts/check-ui-console-workspace b/scripts/check-ui-console-workspace
index ea9c19448e..d5051eb217 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.155 browser-session broker-broadcast
+ 1.6.156 browser-session broker-broadcast
diff --git a/scripts/check-ui-critical-high-dependencies b/scripts/check-ui-critical-high-dependencies
index 60ddcc8683..6c5be3fa5f 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.155":
+if package.get("version") != "1.6.156":
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/unit/services/growl-test.js b/tests/unit/services/growl-test.js
index 2ce40f5fce..3087b1cadb 100644
--- a/tests/unit/services/growl-test.js
+++ b/tests/unit/services/growl-test.js
@@ -1,8 +1,49 @@
import { module, test } from 'qunit';
+import $ from 'jquery';
import Growl from 'ui/services/growl';
module('Unit | Service | growl');
+test('notices use the body outside authenticated pages and move with its mount', function(assert) {
+ let growl = Growl.create();
+ growl.raw('Login error', 'Try again');
+
+ let container = document.getElementById('jGrowl');
+ assert.ok(container && container.classList.contains('jGrowl'), 'the global plugin starts');
+ assert.strictEqual(container.parentNode, document.body, 'login notices use the body');
+ let instance = $(container).data('jGrowl.instance');
+ assert.ok(instance && instance.interval, 'the plugin timer is running');
+
+ let mount = document.createElement('div');
+ mount.id = 'growl-mount';
+ document.getElementById('qunit-fixture').appendChild(mount);
+ growl.placeContainer();
+ assert.strictEqual(container.parentNode, mount, 'authenticated notices use the content flow');
+
+ growl.placeContainer(document.body);
+ assert.strictEqual(container.parentNode, document.body, 'logout keeps the plugin available');
+ assert.strictEqual($(container).data('jGrowl.instance'), instance,
+ 'moving the container preserves its running plugin instance');
+ mount.remove();
+ assert.strictEqual(growl.placeContainer(), document.body, 'later login notices still target the body');
+ growl.raw('Login error', 'Try again');
+ assert.strictEqual(container.parentNode, document.body, 'later login errors still use the body');
+
+ $(container).jGrowl('shutdown');
+ container.remove();
+
+ document.getElementById('qunit-fixture').appendChild(mount);
+ growl.raw('Denied', 'No create permission');
+ let newContainer = document.getElementById('jGrowl');
+ assert.strictEqual(newContainer.parentNode, mount,
+ 'the first authenticated notice is created inside the mount');
+
+ $(newContainer).jGrowl('shutdown');
+ newContainer.remove();
+ mount.remove();
+ growl.destroy();
+});
+
test('denied deletes and actions use neutral, private-safe copy in all supported write locales', async function(assert) {
for (let locale of ['en-us', 'zh-tw', 'ja-jp']) {
let response = await fetch(`/translations/${locale}.json`);
diff --git a/tests/unit/vendor/growl-viewport-layout-test.js b/tests/unit/vendor/growl-viewport-layout-test.js
index 403d9485d2..d746a774a7 100644
--- a/tests/unit/vendor/growl-viewport-layout-test.js
+++ b/tests/unit/vendor/growl-viewport-layout-test.js
@@ -2,7 +2,7 @@ import { module, test } from 'qunit';
module('Unit | Vendor | Growl viewport layout');
-function growlFrame(asset, direction) {
+function growlFrame(asset, direction, headingLayout) {
return new Promise((resolve, reject) => {
let frame = document.createElement('iframe');
let timer = setTimeout(() => reject(new Error(`Timed out loading ${asset}`)), 10000);
@@ -15,47 +15,73 @@ function growlFrame(asset, direction) {
frame.srcdoc = `
-
+
-