mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-09 18:15:50 +00:00
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:
@@ -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.
|
||||
|
||||
@@ -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."
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user