From 4819afb0827455369ca3ff31685c9eaa88b3bcd7 Mon Sep 17 00:00:00 2001 From: rcourtman <8825017+rcourtman@users.noreply.github.com> Date: Mon, 5 Oct 2026 22:08:34 +0100 Subject: [PATCH] Keep Actions target names whole on narrow cards MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Each Actions card's second line shows the target name, its type and the time. Name and type were sibling flex items that shrank together, so on a phone the line read "analytics-ba... · Virtual ma..." and hid which resource the action touches, which is what the operator needs before opening the review. From 360 to 430px every target name was cut. The name and the type now sit in their own row where the name keeps its width (capped at the line) and the type, with its separator, takes whatever is left. Names read in full at every width from 360px (the box now equals the text width at 1x, 2x and 3x pixel ratio), a name longer than the line still truncates with the time visible, and nothing changes from 480px up. --- .../internal/subsystems/unified-resources.md | 8 +- .../2294fd5a84fdc31a.json | 62 +++++++++++++ .../__tests__/Actions.metaLine.test.tsx | 89 +++++++++++++++++++ frontend-modern/src/pages/Actions.tsx | 20 +++-- 4 files changed, 170 insertions(+), 9 deletions(-) create mode 100644 frontend-modern/browser-verification/2294fd5a84fdc31a.json create mode 100644 frontend-modern/src/features/actions/__tests__/Actions.metaLine.test.tsx diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 7269935c42..0db618b95a 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -3568,8 +3568,12 @@ not part of `ActionRequest`, durable plan identity, or `planHash`; a resource rename therefore cannot change action authority. Opaque canonical resource IDs remain in the row's accessible name and title, while the visual row prefers the API-supplied name and type and falls back to the bounded ID-derived type plus -short suffix only when the resource projection is unavailable. Read-only demo -posture is quiet supporting context rather than a page-level callout. The +short suffix only when the resource projection is unavailable. When the row is +too narrow for both, the resource name keeps its width and only the type, with +its separator, gives way: on a phone they used to shrink together, so the row +read "analytics-ba… · Virtual ma…" and hid which resource the action touches. +Read-only demo posture is quiet supporting context rather than a page-level +callout. The empty Open queue must explain producer state rather than promising content: when the effective Patrol mode is Watch only (read via the canonical `/api/ai/patrol/autonomy` settings read, failing closed to the generic calm diff --git a/frontend-modern/browser-verification/2294fd5a84fdc31a.json b/frontend-modern/browser-verification/2294fd5a84fdc31a.json new file mode 100644 index 0000000000..483e099d23 --- /dev/null +++ b/frontend-modern/browser-verification/2294fd5a84fdc31a.json @@ -0,0 +1,62 @@ +{ + "version": 1, + "base_sha": "1dcf9418aae87f9427dbfc6bcae67dbde114bf8a", + "verified_at": "2026-10-05T21:08:04Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/pages/Actions.tsx" + ], + "content_sha256": { + "frontend-modern/src/pages/Actions.tsx": "17ded57aa6d0476a01c4d0cfec5dee8c9fed2be0fbf50c3d3f5cd2e40c278de6" + }, + "routes": [ + "/actions", + "/actions/history" + ], + "viewports": [ + { + "width": 360, + "height": 844 + }, + { + "width": 390, + "height": 844 + }, + { + "width": 430, + "height": 844 + }, + { + "width": 480, + "height": 844 + }, + { + "width": 640, + "height": 844 + }, + { + "width": 768, + "height": 844 + }, + { + "width": 1024, + "height": 844 + }, + { + "width": 1440, + "height": 844 + } + ], + "states": [ + "Private mock stack (PULSE_MOCK_MODE) serving this tree on main 1dcf9418aa; before = main, after = main plus this change, measured in the same session on the three open demo actions", + "Main: the target name and its type shrank together, so from 360 to 430 every name was cut (analytics-batch-01 61-98 of 107px, backup-coordinator 67-107 of 112px) beside a cut type (Virtual ma\u2026, App conta\u2026); from 480 everything fit", + "Patched: the name keeps its full width at every width (box equals text width, 106.61px for analytics-batch-01, at 1x, 2x and 3x device pixel ratio), and the type with its separator takes the rest (Virtual machine 21px at 360, 51px at 390, 91px at 430, whole from 480); nothing changes from 480 up", + "A name longer than the line (very-long-resource-name-on-a-production-cluster-node-07, injected) truncates inside the line at 360 and 390 with the relative time still fully visible, and fits whole at 768", + "Before and after element screenshots of the three cards at 360, 390 and 1024 at 3x density" + ], + "interactions": [ + "Loaded /actions fresh at 390 (mobile emulation) and resized through each viewport before measuring", + "Opened the second card's review at 390 and 1024 (Governed action review, analytics-batch-01 \u00b7 Virtual machine) and closed it with Escape", + "Switched to the History tab (/actions/history, 3 rows); no console errors" + ] +} diff --git a/frontend-modern/src/features/actions/__tests__/Actions.metaLine.test.tsx b/frontend-modern/src/features/actions/__tests__/Actions.metaLine.test.tsx new file mode 100644 index 0000000000..1c717a3e4f --- /dev/null +++ b/frontend-modern/src/features/actions/__tests__/Actions.metaLine.test.tsx @@ -0,0 +1,89 @@ +import { cleanup, render, screen } from '@solidjs/testing-library'; +import { Route, Router } from '@solidjs/router'; +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { ResourceActionsAPI } from '@/api/resourceActions'; +import { SecurityAPI } from '@/api/security'; +import type { ActionAuditRecord } from '@/types/actionAudit'; +import { getActionResourcePresentation } from '../actionPresentation'; +import Actions from '../../../pages/Actions'; + +vi.mock('@/api/resourceActions', () => ({ + ResourceActionsAPI: { listActions: vi.fn(), getAction: vi.fn() }, +})); +vi.mock('@/api/security', () => ({ SecurityAPI: { getStatus: vi.fn() } })); +vi.mock('@/api/patrol', () => ({ getPatrolAutonomySettings: vi.fn().mockResolvedValue({}) })); + +const resourceId = 'docker:container:analytics-batch-01'; + +const audit: ActionAuditRecord = { + id: 'restart-analytics', + createdAt: '2026-09-29T18:00:00Z', + updatedAt: '2026-09-29T18:01:00Z', + state: 'executing', + decisionRevision: 1, + request: { + requestId: 'request-restart-analytics', + resourceId, + capabilityName: 'restart', + reason: 'Recover checkout after three failed health checks', + requestedBy: 'operator', + }, + plan: { + actionId: 'restart-analytics', + requestId: 'request-restart-analytics', + allowed: true, + requiresApproval: false, + approvalPolicy: 'none', + rollbackAvailable: false, + expiresAt: '2026-09-29T21:00:00Z', + planHash: 'sha256:plan-restart-analytics', + }, + verificationOutcome: { status: 'unknown' }, +}; + +beforeEach(() => { + window.history.replaceState(null, '', '/'); + vi.mocked(ResourceActionsAPI.listActions).mockResolvedValue({ + actions: [audit], + count: 1, + view: 'pending', + readOnly: false, + }); + vi.mocked(SecurityAPI.getStatus).mockResolvedValue({ + hasAuthentication: true, + requiresAuth: true, + settingsCapabilities: { authenticationWrite: false }, + } as Awaited>); +}); + +afterEach(() => { + cleanup(); + vi.clearAllMocks(); + window.history.replaceState(null, '', '/'); +}); + +describe('Actions card resource line', () => { + it('keeps the target name whole and lets its type give way on narrow cards', async () => { + const { label, detail } = getActionResourcePresentation(resourceId); + expect(label).toBe('analytics-batch-01'); + expect(detail).not.toBe(''); + + render(() => ( + + + + )); + + // On a phone the name and its type used to shrink together, so the line + // read "analytics-ba… · Virtual ma…" and hid which resource the action + // touches. The name never shrinks below its own width; the type and its + // separator take whatever room is left. + const name = await screen.findByText(label); + expect(name).toHaveClass('shrink-0', 'max-w-full', 'truncate'); + const type = screen.getByText(detail).parentElement!; + expect(type).toHaveClass('min-w-0', 'flex-1', 'truncate'); + expect(type.querySelector('[aria-hidden="true"]')).toHaveTextContent('·'); + expect(type.parentElement).toBe(name.parentElement); + expect(name.parentElement?.closest(`[title="${resourceId}"]`)).not.toBeNull(); + }); +}); diff --git a/frontend-modern/src/pages/Actions.tsx b/frontend-modern/src/pages/Actions.tsx index 1294ba55df..efa47b51d6 100644 --- a/frontend-modern/src/pages/Actions.tsx +++ b/frontend-modern/src/pages/Actions.tsx @@ -334,13 +334,19 @@ export function Actions() { class="mt-1 flex min-w-0 items-center gap-1.5 text-xs text-muted" title={action.request.resourceId} > - {resource().label} - - - - {resource().detail} - - + {/* The name says what the action touches, so it keeps its + width and only the type, with its separator, gives way. */} + + {resource().label} + + + + {resource().detail} + + + {formatRelativeTime(action.updatedAt)}