diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 8b448c8e7..c43244f0c 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -7085,3 +7085,16 @@ Patrol-origin fixes are waiting for approval. Loading, failed-load, no-runs, and truncated-history states carry distinct copy, and a failed load never renders zero counts as if the week were quiet. Browser proof covers the desktop and narrow Activity tab in `frontend-modern/browser-verification.json`. + +### Phone Docker update labels preserve page-owned scrolling + +Below the 33.999rem table-container boundary, Docker update cells reduce inline +padding and wrap existing badge/button content within the allocated cell. The +rule is scoped to `docker-container-update-cell`; it must not restore a nested +horizontal scrollport or change app-shell touch handling. Phone platform wrappers +retain `overflow: clip`. Labels remain text, not icon-only substitutes. + +App.architecture.test.ts protects the CSS scope and scroll ownership; +96-navigation-socket-recovery.spec.ts checks real 390px text ranges and the +non-scrolling wrapper. Browser receipts do not establish physical Android touch +behaviour or every asynchronous update state. diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index da742ea31..a4f1dc5a5 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -5030,3 +5030,17 @@ organisation URL state reset. The global no-op store reports false. A failed admission HTTP refresh is not an empty estate either: retain the last valid facet on request failure. A successful response still replaces it, including all-false admission; first-load failure remains unresolved. + +### Existing Docker update and expanded identity text remain readable + +DockerContainersTable marks its update cell as the phone reflow boundary for both +read-only status and governed UpdateButton content. The boundary must preserve +full action names, review semantics and native agent/container identity; it must +not hide update labels to satisfy width constraints. DockerNativeTables.test.ts +checks that the governed update action remains inside that boundary. + +ResourceDetailDrawer headings wrap long display names rather than ellipsising +them. Overview rows may still truncate identity; keyboard expansion exposes the +full heading. This shared drawer rule applies across resource types. The 390px +browser qualification checks heading fit and keyboard expansion; no data model, +authorisation, action dispatch or resource admission contract changes. diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index b3cec5a33..40ecc4f94 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,37 +1,47 @@ { "version": 1, - "base_sha": "2356e4300022a9e0502b97030b84fc902ca15281", - "verified_at": "2026-09-05T15:02:51Z", + "base_sha": "5beeaf4cbe32603bf4d8dcb07069957a659aa1a4", + "verified_at": "2026-09-05T17:22:19Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx", - "frontend-modern/src/types/config.ts" + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx", + "frontend-modern/src/features/docker/DockerContainersTable.tsx", + "frontend-modern/src/index.css" ], "content_sha256": { - "frontend-modern/src/components/Settings/settingsPanelRegistryContext.tsx": "a4c53fd983db3390e43771e484ef7337c72a0ada6e6c28bcc311b02e88cbde19", - "frontend-modern/src/types/config.ts": "07c9961a8ca87998b6d6e4f398770efd11bfd4b089ce25f18e93e5bafffd9c8d" + "frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx": "0c81c819d7a4fc604df3e202181815b1fcab3b7e3c28198c8a4f7a4d5e441dd3", + "frontend-modern/src/features/docker/DockerContainersTable.tsx": "555c0653223509adafbde423558ef9e89672d68c096e50f6f4772c42088b8286", + "frontend-modern/src/index.css": "db660b38b158594f6e09f179db69e76f85afed11f0dc863fb35619720445312d" }, "routes": [ - "/settings/organization/sharing" + "/docker", + "/proxmox", + "/settings", + "/alerts" ], "viewports": [ - { - "width": 1280, - "height": 720 - }, { "width": 390, "height": 844 + }, + { + "width": 1100, + "height": 900 + }, + { + "width": 1440, + "height": 900 } ], "states": [ - "Source-built isolated e2e_runtime with mock inventory, Chromium desktop context at default zoom; narrow run changes scenario 6 viewport only, not device emulation.", - "Cookie-session organisation switch preserves currentUsername while security detail drops from privileged to authenticated; both owner memberships match. Pending incoming/outgoing shares render; incoming Accept succeeds with persisted accepted status and viewer role.", - "Pending and accepted screenshots inspected at both widths. Wide table rows and narrow scrolled layout are clipped; this is functional identity/acceptance proof, not a clean visual-layout claim. No installed customer, release, reconnect/admission or automatic interruption-cleanup qualification." + "Isolated source-built local backend with synthetic inventory; desktop Chromium at default zoom, not physical Android or touch emulation.", + "Six populated-navigation recovery cases passed: normal and failed admission at each width. Phone update text-range, single-line Current, non-scrolling wrapper and expanded heading checks passed at 390px normal admission.", + "Viewed retained final phone screenshots: Current and Update fully visible; notifications-worker wraps in expanded drawer. No blanket accessibility, 320px, every asynchronous action-state or installed-release claim." ], "interactions": [ - "pulse-heavy-run -- bash tests/integration/scripts/run-tests.sh multi-tenant: 6 passed, 1 expected disabled-feature skip.", - "PULSE_E2E_SHARING_NARROW=1 pulse-heavy-run -- bash tests/integration/scripts/run-tests.sh multi-tenant: 6 passed, 1 expected disabled-feature skip.", - "Scenario 6 creates two organisations and pending shares, reloads scoped cookie session, asserts owner identity, clicks Accept, verifies Active/Remove and accepted API role, switches to default and deletes fixtures. Scenario 7 cookie-session RBAC scope checks pass." + "pulse-heavy-run -- env PULSE_E2E_USE_LOCAL_BACKEND=1 PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL=1 PULSE_MOCK_MODE=true PULSE_E2E_NAVIGATION_RECOVERY=1 PULSE_E2E_TABLE_ACCESS=1 PULSE_E2E_LOCAL_BACKEND_PORT=18765 npm --prefix tests/integration test -- tests/96-navigation-socket-recovery.spec.ts --project=chromium: six passed; managed backend stopped.", + "Interrupted and restored WebSocket connections; preserved populated navigation through successful and failed admission; exercised mobile More, Settings, platform switching and Alerts.", + "At 390px, horizontal wheel and ArrowRight leave wrapper unscrolled; Enter expands container detail, whose heading is scrolled into view and photographed. Receipts: tests/qualification/navigation-reconnect/2026-09-05-phone-layout/.", + "Receipt reconstructed during governance repair from the retained final run and exact unchanged runtime bytes. Only contracts and tests changed after that run." ] } diff --git a/frontend-modern/src/__tests__/App.architecture.test.ts b/frontend-modern/src/__tests__/App.architecture.test.ts index b6a3c7163..494238286 100644 --- a/frontend-modern/src/__tests__/App.architecture.test.ts +++ b/frontend-modern/src/__tests__/App.architecture.test.ts @@ -986,3 +986,18 @@ describe('mobile bottom navigation clearance', () => { expect(offenders).toEqual([]); }); }); + + +describe('Docker phone update reflow boundary', () => { + it('scopes label wrapping to narrow Docker cells without restoring a nested scrollport', () => { + const phoneStyles = appStylesSource.slice(appStylesSource.indexOf('/* Phone update states')); + expect(phoneStyles).toContain('@container (max-width: 33.999rem)'); + expect(phoneStyles).toMatch(/\.docker-container-update-cell\s*\{\s*padding-inline: 0.125rem;/); + expect(phoneStyles).toContain('max-width: 100%'); + expect(phoneStyles).toContain('white-space: normal'); + expect(phoneStyles).toContain('overflow-wrap: anywhere'); + expect(phoneStyles).toContain('flex-wrap: wrap'); + expect(phoneStyles).not.toMatch(/overflow(?:-x|-y)?:\s*(auto|scroll)/); + expect(appStylesSource).toMatch(/\.table-scroll-shell\.table-scroll-shell-phone-page\s*\{\s*overflow: clip;/); + }); +}); diff --git a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx index 7b80aa5f4..0d3cba03b 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceDetailDrawer.tsx @@ -84,7 +84,7 @@ const DrawerContent: Component = (props) => { />

{drawer.displayName()} diff --git a/frontend-modern/src/features/docker/DockerContainersTable.tsx b/frontend-modern/src/features/docker/DockerContainersTable.tsx index d746b7ee0..284cc17ee 100644 --- a/frontend-modern/src/features/docker/DockerContainersTable.tsx +++ b/frontend-modern/src/features/docker/DockerContainersTable.tsx @@ -632,7 +632,7 @@ export const DockerContainersTable: Component = (pro ); case 'updates': return ( - + { // One click plans the governed action; the review dialog is the // confirmation surface, so no second in-row confirm click exists. const updateButton = screen.getByRole('button', { name: /click to review and update/i }); + // The phone reflow boundary must include the governed action, not just read-only badges. + expect(updateButton.closest('td')).toHaveClass('docker-container-update-cell'); + expect(updateButton).toHaveTextContent('Update'); fireEvent.click(updateButton); await waitFor(() => diff --git a/frontend-modern/src/index.css b/frontend-modern/src/index.css index 80676492e..bb3365a7d 100644 --- a/frontend-modern/src/index.css +++ b/frontend-modern/src/index.css @@ -1432,3 +1432,33 @@ body, transition: none; } } + +/* Phone update states must remain readable inside the existing, non-scrolling + * Docker table. Wrap the badge rather than clipping it or hiding its label. */ +@container (max-width: 33.999rem) { + .docker-container-update-cell { + padding-inline: 0.125rem; + } + + .docker-container-update-cell > :is(span, div), + .docker-container-update-cell button { + max-width: 100%; + white-space: normal; + overflow-wrap: anywhere; + overflow: visible; + text-overflow: clip; + } + + .docker-container-update-cell > span, + .docker-container-update-cell button { + flex-wrap: wrap; + justify-content: center; + gap: 0.125rem; + padding-inline: 0.125rem; + font-size: 10px; + } + + .docker-container-update-cell svg { + flex-shrink: 0; + } +} diff --git a/tests/integration/tests/96-navigation-socket-recovery.spec.ts b/tests/integration/tests/96-navigation-socket-recovery.spec.ts index 079123fe7..296194719 100644 --- a/tests/integration/tests/96-navigation-socket-recovery.spec.ts +++ b/tests/integration/tests/96-navigation-socket-recovery.spec.ts @@ -137,6 +137,37 @@ for (const admissionFailure of [false, true]) { })); await row.scrollIntoViewIfNeeded(); const initial = await measure(); + // Text ranges catch clipped glyphs even when the wrapper itself has no + // scrollable overflow. Check every rendered update state, not one row. + await expect(table.locator('.docker-container-update-cell').filter({ hasText: 'Current' }).first()).toBeVisible(); + const clippedUpdateText = await table.locator('.docker-container-update-cell').evaluateAll(cells => + cells.flatMap(cell => { + const bounds = cell.getBoundingClientRect(); + const walker = document.createTreeWalker(cell, NodeFilter.SHOW_TEXT); + const clipped: string[] = []; + while (walker.nextNode()) { + if (!walker.currentNode.textContent?.trim()) continue; + const range = document.createRange(); + range.selectNodeContents(walker.currentNode); + if (Array.from(range.getClientRects()).some(rect => + rect.left < bounds.left - 1 || rect.right > bounds.right + 1)) { + clipped.push(walker.currentNode.textContent!); + } + } + return clipped; + }), + ); + expect(clippedUpdateText).toEqual([]); + const currentLabel = table.locator('.docker-container-update-cell span').filter({ hasText: /^Current$/ }).last(); + expect(await currentLabel.evaluate(el => { + const range = document.createRange(); + range.selectNodeContents(el); + return range.getClientRects().length; + })).toBe(1); + + expect(initial.overflowX).toBe('clip'); + expect(initial.scrollWidth).toBe(initial.width); + await row.hover(); await page.mouse.wheel(2000, 0); await page.waitForTimeout(350); @@ -156,6 +187,13 @@ for (const admissionFailure of [false, true]) { await expect(toggle).toHaveAttribute('aria-expanded', 'true'); const detailId = await toggle.getAttribute('aria-controls'); const detailText = detailId ? await page.locator(`[id="${detailId}"]`).innerText() : null; + const heading = page.locator(`[id="${detailId}"] h2`).first(); + await expect(heading).toContainText('notification'); + expect(await heading.evaluate(el => el.scrollWidth <= el.clientWidth && + getComputedStyle(el).textOverflow !== 'ellipsis')).toBe(true); + await heading.scrollIntoViewIfNeeded(); + await capture('table-full-detail-heading'); + await testInfo.attach('narrow-table-access', { body: JSON.stringify({ initial, pointer, keyboard, accessibleRow, expanded, detailText }, null, 2), contentType: 'application/json', diff --git a/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/README.md b/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/README.md new file mode 100644 index 000000000..9df1ae284 --- /dev/null +++ b/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/README.md @@ -0,0 +1,54 @@ +# Docker phone update-cell correction — 5 September 2026 + +Mainline base: `5beeaf4cbe32603bf4d8dcb07069957a659aa1a4`. + +## Decision and scope + +The preceding access receipt demonstrates clipped update badges at 390×844, +with neither wheel nor keyboard horizontal input recovering them. Viewed its +retained screenshot again. Fresh external reading of W3C's Reflow guidance +(https://www.w3.org/WAI/WCAG22/Understanding/reflow.html, September 5) confirms +that a table exception does not justify losing cell information. This is not +a claim of WCAG conformance or new operator demand. + +Read the current pulse-pro FEATURE_REQUESTS.md from the supplied team's +20260905T170527Z-pro-customer checkout. The shipped resizable-columns entry +explicitly keeps phones responsive; it does not authorise extending desktop +manual scrolling to phones. No new surface or named product bet is introduced. + +The existing Docker update cell now reduces phone padding and wraps badge/action +content rather than clipping it. Labels, accessible names, handlers and review +requirements are unchanged. Detail headings wrap long identities instead of +ellipsising them (shared drawer, not only Docker). No overflow-container or +touch-handler change; the Android page-owned scrolling rule remains intact. + +## Validation + +- Focused Vitest: DockerNativeTables and ResourceDetailDrawer.docker-container: + 38 tests passed. +- Initial real-backend Chromium run: 390px reconnect and new text-bounds checks + passed. Screenshot inspection nevertheless found avoidable mid-word breaks; + cell padding was then reduced and a single-line Current-label assertion added. +- Final run: all six Chromium cases passed (390, 1100, 1440px; normal and + failed admission). Managed backend PID 2415019 stopped afterwards. Browser assertions inspect text ranges in all + rendered Docker update cells, assert clip/non-scrollable wrapper geometry, + preserve Enter expansion, and verify a non-ellipsised, fitting detail heading. + The populated synthetic fixture must include a visible Current status. + +Reproduce from repository root (locked packages and pinned Chromium installed): + +```sh +pulse-heavy-run -- env PULSE_E2E_USE_LOCAL_BACKEND=1 PULSE_E2E_SKIP_PLAYWRIGHT_INSTALL=1 PULSE_MOCK_MODE=true PULSE_E2E_NAVIGATION_RECOVERY=1 PULSE_E2E_TABLE_ACCESS=1 PULSE_E2E_LOCAL_BACKEND_PORT=18765 npm --prefix tests/integration test -- tests/96-navigation-socket-recovery.spec.ts --project=chromium +``` + +No full repository suite, physical Android touch, screen-reader, installed +release or every asynchronous action-state qualification. Full names remain +available by expanding the existing row; overview names remain truncated. +Review and integration are still required; nothing was published or deployed. + +Final screenshots were viewed: Current and Update fit without mid-word breaks +at 390px, and notifications-worker wraps fully in the expanded heading. +Screenshots contain only synthetic local inventory. The final run rebuilt the +embedded frontend and core backend through pulse-heavy-run. Wider cases protect +navigation recovery; phone text-bound assertions run in the 390px normal-admission +case. The fixture does not guarantee every update error/progress state. diff --git a/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/result.txt b/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/result.txt new file mode 100644 index 000000000..5fbd0750b --- /dev/null +++ b/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/result.txt @@ -0,0 +1,20 @@ +Running 6 tests using 1 worker + +(node:2415548) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use `node --trace-warnings ...` to show where the warning was created) +(node:2415548) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set. +(Use `node --trace-warnings ...` to show where the warning was created) + ✓ 1 [chromium] › tests/96-navigation-socket-recovery.spec.ts:79:5 › populated navigation survives socket loss at 1440px (admission failure: false) (8.3s) + ✓ 2 [chromium] › tests/96-navigation-socket-recovery.spec.ts:79:5 › populated navigation survives socket loss at 1100px (admission failure: false) (9.8s) + ✓ 3 [chromium] › tests/96-navigation-socket-recovery.spec.ts:79:5 › populated navigation survives socket loss at 390px (admission failure: false) (16.2s) + ✓ 4 [chromium] › tests/96-navigation-socket-recovery.spec.ts:79:5 › populated navigation survives socket loss at 1440px (admission failure: true) (6.5s) + ✓ 5 [chromium] › tests/96-navigation-socket-recovery.spec.ts:79:5 › populated navigation survives socket loss at 1100px (admission failure: true) (7.1s) + ✓ 6 [chromium] › tests/96-navigation-socket-recovery.spec.ts:79:5 › populated navigation survives socket loss at 390px (admission failure: true) (13.9s) + + 6 passed (1.0m) + +To open last HTML report run: + + npx playwright show-report + +[integration] Stopped managed local backend diff --git a/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/table-after-horizontal-wheel.png b/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/table-after-horizontal-wheel.png new file mode 100644 index 000000000..57be30f43 Binary files /dev/null and b/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/table-after-horizontal-wheel.png differ diff --git a/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/table-full-detail-heading.png b/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/table-full-detail-heading.png new file mode 100644 index 000000000..89637d250 Binary files /dev/null and b/tests/qualification/navigation-reconnect/2026-09-05-phone-layout/table-full-detail-heading.png differ