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)}