diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index 9cb55650f..a1b1bcfab 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -1239,10 +1239,11 @@ not a replacement status card, CTA band, or page-local nested card. drawer-width `justify-between` rows on desktop. This contract also covers secondary drawer facts in availability status and suggestion cards, resource change-history entries, Docker/PBS/PMG service - support panels, and Docker container-update management cards. Headers, - status summaries, actions, disk capacity summaries, and RAID state pairs may - retain intentional endpoint alignment; ordinary label/value facts in those - surfaces may not. + support panels, Docker container-update management cards, action history, + Discovery summaries, and the specialized Proxmox Mail Gateway drawer. + Headers, status summaries, actions, disk capacity summaries, and RAID state + pairs may retain intentional endpoint alignment; ordinary label/value facts + in those surfaces may not. Read-only metadata chips belong to `MetadataBadge` and domain wrappers over it. Organization role and share-status chips must use `OrganizationRoleBadge` and `OrganizationShareStatusBadge`, so role/status diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 4b287deb8..744326178 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -7616,6 +7616,7 @@ "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx", "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx", "frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx", "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx", "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx", "frontend-modern/src/features/proxmox/proxmoxPageModel.ts", @@ -7815,6 +7816,7 @@ "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx", "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx", "frontend-modern/src/features/proxmox/proxmoxHostTableModel.ts", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx", "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx", "frontend-modern/src/features/proxmox/ProxmoxNodesTable.tsx", "frontend-modern/src/features/proxmox/proxmoxPageModel.ts", @@ -7889,6 +7891,7 @@ "frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupServersTable.test.ts", "frontend-modern/src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx", "frontend-modern/src/features/proxmox/__tests__/ProxmoxCoverageTable.test.tsx", + "frontend-modern/src/features/proxmox/__tests__/ProxmoxMailGatewayTable.test.ts", "frontend-modern/src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx", "frontend-modern/src/features/proxmox/__tests__/proxmoxPageModel.test.ts", "frontend-modern/src/features/proxmox/__tests__/ProxmoxPageSurface.contract.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 c83bf6ee5..b10f895a3 100644 --- a/docs/release-control/v6/internal/subsystems/unified-resources.md +++ b/docs/release-control/v6/internal/subsystems/unified-resources.md @@ -268,6 +268,7 @@ about the same disk cannot diverge. 125. `frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx` 126. `frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx` 127. `frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx` +127a. `frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx` 128. `frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx` 129. `frontend-modern/src/features/proxmox/ProxmoxRecoverableTable.tsx` 130. `frontend-modern/src/features/proxmox/ProxmoxReplicationTable.tsx` @@ -1331,9 +1332,11 @@ desktop rows use the shared fixed label track so labels and values remain visually adjacent instead of spanning the full drawer width. The same boundary applies to availability facts, resource change-history metadata, Docker/PBS/PMG service facts, nested PMG queue/mail breakdowns, and -Docker container-update management facts. Their headers, actions, and compact -status summaries remain feature-owned, while ordinary label/value rows compose -`InfoCardKeyValueRow` rather than restoring a drawer-width flex split. +Docker container-update management facts. It also covers action-history facts, +Discovery facts, and the specialized `ProxmoxMailGatewayDrawer.tsx` inbound +statistics. Their headers, actions, and compact status summaries remain +feature-owned, while ordinary label/value rows compose `InfoCardKeyValueRow` +rather than restoring a drawer-width flex split. Curated technical inventory follows the shared compact-row contract instead of the secondary-card contract. Docker-host drawers must project system, runtime, memory, storage, and telemetry facts through diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 534cd0dec..2233b130d 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,38 +1,32 @@ { "version": 1, - "base_sha": "478a39bdec4b3a20f309490a564bcaca8e423a34", - "verified_at": "2026-08-27T20:06:10Z", + "base_sha": "4b9abd4eca5a1399a2386814d98eeadded198e45", + "verified_at": "2026-08-27T20:10:44Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx", - "frontend-modern/src/features/alerts/alertsConfigurationModel.ts", - "frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx", - "frontend-modern/src/features/alerts/thresholds/types.ts", - "frontend-modern/src/features/alerts/useAlertsConfigurationSnapshotState.ts", - "frontend-modern/src/types/alerts.ts" + "frontend-modern/src/components/Discovery/DiscoveryTab.tsx", + "frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx" ], "content_sha256": { - "frontend-modern/src/features/alerts/AlertsConfigurationSurface.tsx": "6e52a34562926185ae32483add8e303b0a8fae5c3d5315f5b87865f960f54929", - "frontend-modern/src/features/alerts/alertsConfigurationModel.ts": "adb7c745b8fdee148edf56beea8f1b7a02f30e6495d6fc6d7e2eff36f228d10c", - "frontend-modern/src/features/alerts/tabs/ThresholdsTab.tsx": "00e8e50cd3892262c9195129b28fc834d4dbc87adb37aeb6f652957869367ad8", - "frontend-modern/src/features/alerts/thresholds/types.ts": "d87f4509648798b55c01ac9e701faa2b9a96de7e25bf6453bfc10fbc5176e1f3", - "frontend-modern/src/features/alerts/useAlertsConfigurationSnapshotState.ts": "34fff486ecf33457f42cefeb0b04bbd4b3e04dad24faca8bd99bfd86a75e70f7", - "frontend-modern/src/types/alerts.ts": "ef4e8322a38e8443393f762ee4ba6c512ffd4ff18b3f32ff5fd64f5d3754585d" + "frontend-modern/src/components/Discovery/DiscoveryTab.tsx": "50839ded7dc62cbc3a9fb11fc5fc575acd1997ec9a4b0d7331fba22f0afb0ab4", + "frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx": "241e5f93f444b3838b80c05f6b4f78c4788931c760d01dc63f54ee5d4ee5270a", + "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx": "26faeb83e422b1d928385ce836e86a701ce544aa936b8de5bb38caee845bcb4d" }, - "routes": ["/alerts/thresholds/proxmox"], + "routes": ["/proxmox/mail", "/standalone/machines", "/docker/overview", "/actions"], "viewports": [ - { "width": 1440, "height": 900 }, + { "width": 1920, "height": 800 }, { "width": 390, "height": 844 } ], "states": [ - "CPU evaluation window loaded with the global five-minute rule and inherited platform overrides", - "mobile platform overrides collapsed and expanded", - "global rule changed to one minute with inherited labels updating immediately" + "mail-gateway-eu expanded with inbound disposition statistics visible", + "Apollo-114 and auth-service-01 management drawers inspected for Discovery and action-audit availability", + "Actions open and history views inspected for seeded action records" ], "interactions": [ - "confirmed the desktop three-column profile grid is readable without displacing threshold navigation", - "confirmed the narrow layout keeps the global rule visible and collapses platform overrides", - "expanded mobile platform overrides and confirmed every selector remains accessible", - "changed the global CPU window to one minute and confirmed inherited labels and unsaved-change state update" + "expanded mail-gateway-eu and confirmed Greylist, RBL, and average-process values remain adjacent to their labels on desktop", + "confirmed the three inbound statistic rows span the mobile card cleanly at 390px without overlap", + "repeated the PMG desktop check after the mobile span correction", + "inspected machine and Docker host drawers plus Actions history; the demo does not seed resource-level Discovery facts or action-audit drawer rows, so those responsive states are covered by rendered component tests" ] } diff --git a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx index 95c88e7d3..4e37e0e2d 100644 --- a/frontend-modern/src/components/Discovery/DiscoveryTab.tsx +++ b/frontend-modern/src/components/Discovery/DiscoveryTab.tsx @@ -32,7 +32,7 @@ import { } from '@/utils/resourceAnalysisPresentation'; import { ButtonLink, CopyValueButton } from '@/components/shared/Button'; import { CopyableCodeRow } from '@/components/shared/CopyableCodeRow'; -import { InfoCardFrame } from '@/components/shared/InfoCardFrame'; +import { InfoCardFrame, InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame'; import { useDiscoveryTabState } from './useDiscoveryTabState'; import { orderFactsByActionability } from './factOrdering'; import { deriveCliCommand } from './cliCommand'; @@ -1020,22 +1020,32 @@ export const DiscoveryTab: Component = (props) => {
{(fact) => ( -
- {fact.key} -
- - {fact.value} - - -
-
+ + + {fact.value} + + + + } + valueClass="flex items-center gap-1.5" + valueTitle={fact.value} + /> )}
diff --git a/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx b/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx index 16e2ed102..1a5d6da53 100644 --- a/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx +++ b/frontend-modern/src/components/Discovery/__tests__/DiscoveryTab.test.tsx @@ -226,7 +226,16 @@ describe('DiscoveryTab', () => { service_version: '16.1', category: 'database', cli_access: 'psql', - facts: [], + facts: [ + { + category: 'network', + key: 'Listen address', + value: '0.0.0.0:5432', + source: 'ss', + confidence: 0.95, + discovered_at: '2026-04-15T00:00:00Z', + }, + ], config_paths: [], data_paths: [], log_paths: [], @@ -255,6 +264,12 @@ describe('DiscoveryTab', () => { )); const runButton = await screen.findByRole('button', { name: 'Run Discovery' }); + const factRow = (await screen.findByText('Listen address')).parentElement; + expect(factRow).toHaveClass('justify-between', 'lg:grid', 'lg:grid-cols-[7rem_minmax(0,1fr)]'); + expect(screen.getByText('0.0.0.0:5432').parentElement).toHaveClass( + 'text-right', + 'lg:text-left', + ); await waitFor(() => expect(runButton).not.toBeDisabled()); fireEvent.click(runButton); diff --git a/frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx b/frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx index 1bcb8a173..a14fc47b6 100644 --- a/frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx +++ b/frontend-modern/src/components/Infrastructure/ResourceActionHistory.tsx @@ -1,6 +1,6 @@ import { For, Show } from 'solid-js'; import type { Component } from 'solid-js'; -import { InfoCardFrame } from '@/components/shared/InfoCardFrame'; +import { InfoCardFrame, InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame'; import type { ActionAuditRecord } from '@/types/actionAudit'; import { formatRelativeTime } from '@/utils/format'; import { @@ -55,27 +55,22 @@ const ActionHistoryRow: Component<{ audit: ActionAuditRecord }> = (props) => {
{props.audit.request.reason}
-
- Approval - - {formatActionApprovalPolicyLabel(props.audit.plan?.approvalPolicy)} - -
+ -
- Dry run - - {preflight()?.dryRunAvailable ? 'Available' : 'Not available'} - -
+
-
- Intent - - {preflight()?.intendedChange} - -
+
0}>
@@ -113,19 +108,20 @@ const ActionHistoryRow: Component<{ audit: ActionAuditRecord }> = (props) => { })()} -
{(fact) => ( -
-
{fact.label}
-
{fact.value}
-
+ )}
-
+
{(() => { diff --git a/frontend-modern/src/components/Infrastructure/__tests__/ResourceActionHistory.verification.test.ts b/frontend-modern/src/components/Infrastructure/__tests__/ResourceActionHistory.verification.test.ts index 63e8ae244..320530f91 100644 --- a/frontend-modern/src/components/Infrastructure/__tests__/ResourceActionHistory.verification.test.ts +++ b/frontend-modern/src/components/Infrastructure/__tests__/ResourceActionHistory.verification.test.ts @@ -70,6 +70,13 @@ describe('ResourceActionHistory verification rendering', () => { ); const actionHistory = within(screen.getByTestId('resource-action-history-section')); + const approvalRow = actionHistory.getAllByText('Approval')[0]?.parentElement; + expect(approvalRow).toHaveClass( + 'justify-between', + 'lg:grid', + 'lg:grid-cols-[7rem_minmax(0,1fr)]', + ); + expect(approvalRow?.lastElementChild).toHaveClass('text-right', 'lg:text-left'); expect(actionHistory.getAllByText('Legacy check passed (source unclassified)')).toHaveLength(1); expect(actionHistory.getByText("systemctl is-active 'nginx'")).toBeInTheDocument(); expect(actionHistory.queryByText('should not render')).toBeNull(); diff --git a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts index 6364fec79..82daa3029 100644 --- a/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts +++ b/frontend-modern/src/components/shared/SharedPrimitives.guardrails.test.ts @@ -1247,6 +1247,18 @@ describe('shared primitive guardrails', () => { /class="flex items-(?:center|start) justify-between gap-2">\s*/, ); } + + for (const consumerPath of [ + 'src/components/Discovery/DiscoveryTab.tsx', + 'src/components/Infrastructure/ResourceActionHistory.tsx', + 'src/features/proxmox/ProxmoxMailGatewayDrawer.tsx', + ]) { + const source = readFrontendSource(consumerPath); + expect(source).toContain('InfoCardKeyValueRow'); + expect(source).not.toMatch( + /class="flex items-(?:baseline|center|start) justify-between[^\"]*">\s*<(?:span|dt) class="[^\"]*text-muted/, + ); + } }); it('keeps shared subtabs as one primitive and leaves shell styling to owning surfaces', () => { diff --git a/frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx b/frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx index 1a0b9fa00..378691f1e 100644 --- a/frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx +++ b/frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx @@ -1,6 +1,7 @@ import { For, Show, createMemo, createResource, type Component } from 'solid-js'; import { Card } from '@/components/shared/Card'; import { DrawerSubjectHeading } from '@/components/shared/DrawerSubjectHeading'; +import { InfoCardKeyValueRow } from '@/components/shared/InfoCardFrame'; import { ObjectDrawerHeader } from '@/components/shared/ObjectDrawerHeader'; import { StatusDot } from '@/components/shared/StatusDot'; import { TableCell, TableHead, TableRow } from '@/components/shared/Table'; @@ -396,34 +397,28 @@ export const ProxmoxMailGatewayDrawer: Component<{ outValue={stats()!.bytesOut ?? 0} format={(v) => (v > 0 ? formatBytes(v) : '—')} /> -
-
- Greylist / Junk in - - {formatPlatformTableIntegerValue(stats()!.greylistCount)} /{' '} - {formatPlatformTableIntegerValue(stats()!.junkIn)} - -
-
-
-
- RBL / Pregreet rejects - - {formatPlatformTableIntegerValue(stats()!.rblRejects)} /{' '} - {formatPlatformTableIntegerValue(stats()!.pregreetRejects)} - -
-
-
-
- Avg process - - {stats()?.averageProcessTimeMs - ? `${Math.round(stats()!.averageProcessTimeMs)}ms` - : '—'} - -
-
+ + +
diff --git a/frontend-modern/src/features/proxmox/__tests__/ProxmoxMailGatewayTable.test.ts b/frontend-modern/src/features/proxmox/__tests__/ProxmoxMailGatewayTable.test.ts index 37a704d6f..0347920fd 100644 --- a/frontend-modern/src/features/proxmox/__tests__/ProxmoxMailGatewayTable.test.ts +++ b/frontend-modern/src/features/proxmox/__tests__/ProxmoxMailGatewayTable.test.ts @@ -6,8 +6,19 @@ import { MAIL_GATEWAY_PHONE_COLUMNS, MAIL_GATEWAY_PHONE_COLUMN_WIDTHS, } from '../ProxmoxMailGatewayTable'; +import mailGatewayDrawerSource from '../ProxmoxMailGatewayDrawer.tsx?raw'; describe('ProxmoxMailGatewayTable phone presentation', () => { + it('keeps compact drawer statistics on the shared responsive row contract', () => { + expect(mailGatewayDrawerSource).toContain('InfoCardKeyValueRow'); + expect(mailGatewayDrawerSource).not.toMatch( + /class="flex items-baseline justify-between[^\"]*">\s*/, + ); + expect( + mailGatewayDrawerSource.match(/class="col-span-2 text-\[11px\] sm:col-span-1"/g), + ).toHaveLength(3); + }); + it('keeps fleet context and mail-flow counters visible without scrolling', () => { expect(MAIL_GATEWAY_PHONE_COLUMNS).toEqual([ 'instance',