From 0a58e77bc6017e7711936724017a91dc78a30fdd Mon Sep 17 00:00:00 2001 From: rcourtman Date: Mon, 24 Aug 2026 21:52:47 +0100 Subject: [PATCH] Eliminate remaining frontend template drift --- ...tform-canonicalization-audit-2026-08-24.md | 27 +++-- .../subsystems/frontend-primitives.md | 5 + frontend-modern/browser-verification.json | 108 ++++-------------- .../scripts/shared-template-registry.json | 30 ++++- .../Settings/RelayPairingSection.tsx | 10 +- .../__tests__/RelaySettingsPanel.test.ts | 2 + .../SharedPrimitives.guardrails.test.ts | 18 ++- .../patrol/PatrolAttentionWorkbench.tsx | 65 +++++------ .../features/patrol/PatrolObjectivesPanel.tsx | 49 ++++---- .../src/utils/relayPresentation.ts | 2 - 10 files changed, 142 insertions(+), 174 deletions(-) diff --git a/docs/release-control/v6/internal/records/frontend-platform-canonicalization-audit-2026-08-24.md b/docs/release-control/v6/internal/records/frontend-platform-canonicalization-audit-2026-08-24.md index 6462039f5..a2b1c30a9 100644 --- a/docs/release-control/v6/internal/records/frontend-platform-canonicalization-audit-2026-08-24.md +++ b/docs/release-control/v6/internal/records/frontend-platform-canonicalization-audit-2026-08-24.md @@ -146,14 +146,18 @@ were not artificially inferred from healthy pixels. excludes tests from runtime chevron/native-control rules, and retains the canonical-platform lint gate. -### P3 — unrelated pre-existing shared-template findings +### P3 — remaining shared-template findings resolved in follow-up -The complete shared-template audit still reports four consumers outside this -platform canonicalization slice: `ApprovalBanner`/`LoadingSpinner`, -`UpdatesSettingsPanel`/`FormSelect`, `RelayPairingSection`/`ExternalTextLink`, -and `PatrolAttentionWorkbench`/`FormSelect`. None is rendered by or newly -introduced in the audited platform inventory/drawer contract. They remain -explicit exceptions rather than being hidden by this slice. +The follow-up complete shared-template audit now passes without exceptions. +Two reported consumers were inaccurate ownership declarations: +`ApprovalBanner` has no loading state and `UpdatesSettingsPanel` has no native +select, so those paths were removed from the respective required-consumer +inventories. The two real findings were migrated: `RelayPairingSection` now +composes `ExternalTextLink`, while the Patrol suppression reason and duration +compose `FormTextarea` and `FormSelect`. A Patrol textarea guard now protects +that additional form-control boundary and identified the same latent fork in +`PatrolObjectivesPanel`; both objective textareas now compose `FormTextarea` as +well. ## Decisions for previously ambiguous contracts @@ -203,8 +207,7 @@ explicit exceptions rather than being hidden by this slice. ## Remaining exceptions -There are no known platform-page exceptions to the canonical table or summary -row interaction contracts after this slice. The four unrelated static-audit -findings listed above remain outside this task. Route-specific cells, metrics, -drawer sections, and technical details are intentional object content inside -the shared contract, not forks. +There are no known static-audit or platform-page exceptions to the canonical +table, summary-row, external-link, or labelled native-control contracts after +the follow-up. Route-specific cells, metrics, drawer sections, and technical +details are intentional object content inside the shared contract, not forks. diff --git a/docs/release-control/v6/internal/subsystems/frontend-primitives.md b/docs/release-control/v6/internal/subsystems/frontend-primitives.md index aaa676f93..e442436de 100644 --- a/docs/release-control/v6/internal/subsystems/frontend-primitives.md +++ b/docs/release-control/v6/internal/subsystems/frontend-primitives.md @@ -2832,6 +2832,11 @@ for desktop and mobile accessible navigation counts. The label remains buttons, visible focus, selected-state semantics, focus restoration, narrow viewport ordering, and reduced-motion-safe behavior. Detail deep links use `frontend-modern/src/routing/resourceLinks.ts` and remain stable across reload. +The temporary-suppression reason and duration are labelled native controls +owned by `FormTextarea` and `FormSelect`; Patrol must not recreate their label, +focus, responsive touch-target, or controlled-value shells locally. +The objective brief and optional-context fields in `PatrolObjectivesPanel` +share the same `FormTextarea` ownership contract. The default queue shows severity, lifecycle state, plain-language consequence, resource, the required review posture, and observation age. It sorts severity diff --git a/frontend-modern/browser-verification.json b/frontend-modern/browser-verification.json index 16df1a464..eb6a6e9a5 100644 --- a/frontend-modern/browser-verification.json +++ b/frontend-modern/browser-verification.json @@ -1,76 +1,23 @@ { "version": 1, - "base_sha": "164e03ad3c77a50574df782409ee7a417efe8955", - "verified_at": "2026-08-24T17:44:49Z", + "base_sha": "a3d7a90b1bfb14e60432a2a23b0cbd73d6911101", + "verified_at": "2026-08-24T20:51:46Z", "result": "passed", "changed_paths": [ - "frontend-modern/src/components/Docker/SwarmServicesDrawer.tsx", - "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx", - "frontend-modern/src/components/Kubernetes/K8sDeploymentsDrawer.tsx", - "frontend-modern/src/components/Kubernetes/K8sNamespacesDrawer.tsx", - "frontend-modern/src/components/shared/Form.ts", - "frontend-modern/src/components/shared/FormSelect.tsx", - "frontend-modern/src/components/shared/FormTextarea.tsx", - "frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx", - "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx", - "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx", - "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx" + "frontend-modern/src/components/Settings/RelayPairingSection.tsx", + "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx", + "frontend-modern/src/features/patrol/PatrolObjectivesPanel.tsx", + "frontend-modern/src/utils/relayPresentation.ts" ], "content_sha256": { - "frontend-modern/src/components/Docker/SwarmServicesDrawer.tsx": "cfe0382ab9ac0b73ac3d1c0a5c7fdd61e43d1851164cf38071b0d8d99a48fb7e", - "frontend-modern/src/components/Infrastructure/ResourceOperatorStateSection.tsx": "80334e63a0a5560063cf8e3b26e0fd5316989f146f6f014242193c66c51d20a0", - "frontend-modern/src/components/Kubernetes/K8sDeploymentsDrawer.tsx": "1806e0ca765a2fe73f456760f1e4a6506befcbba9e56bbd5249c7ee685b4b225", - "frontend-modern/src/components/Kubernetes/K8sNamespacesDrawer.tsx": "d5d09d77eb54ba91e5e2539c31b5104473ecc73a7e891c50cca4e662975cd294", - "frontend-modern/src/components/shared/Form.ts": "0186bf1fe8d10378d8d603dec8f6b9796302b418e02444a8d18044692815c2b5", - "frontend-modern/src/components/shared/FormSelect.tsx": "38b64e18d93bf7f374e18e29c2f633028c52c4e8b1207a63578f22e0e65c3ec2", - "frontend-modern/src/components/shared/FormTextarea.tsx": "cccbd6e027aabf61eddebab7c9d153737e3b607f64568751cd03c7c138b483f6", - "frontend-modern/src/features/platformPage/PlatformResourceDetailTableRow.tsx": "3c07555f296ab58dec752bd2ce8f8ac058257d0f1efd4d3823c1b5220525fd1b", - "frontend-modern/src/features/platformPage/sharedPlatformPage.tsx": "97a23102f8aff06e4e271248244d9bc63d9fd61ef8d6afe7705353189a153671", - "frontend-modern/src/features/proxmox/ProxmoxBackupServersTable.tsx": "b9a2da5b1191687ffcbbb1ebf95a766c303fbe5717dc30a43f80908481d6b02e", - "frontend-modern/src/features/proxmox/ProxmoxCephClusterDrawer.tsx": "aa8df0cef4a602ab81b8d53251d3c1f7b9b4334f18a2caaa0ba71c70f079c73c", - "frontend-modern/src/features/proxmox/ProxmoxCephTable.tsx": "663ffc8917075275617d1b0ca40e5739429553a855c0e130aa162b2421dca89b", - "frontend-modern/src/features/proxmox/ProxmoxCoverageTable.tsx": "276ee693542e06b116cda0b8b6b9771fba1e416bc11f3645a4d265883d6353f2", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayDrawer.tsx": "b9119d1611e87d8115c4f8e378b169b0db5750a077fc433f57969877bd6d4b8e", - "frontend-modern/src/features/proxmox/ProxmoxMailGatewayTable.tsx": "f9e3185ca23b59bdfc7f77a2738c2cd9e7d5d768ad10846b5c9b3680750bc17a" + "frontend-modern/src/components/Settings/RelayPairingSection.tsx": "85c907c138a6d8693cff5c596236ea09b6188242a7454c42ce341ae02c59371a", + "frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx": "280ee62dc69a1d165da6dcb6db1dff2716c4905d43be8fa50ec5aa9628a4934d", + "frontend-modern/src/features/patrol/PatrolObjectivesPanel.tsx": "f5512db67c12c8a353680eb11af98d61493d942e24c2822d65d7e57a303cdf6c", + "frontend-modern/src/utils/relayPresentation.ts": "930d00c8191a34d6ce6b50197b1dbe94856056c8abeb1de91db312b6cd8966d7" }, "routes": [ - "/proxmox/overview", - "/proxmox/storage", - "/proxmox/replication", - "/proxmox/backups/date", - "/proxmox/backups/coverage", - "/proxmox/ceph", - "/proxmox/mail", - "/docker/overview", - "/docker/images", - "/docker/storage", - "/docker/networks", - "/docker/swarm", - "/kubernetes/overview", - "/kubernetes/nodes", - "/kubernetes/workloads", - "/kubernetes/services", - "/kubernetes/storage", - "/kubernetes/configuration", - "/kubernetes/events", - "/truenas/overview", - "/truenas/storage", - "/truenas/services", - "/truenas/apps", - "/truenas/vms", - "/truenas/shares", - "/truenas/protection", - "/vmware/overview", - "/vmware/storage", - "/vmware/networks", - "/vmware/health", - "/vmware/activity", - "/machines", - "/availability" + "/patrol", + "/settings/system-relay" ], "viewports": [ { @@ -83,27 +30,18 @@ } ], "states": [ - "default populated inventories for every Proxmox, Docker, Kubernetes, TrueNAS, vSphere, Machines, and availability route", - "live loading state while platform resources connected", - "healthy, attention, alert, stale, and disconnected resource presentations", - "grouped and flat inventory views where available", - "expanded Proxmox backup server, backup coverage evidence, Ceph cluster, and Mail Gateway rows", - "node, guest, storage, host, cluster, system, machine, and availability drawers", - "drawer Overview, Manage, Discovery when enabled, and History with range controls", - "cardless Docker Swarm, Kubernetes namespace and deployment, Ceph, Mail Gateway, and restore-evidence tables", - "rebased Proxmox Storage table with the newer mainline plain-header contract" + "populated Patrol decision inbox with critical and warning attention items", + "Patrol Protection view with the add-objective dialog open", + "Patrol attention detail with the temporary-suppression form expanded", + "Relay settings capability-gated state for the current runtime" ], "interactions": [ - "activated legacy Proxmox rows by whole-row pointer click and confirmed aria-expanded and aria-controls", - "activated Mail Gateway detail with Enter and Space and confirmed both keyboard paths toggle the same row", - "confirmed embedded controls do not trigger the owning summary row", - "confirmed desktop disclosure controls remain available and phone disclosure buttons have no visible footprint", - "opened backup coverage restore evidence and confirmed the nested canonical table header and body", - "opened representative platform drawers, switched to History, and inspected chart range placement", - "inspected Manage controls using the compact shared select and textarea presentation", - "confirmed no document-level horizontal overflow at 1280 by 800 or 390 by 844", - "independently inspected the fully rendered post-change backup coverage route with Computer Use in Safari", - "rechecked the rebased Storage route at exactly 1280 by 800 and backup coverage at exactly 390 by 844", - "confirmed phone disclosure controls remain accessibility-exposed while their clipped visual footprint leaves the whole row as the activation affordance" + "opened the Patrol Protection tab and Add objective dialog at 1280 by 800", + "confirmed both objective textareas have generated ids and explicit label associations", + "opened an Inbox decision and expanded temporary suppression at 390 by 844", + "confirmed the shared suppression textarea and select preserve 80 pixel and 44 pixel phone targets", + "confirmed no document-level horizontal overflow at either exact viewport", + "inspected the Relay route without mutating its capability-gated live configuration", + "independently inspected the populated Patrol interface and accessibility hierarchy with Computer Use in Safari" ] } diff --git a/frontend-modern/scripts/shared-template-registry.json b/frontend-modern/scripts/shared-template-registry.json index 25f5dd824..8b1aa02f5 100644 --- a/frontend-modern/scripts/shared-template-registry.json +++ b/frontend-modern/scripts/shared-template-registry.json @@ -822,7 +822,6 @@ { "path": "src/components/shared/DiscoveryLoadingFallback.tsx" }, { "path": "src/components/shared/HistoryChartOverlay.tsx" }, { "path": "src/components/shared/PulseDataGrid.tsx" }, - { "path": "src/components/patrol/ApprovalBanner.tsx" }, { "path": "src/components/patrol/ApprovalSection.tsx" }, { "path": "src/components/patrol/InvestigationMessages.tsx" }, { "path": "src/components/patrol/InvestigationSection.tsx" }, @@ -1701,7 +1700,6 @@ { "path": "src/components/Settings/RecoverySettingsPanel.tsx" }, { "path": "src/components/Settings/RolesEditorDialog.tsx" }, { "path": "src/components/Settings/SystemLogsPanel.tsx" }, - { "path": "src/components/Settings/UpdatesSettingsPanel.tsx" }, { "path": "src/components/Storage/DiskDetail.tsx" }, { "path": "src/components/Storage/StoragePageControls.tsx" }, { "path": "src/components/Storage/StoragePoolDetail.tsx" }, @@ -1711,7 +1709,8 @@ { "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" }, { "path": "src/features/alerts/AlertDeliveryRoutingSection.tsx" }, { "path": "src/features/alerts/AlertEscalationSection.tsx" }, - { "path": "src/features/alerts/AlertQuietHoursSection.tsx" } + { "path": "src/features/alerts/AlertQuietHoursSection.tsx" }, + { "path": "src/features/patrol/PatrolAttentionWorkbench.tsx" } ], "proof": [ "src/components/shared/SharedPrimitives.guardrails.test.ts", @@ -1736,7 +1735,9 @@ { "path": "src/components/Settings/AgentProfilesPanel.tsx" }, { "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx" }, { "path": "src/components/Settings/SuggestProfileModal.tsx" }, - { "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" } + { "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" }, + { "path": "src/features/patrol/PatrolAttentionWorkbench.tsx" }, + { "path": "src/features/patrol/PatrolObjectivesPanel.tsx" } ], "proof": [ "src/components/shared/SharedPrimitives.guardrails.test.ts", @@ -2009,7 +2010,6 @@ "requiredConsumers": [ { "path": "src/components/shared/OrganizationBadges.tsx" }, { "path": "src/components/AI/FindingsPanel.tsx" }, - { "path": "src/components/patrol/ApprovalBanner.tsx" }, { "path": "src/components/patrol/ApprovalSection.tsx" }, { "path": "src/components/patrol/InvestigationSection.tsx" }, { "path": "src/components/patrol/RunHistoryEntry.tsx" }, @@ -5359,6 +5359,26 @@ "scripts/shared-template-audit.mjs" ] }, + { + "id": "form-textarea-local-patrol-fields", + "category": "form-control", + "summary": "Patrol decision forms must compose FormTextarea instead of recreating labelled native textarea wrappers locally.", + "canonical": { + "path": "src/components/shared/FormTextarea.tsx", + "export": "FormTextarea" + }, + "scopes": ["src/features/patrol"], + "extensions": [".tsx"], + "allPatterns": [" = (props)

Generate a QR code to securely link the Pulse Mobile app on your phone to this Pulse instance. {RELAY_PAIRING_APP_AVAILABILITY_TEXT}{' '} - {RELAY_PAIRING_APP_DOWNLOAD_LINK_LABEL} - + .

diff --git a/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.test.ts b/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.test.ts index e81b645ef..f6096c238 100644 --- a/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/RelaySettingsPanel.test.ts @@ -59,5 +59,7 @@ describe('Onboarding QR payload contract', () => { it('points pairing users at the download page for the Pulse Mobile app', () => { expect(relayPairingSectionSource).toContain('PULSE_PRO_DOWNLOAD_URL'); expect(relayPairingSectionSource).toContain('RELAY_PAIRING_APP_AVAILABILITY_TEXT'); + expect(relayPairingSectionSource).toContain('ExternalTextLink'); + expect(relayPairingSectionSource).not.toContain(' { 'src/components/Settings/RecoverySettingsPanel.tsx', 'src/components/Settings/RolesEditorDialog.tsx', 'src/components/Settings/SystemLogsPanel.tsx', - 'src/components/Settings/UpdatesSettingsPanel.tsx', 'src/components/Storage/DiskDetail.tsx', 'src/components/Storage/StoragePageControls.tsx', 'src/components/Storage/StoragePoolDetail.tsx', @@ -697,6 +698,7 @@ describe('shared primitive guardrails', () => { 'src/features/alerts/AlertDeliveryRoutingSection.tsx', 'src/features/alerts/AlertEscalationSection.tsx', 'src/features/alerts/AlertQuietHoursSection.tsx', + 'src/features/patrol/PatrolAttentionWorkbench.tsx', ]); expect(registeredGuard?.canonical?.path).toBe('src/components/shared/FormSelect.tsx'); expect(registeredGuard?.canonical?.export).toBe('FormSelect'); @@ -774,6 +776,9 @@ describe('shared primitive guardrails', () => { const infrastructureGuard = registry.patternGuards?.find( (guard) => guard.id === 'form-textarea-local-infrastructure-fields', ); + const patrolGuard = registry.patternGuards?.find( + (guard) => guard.id === 'form-textarea-local-patrol-fields', + ); expect(registeredRule?.canonical?.path).toBe('src/components/shared/FormTextarea.tsx'); expect(registeredRule?.canonical?.export).toBe('FormTextarea'); @@ -787,6 +792,8 @@ describe('shared primitive guardrails', () => { 'src/components/Settings/SelfHostedCommercialRecoverySection.tsx', 'src/components/Settings/SuggestProfileModal.tsx', 'src/features/alerts/AlertAppriseDestinationsSection.tsx', + 'src/features/patrol/PatrolAttentionWorkbench.tsx', + 'src/features/patrol/PatrolObjectivesPanel.tsx', ]); expect(alertGuard?.canonical?.path).toBe('src/components/shared/FormTextarea.tsx'); expect(alertGuard?.canonical?.export).toBe('FormTextarea'); @@ -803,6 +810,11 @@ describe('shared primitive guardrails', () => { expect(infrastructureGuard?.allPatterns).toEqual([' { suggestProfileModalSource, ssoProvidersPanelSource, alertAppriseDestinationsSectionSource, + patrolAttentionWorkbenchSource, + patrolObjectivesPanelSource, ]; for (const source of migratedConsumers) { expect(source).toContain('FormTextarea'); @@ -2511,7 +2525,6 @@ describe('shared primitive guardrails', () => { expect(metadataRule?.requiredConsumers?.map((consumer) => consumer.path)).toEqual([ 'src/components/shared/OrganizationBadges.tsx', 'src/components/AI/FindingsPanel.tsx', - 'src/components/patrol/ApprovalBanner.tsx', 'src/components/patrol/ApprovalSection.tsx', 'src/components/patrol/InvestigationSection.tsx', 'src/components/patrol/RunHistoryEntry.tsx', @@ -2798,7 +2811,6 @@ describe('shared primitive guardrails', () => { 'src/components/shared/DiscoveryLoadingFallback.tsx', 'src/components/shared/HistoryChartOverlay.tsx', 'src/components/shared/PulseDataGrid.tsx', - 'src/components/patrol/ApprovalBanner.tsx', 'src/components/patrol/ApprovalSection.tsx', 'src/components/patrol/InvestigationMessages.tsx', 'src/components/patrol/InvestigationSection.tsx', diff --git a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx index ec226a702..af64e62b2 100644 --- a/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx +++ b/frontend-modern/src/features/patrol/PatrolAttentionWorkbench.tsx @@ -37,6 +37,8 @@ import { } from '@/api/patrolAttention'; import { ResourceActionsAPI } from '@/api/resourceActions'; import { Button, ButtonLink, CopyValueButton } from '@/components/shared/Button'; +import { FormSelect } from '@/components/shared/FormSelect'; +import { FormTextarea } from '@/components/shared/FormTextarea'; import { LoadingSpinner } from '@/components/shared/LoadingSpinner'; import { MetadataBadge, type MetadataBadgeTone } from '@/components/shared/MetadataBadge'; import { ActionReviewDialog } from '@/features/actions/ActionReviewDialog'; @@ -1346,42 +1348,33 @@ function AttentionLifecycleControls(props: { class="mt-3 space-y-3 border-t border-border-subtle pt-3" onSubmit={submitSuppression} > -
- -