fix(web): wrap entitled organization header on compact screens

Activate the offline default context before mobile organization audits. Preserve visible session controls without page overflow, with real-backend Chromium and WebKit proof.

Change-source: pulse-maintainer
This commit is contained in:
pulse-triage[bot]
2026-09-09 11:42:06 +01:00
parent 14a109444c
commit 6cddb75fb1
8 changed files with 91 additions and 20 deletions
@@ -8311,3 +8311,13 @@ Related command targets do not retarget an alert. The funded OpenRouter
`openai/gpt-6-astra` route has explicitly reviewed, tiered cost estimates dated
2026-09-07. Unreviewed aliases stay unknown. Its history-explanation qualification
is distinct from the earlier Gemini lab matrix and from wider rollout evidence.
### Compact shell Assistant coexistence
The mobile Assistant launcher remains in shared header flow. That header and
its controls may wrap at compact widths so an entitled Organization selector
does not displace session actions or the connection indicator beyond the page.
Keep desktop launcher placement and existing visibility decisions unchanged;
wrapping must not introduce entitlement reads or alter Assistant authority.
AppLayout component and architecture regressions preserve these boundaries;
real-backend phone geometry proof is retained by the Organization mobile audit.
@@ -3462,3 +3462,13 @@ A failed reconnect admission request retains the current tenant's last valid
facet. The synchronous organisation-switch reset still clears outgoing
admission before requesting the new tenant, including when that request fails.
This is navigation continuity, not an authorization or entitlement decision.
### Compact entitled Organization chrome
An entitled self-hosted Organization selector must not push session controls
outside a compact viewport. The shared header may wrap on phones while retaining
desktop placement; this must not change organization scope, feature gates or
activation semantics. The offline mobile audit explicitly activates the default
context before it creates and activates isolated organizations, using the normal
authenticated API. Its route/overflow checks and AppLayout regression cover this
boundary without granting Community private RBAC or changing CI tier membership.
@@ -7280,3 +7280,17 @@ must not clear a background history filter or invalidate its return-focus target
Ordinary search shortcuts resume when the background becomes interactive again.
When Assistant is already open, a desktop-to-phone transition must retain that
destination rather than reopening the underlying history drawer above it.
### Compact Organization header wrapping
The shared header and its controls must wrap below the small-screen breakpoint
instead of forcing the document wider when an entitled Organization selector
joins the session controls. Keep the selector, kiosk/logout controls and
connection indicator available; do not hide overflow to conceal an inaccessible
action. Desktop grid placement remains unchanged.
Proof: `AppLayout.test.tsx` pins wrapping and retained logout semantics. The
signed-offline `05-settings-mobile-audit.spec.ts` exercises real Organization,
Access and Sharing routes at 320px and 390px, with app-shell width and full-scroll
assertions; header screenshots retain the compact layout. This is independent
of private RBAC implementation and hosted probation acceptance.
+21 -18
View File
@@ -1,39 +1,42 @@
{
"version": 1,
"base_sha": "6f3547bc5fbb10ddf313a8cd2f184b6359173a59",
"verified_at": "2026-09-08T22:29:31.643744Z",
"base_sha": "14a109444c6d1cad420797ea30f279e77b6fc517",
"verified_at": "2026-09-09T10:42:06.278902Z",
"result": "passed",
"changed_paths": [
"frontend-modern/src/components/Alerts/useWebhookConfigState.ts"
"frontend-modern/src/AppLayout.tsx"
],
"content_sha256": {
"frontend-modern/src/components/Alerts/useWebhookConfigState.ts": "14f0dceb069fe52b9a47100c50998ae53a2c5c8a29a524e342f7458546cb4a19"
"frontend-modern/src/AppLayout.tsx": "71cbc527fae0dbae3891174bb8a140d90f50917405d6c15a3b0d8ae7dcb31644"
},
"routes": [
"/qualification (real WebhookConfigForm and hook; synthetic callbacks)"
"/settings/organization",
"/settings/organization/access",
"/settings/organization/sharing"
],
"viewports": [
{
"width": 1440,
"height": 900
},
{
"width": 900,
"height": 900
"width": 320,
"height": 568
},
{
"width": 390,
"height": 900
"height": 844
},
{
"width": 1280,
"height": 720
}
],
"states": [
"Unsaved manually entered Pushover aliases",
"Test callback payload",
"Save callback payload"
"Signed entitled default and created organizations; non-demo Community backend",
"Access invitation and outgoing sharing fixture",
"Desktop incoming share pending then accepted"
],
"interactions": [
"Click Test then Add Webhook at each width; assert both canonical token/user payloads, no page errors or horizontal overflow."
"Chromium and mobile WebKit: switch organization; audit three routes at both phone widths; reach shell bottom and assert page/shell width within one pixel",
"Desktop Chromium Scenario 6: verify owner identity relationships, editor/viewer roles and pending approval; click Accept and verify accepted state via API"
],
"notes": "Real component with Vite dev transform in Chromium, not production build or installed/provider delivery. Phone screenshot visually inspected; synthetic credentials only. Screenshots /tmp/pulse-webhook-parity.",
"command": "pulse-heavy-run -- node scripts/check-webhook-test-save-parity.mjs"
"command": "pulse-heavy-run -- bash -c \"cd tests/integration && PULSE_E2E_USE_LOCAL_BACKEND=true node scripts/with-offline-entitlements.mjs node scripts/run-playwright.mjs tests/05-settings-mobile-audit.spec.ts --project=chromium --project=mobile-safari --grep settings/organization --workers=1 --retries=0\"; separate diagnostic config Scenario 6 desktop run",
"notes": "Exact actual mobile grep: settings/organization(/(sharing|access))?$ . Six mobile route tests passed (53.0s); desktop sharing scenario passed (3.8s). Source-built real Community backend, ephemeral loopback signing issuer, no mock mode or signature bypass. Phone Chromium/WebKit and desktop acceptance screenshots inspected. Evidence retained in Web20260909T103517Z outcome directory browser-proof, org-fixed.log and scenario6-desktop.log. Not private RBAC, hosted probation clearance or release qualification."
}
+2 -2
View File
@@ -741,7 +741,7 @@ export function AppLayout(props: AppLayoutProps) {
/>
</Show>
<div
class={`header mb-1 flex items-center gap-1 sm:mb-3 sm:gap-2 ${
class={`header mb-1 flex flex-wrap items-center gap-1 sm:flex-nowrap sm:mb-3 sm:gap-2 ${
kioskMode()
? 'fixed top-0 left-0 right-0 z-50 justify-end bg-surface shadow-sm'
: 'justify-between sm:grid sm:grid-cols-[1fr_auto_1fr] sm:items-center sm:gap-0'
@@ -808,7 +808,7 @@ export function AppLayout(props: AppLayoutProps) {
</div>
</Show>
<div
class={`header-controls flex items-center gap-1 sm:gap-2 ${kioskMode() ? '' : 'justify-end sm:col-start-3 sm:col-end-4 sm:w-auto sm:justify-end sm:justify-self-end'}`}
class={`header-controls flex max-w-full flex-wrap items-center gap-1 sm:flex-nowrap sm:gap-2 ${kioskMode() ? '' : 'justify-end sm:col-start-3 sm:col-end-4 sm:w-auto sm:justify-end sm:justify-self-end'}`}
>
<Show when={assistantLauncherVisible() && viewport.isBelow('lg')}>
{renderAssistantLauncher(AI_CHAT_MOBILE_LAUNCHER_BUTTON_CLASS)}
@@ -571,6 +571,20 @@ describe('App architecture', () => {
expect(retiredRouteNavigations).toEqual([]);
});
it('keeps compact organization chrome presentational and its offline audit explicitly entitled', () => {
expect(appLayoutSource).toContain('<Show when={props.showOrgSwitcher()}>');
expect(appLayoutSource).toContain('selectedOrgId={props.activeOrgID()}');
expect(appLayoutSource).toContain('onChange={props.onSwitchOrg}');
expect(appLayoutSource).not.toContain('/api/license/activate');
const audit = readFileSync(join(integrationTestsDir, '05-settings-mobile-audit.spec.ts'), 'utf8');
const preparation = audit.slice(audit.indexOf('const prepareOrganizationAuditFixture'), audit.indexOf('const test = base.extend'));
const activation = preparation.indexOf('await activateOfflineOrganization(page)');
expect(activation).toBeGreaterThan(preparation.indexOf('await ensureSessionAuthenticated(page)'));
expect(activation).toBeLessThan(preparation.indexOf('await createOrg(page'));
expect(activation).toBeGreaterThan(0);
expect(preparation).toContain('process.env.PULSE_E2E_OFFLINE_ACTIVATION_KEY');
});
it('keeps authenticated chrome in AppLayout and hosted bootstrap in useAppRuntimeState', () => {
expect(appLayoutSource).toContain('export function AppLayout(props: AppLayoutProps)');
expect(appLayoutSource).toContain(
@@ -510,6 +510,14 @@ describe('AppLayout navigation icons', () => {
expect(launcher.closest('.header-controls')).toBeInTheDocument();
});
it('allows mobile header controls to wrap without hiding organization or session actions', () => {
setViewportWidth(320);
const { container } = renderLayout();
expect(container.querySelector('.header')).toHaveClass('flex-wrap', 'sm:flex-nowrap');
expect(container.querySelector('.header-controls')).toHaveClass('max-w-full', 'flex-wrap', 'sm:flex-nowrap');
expect(screen.getByRole('button', { name: 'Logout' })).toBeInTheDocument();
});
it('preserves the Assistant edge launcher on desktop', () => {
setViewportWidth(1440);
renderLayout();
@@ -5,6 +5,8 @@ import { fileURLToPath } from "node:url";
import { test as base, expect } from "@playwright/test";
import {
apiRequest,
activateOfflineOrganization,
ensureSessionAuthenticated,
createAuthenticatedStorageState,
createOrg,
} from "./helpers";
@@ -98,6 +100,11 @@ const prepareOrganizationAuditFixture = async (
if (!(ORGANIZATION_SETTINGS_ROUTES as readonly string[]).includes(route))
return [];
if (process.env.PULSE_E2E_OFFLINE_ACTIVATION_KEY) {
await ensureSessionAuthenticated(page);
await activateOfflineOrganization(page);
}
await page.goto("/settings/infrastructure", {
waitUntil: "domcontentloaded",
});
@@ -255,6 +262,11 @@ test.describe("Settings mobile optimization audit", () => {
audit.pageWidth,
`Mobile overflow on ${route} at ${viewport.width}px (viewport=${audit.viewportWidth}, page=${audit.pageWidth}, offenders=${JSON.stringify(audit.offenders)})`,
).toBeLessThanOrEqual(audit.viewportWidth + 1);
if ((ORGANIZATION_SETTINGS_ROUTES as readonly string[]).includes(route)) {
// Retain the header as well as the bottom-of-content width proof.
await page.locator(".app-scroll-shell").evaluate((element) => { element.scrollTop = 0; });
await page.screenshot({ path: test.info().outputPath(`organization-${viewport.width}.png`) });
}
}
} finally {
if (createdOrgIDs.length > 0) {