diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index c866d04df2..6ac37f5e92 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2340,6 +2340,13 @@ not a replacement status card, CTA band, or page-local nested card. disclosure; a non-expandable identity cell must otherwise expose its complete value without requiring hover. Default data rows stay single-line so scan density and row rhythm are not traded for automatic text wrapping. + A summary value that must truncate sits in a `block truncate` span as the + cell's only child, so it fills the fixed-layout column and ends in an + ellipsis inside it on the row's text line. A capped `inline-block max-w-* + truncate` span ignores the column width: it spills past a narrower cell + with no ellipsis, wastes room in a wider one, and sits about 2 pixels above + the row baseline. `platformOverviewLayout.guardrails.test.ts` holds the + Docker, Kubernetes, Proxmox and vSphere table sources to that rule. When a platform table row itself owns that keyboard- and touch-operable disclosure, `SummaryRowActionButton` removes the redundant visible chevron below the shared mobile breakpoint while preserving the button for screen @@ -5184,6 +5191,13 @@ owned title so feature drawers can place web-interface controls inside a larger access surface without forking the save/remove/runtime behavior. Future web-interface URL work should extend those owners instead of pushing metadata transport or validation back into the shared shell. +`WebInterfaceUrlField.tsx` renders read-only when +`sessionCanWriteMonitoringMetadata()` is false. That mirrors the backend +metadata write gate: browser sessions always pass, while API-token sessions +such as kiosk links need `monitoring:write` or the wildcard scope. Read-only +sessions keep the saved URL with its open and copy controls and a one-line +explanation, but get no input, Save, Remove, or suggested-URL adoption that the +server would refuse. Missing-suggested-URL diagnostics remain useful only when the operator has no saved or entered URL; once a custom web-interface URL is present, the shared field must suppress "no suggested URL" warnings so Discovery does not make a diff --git a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md index 8e1892536b..8610516f3c 100644 --- a/docs/release-control/v6/internal/subsystems/performance-and-scalability.md +++ b/docs/release-control/v6/internal/subsystems/performance-and-scalability.md @@ -1175,6 +1175,13 @@ change may globally weaken the Task 03 lifecycle-state idempotency invariant. keep unchanged row objects across inventory ticks so an open editor never remounts the input being typed in. `WorkloadsSurface.performance.contract.test.tsx` pins that wiring. + The editor takes a source rather than guests: workload tables pass live + guests and metadata so the row model loads only with the panel, while the + Docker containers table passes ready rows and lazy-loads the trigger so + it does not merge into the chunk the Docker and workloads pages share. + Saves run one metadata write per changed link, then dispatch the + metadata-changed events together so whole-snapshot listeners coalesce + onto one in-flight refetch instead of refetching per link. Drawer history charts belong to `frontend-modern/src/components/Workloads/GuestDrawerHistory.tsx`. A current metric may remain visible in a chart legend only when labelled `current`; it must never be expanded into synthetic timestamps or a flat diff --git a/docs/release-control/v6/internal/subsystems/storage-recovery.md b/docs/release-control/v6/internal/subsystems/storage-recovery.md index ed3f3e4803..dd5250c085 100644 --- a/docs/release-control/v6/internal/subsystems/storage-recovery.md +++ b/docs/release-control/v6/internal/subsystems/storage-recovery.md @@ -3094,6 +3094,17 @@ presentation and sort change only; it adds no backup, restore, or retention authority. Pinned by `proxmoxBackupRecoveryModel.test.ts` and `ProxmoxCoverageTable.test.tsx`. +### Backup table values truncate inside their own column + +The coverage Node cell, the restore-evidence Location and Details cells, and +the recoverable table's Location and Details cells render their value in a +block span that fills the fixed-layout cell, so a long node name, datastore path +or detail ends in an ellipsis inside its column on the row's text line, with the +full value on the title. A capped inline-block span let a long detail run past +its cell with no ellipsis and sat above the row. This is presentation only; it +adds no backup, restore, or retention authority. Pinned by +`ProxmoxCoverageTable.test.tsx` and `platformOverviewLayout.guardrails.test.ts`. + ### Retained Patrol objectives do not create recovery authority The shared `internal/api` retained-objective endpoints may scope an outcome to diff --git a/docs/release-control/v6/internal/subsystems/unified-resources.md b/docs/release-control/v6/internal/subsystems/unified-resources.md index 29c8e89539..e2af35bd29 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -1212,7 +1212,12 @@ RBAC names first, then the namespace scope, then the short Lifecycle and Data shape values, then Kind. Kubernetes summary cells truncate through a block span that fills the cell, so a long value ends in an ellipsis inside its own column on the row's text line; a fixed-width inline-block cap spilled past narrow -cells without an ellipsis and sat above the row baseline. +cells without an ellipsis and sat above the row baseline. Docker image, network, +volume, service, task, secret and config cells, Proxmox backup coverage, +restore-evidence and recoverable cells, the Mail Gateway drawer's top domains, +and vSphere host vCenter cells follow the same rule. The Docker service Update +cell still wraps on phones and from md truncates inside its column instead of +at a 10rem cap. Duration and interval cells keep the same split: unified-resource or source-specific consumers own which elapsed duration, human fallback, or poll interval field is meaningful, while dense platform table rendering must use @@ -1453,6 +1458,12 @@ pod UIDs, and generated registry IDs remain fallback migration, action, discovery, or metrics coordinates and must not be the primary metadata write key. Missing cluster/host, namespace, kind, or name scope must fail closed to the legacy resource key rather than constructing a broadened stable key. +The Docker containers table's bulk web-link editor is a third writer of these +records. It keys every row through `toDiscoveryConfig`, the container drawer's +own metadata target, and lists only rows whose target is guest metadata, so +bulk and drawer saves for a container read and write the same +`app-container::name:` record and the editor never +falls back to a runtime-ID key. Product-originated resource references may arrive as registered unified resource IDs, source-specific IDs, or canonical identity aliases. The unified-resource registry owns resolving those references through diff --git a/frontend-modern/browser-verification/737e5f5d4595888b.json b/frontend-modern/browser-verification/737e5f5d4595888b.json new file mode 100644 index 0000000000..172f9537e5 --- /dev/null +++ b/frontend-modern/browser-verification/737e5f5d4595888b.json @@ -0,0 +1,90 @@ +{ + "version": 1, + "base_sha": "914fabfc1fef6d728bf145cf3097557ef82eafe7", + "verified_at": "2026-10-05T13:10:50Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/features/docker/DockerConfigsTable.tsx", + "frontend-modern/src/features/docker/DockerImagesTable.tsx", + "frontend-modern/src/features/docker/DockerNetworksTable.tsx", + "frontend-modern/src/features/docker/DockerSecretsTable.tsx", + "frontend-modern/src/features/docker/DockerServicesTable.tsx", + "frontend-modern/src/features/docker/DockerTasksTable.tsx", + "frontend-modern/src/features/docker/DockerVolumesTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx", + "frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx" + ], + "content_sha256": { + "frontend-modern/src/features/docker/DockerConfigsTable.tsx": "5e5aa4ec47bff613fa1e61fffc6e5f78fbb52a11e74fd692f632f7d9f39c68ed", + "frontend-modern/src/features/docker/DockerImagesTable.tsx": "dad1a0a668160d0590d376d35da94f0e2e500965852631fff40e85a09ece62c1", + "frontend-modern/src/features/docker/DockerNetworksTable.tsx": "a4fbc6cfe58dabb7e453e0e65b6bc6aeb72bcc05945fb68a6571e503f44722c9", + "frontend-modern/src/features/docker/DockerSecretsTable.tsx": "3a68466cc647a84f4bc7cf9d9b6184ebf0476b9f21d05b9329ae400cfd9f7460", + "frontend-modern/src/features/docker/DockerServicesTable.tsx": "aa3cf92e9aeb611027cec10dc3596e81907732e20930c06ba5d9275451f42c1e", + "frontend-modern/src/features/docker/DockerTasksTable.tsx": "02435077ae71767e6f6db6bc0f3441a585661013dd62c35892e48229cc53e7b6", + "frontend-modern/src/features/docker/DockerVolumesTable.tsx": "020345e4bc6c9a4389410cbfbe566aa34dae8ea7c702f65246bdfb75ce9e3e96", + "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx": "b16eaab7cc00f13a858b290f453e46ee1886c1e50a9a542999f4568a0d42aead", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx": "18c6dbea9038cc6561f5f914f61e67954abc4a3a93f83560b76398ae2915b7ba", + "frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx": "f8f3695bbd200fa8a8f7091ec6431faa1dada5fbdbb98b80b9fc8c5f7ede304b", + "frontend-modern/src/features/vmware/VsphereHostsTable.tsx": "e6caa80836b45e9509d450e6816645062adc1cf6ce29041810e5daf70491ac6f" + }, + "routes": [ + "/docker/images", + "/docker/networks", + "/docker/storage", + "/docker/swarm", + "/proxmox/backups/date", + "/proxmox/backups/coverage", + "/proxmox/mail", + "/vmware/overview" + ], + "viewports": [ + { + "width": 390, + "height": 1000 + }, + { + "width": 600, + "height": 1000 + }, + { + "width": 700, + "height": 1000 + }, + { + "width": 768, + "height": 1000 + }, + { + "width": 900, + "height": 1000 + }, + { + "width": 1100, + "height": 1000 + }, + { + "width": 1280, + "height": 1000 + }, + { + "width": 1440, + "height": 1000 + } + ], + "states": [ + "Live DOM probe on main at 700 and 1440 over every changed span (Docker images, networks, volumes, services, tasks, secrets, configs; Proxmox recoverable, coverage Node and restore-evidence Location/Details, Mail Gateway drawer Top domains; vSphere hosts vCenter): all 1000 measured spans are the only child of their cell and every table, including the drawer's Top domains table, computes table-layout fixed", + "Main: 66 spans ran past their cell with no ellipsis (Service Image 225px and Secret Labels 184px at 700, Mountpoint 51px and restore-evidence Details 22px at 1440, PMG domains 71px at 700), and every inline-block span sat 2 to 2.5px above the row's text line", + "Patched: no span runs past its cell, all 134 truncated values end in an ellipsis inside their own column, and each value sits on the row's text line (0px; the 11px restore-evidence rows read 0.5px)", + "Docker service Update cell still wraps Rollback started onto two lines below md (700) and truncates inside its column from md (1440) instead of at a 10rem cap", + "Clip sweep (scrollWidth over clientWidth on every cell, screen-reader text excluded) across all eight routes at 390 to 1440: clipped cells 931 on main and 928 patched over 19209 measured cells, no column worse at any width (Secret Labels, Mountpoint and Used by each one better)", + "Before and after element screenshots of all eleven affected tables at 700 and 1440 show hard cuts such as registry.exa and mail-relay.customer-opera on main becoming registry... and mail-relay.customer-op... inside the cell, with Created and Mountpoint back on the row line", + "Mock PMG reports no domain stats, so the drawer's Top domains table was mounted by adding eight domains (4 to 54 characters) to the real /api/pmg/instances response through a Playwright route intercept" + ], + "interactions": [ + "Expanded the first three Proxmox coverage rows to mount the restore-evidence table", + "Opened the first Mail Gateway row's drawer to mount the Top domains table", + "Loaded each Docker, Proxmox backups (by date and coverage) and vSphere overview tab fresh at every viewport and waited for the row count to settle before measuring" + ] +} diff --git a/frontend-modern/browser-verification/e8946b639f30703c.json b/frontend-modern/browser-verification/e8946b639f30703c.json new file mode 100644 index 0000000000..aa697fe0bc --- /dev/null +++ b/frontend-modern/browser-verification/e8946b639f30703c.json @@ -0,0 +1,54 @@ +{ + "version": 1, + "base_sha": "914fabfc1fef6d728bf145cf3097557ef82eafe7", + "verified_at": "2026-10-05T13:09:31Z", + "result": "passed", + "changed_paths": [ + "frontend-modern/src/components/Workloads/WorkloadWebLinksAction.tsx", + "frontend-modern/src/components/Workloads/WorkloadWebLinksDialog.tsx", + "frontend-modern/src/components/Workloads/WorkloadsSurface.tsx", + "frontend-modern/src/components/Workloads/useWorkloadWebLinksState.ts", + "frontend-modern/src/components/Workloads/workloadWebLinksModel.ts", + "frontend-modern/src/components/shared/WebInterfaceUrlField.tsx", + "frontend-modern/src/features/docker/DockerContainersTable.tsx", + "frontend-modern/src/stores/sessionCapabilities.ts", + "frontend-modern/src/stores/sessionPresentationPolicy.ts" + ], + "content_sha256": { + "frontend-modern/src/components/Workloads/WorkloadWebLinksAction.tsx": "319ff6e1265c7383699a0d916e0256a3d55d3844c06b33577197aa13f975e2e8", + "frontend-modern/src/components/Workloads/WorkloadWebLinksDialog.tsx": "198f5688928616f5ef6f3b2fbfdfd95ab9ae54bc62a46faaa0067cabac46c2c6", + "frontend-modern/src/components/Workloads/WorkloadsSurface.tsx": "e8bdb9ba10380b569366231a4e1a8324078c1ac5140ca3a0597d09e6f75689a1", + "frontend-modern/src/components/Workloads/useWorkloadWebLinksState.ts": "5c84b588fdbe003d1afb3d328aec05635c3e2f4c42a5bb543cb836f121f38389", + "frontend-modern/src/components/Workloads/workloadWebLinksModel.ts": "5aa7607047001ad01a5090a06d714c7a30a1e77f446660fb25d9954346166342", + "frontend-modern/src/components/shared/WebInterfaceUrlField.tsx": "bf904f63e28ee27e571b55d48595b33058231686e4634c66fee248dd15dee784", + "frontend-modern/src/features/docker/DockerContainersTable.tsx": "4bcbc9060d78a78b4c2371cc2d5822bbf0ac01bd69f5ae476c7f050a6cbe5d1a", + "frontend-modern/src/stores/sessionCapabilities.ts": "77da62c75db732e85c78c5a7f60d53ac6bff5058e34ea14783bcafc27ac0636a", + "frontend-modern/src/stores/sessionPresentationPolicy.ts": "ed40857a67062002721738ad51e3a7a6c909c384f28a74569ca21f3aa695f648" + }, + "routes": [ + "/docker", + "/proxmox" + ], + "viewports": [ + { + "width": 1280, + "height": 800 + }, + { + "width": 375, + "height": 812 + } + ], + "states": [ + "Private mock stack from this tree (backend :7701, Vite :5221): Docker page with 7 containers across a Docker host and Docker-in-LXC guests; Proxmox page with 31 guests.", + "Docker containers header shows Edit links; the panel says 'for each container', lists all 7 containers with host and image detail, keyed app-container::name: exactly as toDiscoveryConfig resolves for the container drawer.", + "Saving two container links, then a mix of two additions and two removals, wrote each through PUT /api/guests/metadata, closed the panel and updated the table links; the network log showed exactly two /api/resources refetches per save regardless of how many links changed.", + "Token gate with the admin bypass off: a monitoring:read token saw the Proxmox guests (31 rows) and Docker containers without any Edit links action, and the guest drawer's Web Interface URL showed the saved URL, open and copy, and 'This session can open saved web interface URLs but not change them.' with no input, Save or Remove; the drawer header Open action stayed. A monitoring:read+write token saw Edit links and saved a container link successfully.", + "Phone 375x812: Docker editor full-width with 44px inputs and no horizontal overflow. Production build: WorkloadsSurface 69.08 kB, Docker 28.56 kB, DrawerDiskListCard unchanged at 6.08 kB, trigger 1.6 kB and panel 4.2 kB as separate lazy chunks; no bundle violations. Not physical-device, vSphere-estate or release acceptance.", + "Re-checked after the commit hook's Prettier pass reformatted two staged files (whitespace only): the Docker editor opened, saved inventory-api through PUT /api/guests/metadata, closed and the table gained the link." + ], + "interactions": [ + "Opened Edit links on the Docker page at 1280 and 375 widths; typed links, cleared links, submitted the form; reopened to confirm saved values.", + "Loaded the app as a read-only API token and as a write-scoped token (local test tokens set in session storage) and opened the Proxmox guest drawer Manage tab and the Docker containers header in each." + ] +} diff --git a/frontend-modern/src/components/Workloads/WorkloadWebLinksAction.tsx b/frontend-modern/src/components/Workloads/WorkloadWebLinksAction.tsx index aac6b56333..75a9901123 100644 --- a/frontend-modern/src/components/Workloads/WorkloadWebLinksAction.tsx +++ b/frontend-modern/src/components/Workloads/WorkloadWebLinksAction.tsx @@ -1,21 +1,23 @@ import { Show, Suspense, createSignal, lazy, type Component } from 'solid-js'; import Link2Icon from 'lucide-solid/icons/link-2'; import { TABLE_CARD_HEADER_CLEAR_BUTTON_CLASS } from '@/components/shared/TableCardHeader'; +import { sessionCanWriteMonitoringMetadata } from '@/stores/sessionCapabilities'; import { presentationPolicyIsReadOnly } from '@/stores/sessionPresentationPolicy'; -import type { WorkloadGuest } from '@/types/workloads'; -import type { WorkloadGuestMetadataMap } from './workloadGuestMetadataRecord'; -import type { WorkloadWebLinkDrafts } from './workloadWebLinksModel'; +import type { WorkloadWebLinkDrafts, WorkloadWebLinksSource } from './workloadWebLinksModel'; // The panel and its row model load on first open, so they stay out of the // WorkloadsSurface chunk and do no work while the editor is closed. const WorkloadWebLinksDialog = lazy(() => import('./WorkloadWebLinksDialog')); export interface WorkloadWebLinksActionProps { - guests: () => WorkloadGuest[]; - guestMetadata: () => WorkloadGuestMetadataMap; + source: WorkloadWebLinksSource; } -/** Table-header entry point for setting many guest web links at once. */ +/** + * Table-header entry point for setting many web links at once. Hidden where a + * save would be refused: read-only presentations and API-token sessions + * without `monitoring:write`. + */ export const WorkloadWebLinksAction: Component = (props) => { const [open, setOpen] = createSignal(false); // Drafts outlive the panel so closing it by accident keeps typed links. @@ -24,7 +26,7 @@ export const WorkloadWebLinksAction: Component = (p const preload = () => void WorkloadWebLinksDialog.preload(); return ( - + + state.setUrlValue(e.currentTarget.value)} + onKeyDown={(e) => { + if (e.key === 'Enter') { + void state.handleSaveUrl(); + } + }} + disabled={state.urlSaving()} + /> + + = (props size="lg" /> - +