mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
3814 lines
170 KiB
JSON
3814 lines
170 KiB
JSON
{
|
|
"version": 1,
|
|
"rules": [
|
|
{
|
|
"id": "runtime-web-interface-name-link",
|
|
"category": "resource-launch",
|
|
"summary": "Runtime resource rows must launch saved or inferred web-interface URLs through the shared name-link template, not a page-local link or separate web column.",
|
|
"canonical": {
|
|
"path": "src/components/shared/WebInterfaceNameLink.tsx",
|
|
"export": "WebInterfaceNameLink"
|
|
},
|
|
"requiredConsumers": [
|
|
{
|
|
"path": "src/components/shared/NodeGroupHeader.tsx"
|
|
},
|
|
{
|
|
"path": "src/components/Workloads/GuestRow.tsx"
|
|
},
|
|
{
|
|
"path": "src/features/standalone/AgentsMachinesTable.tsx"
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxNodesTable.tsx"
|
|
}
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Workloads/GuestRow.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noopener noreferrer\""]
|
|
},
|
|
{
|
|
"path": "src/components/shared/NodeGroupHeader.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noopener noreferrer\""]
|
|
},
|
|
{
|
|
"path": "src/features/standalone/AgentsMachinesTable.tsx",
|
|
"patterns": [
|
|
"target=\"_blank\"",
|
|
"rel=\"noopener noreferrer\"",
|
|
"AgentMachineWebLinkCell",
|
|
"data-agent-machine-web-link"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/standalone/agentMachineTableModel.ts",
|
|
"patterns": ["id: 'web'", "label: 'Web'"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxNodesTable.tsx",
|
|
"patterns": [
|
|
"target=\"_blank\"",
|
|
"rel=\"noopener noreferrer\"",
|
|
"data-proxmox-host-web-link"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/proxmoxHostTableModel.ts",
|
|
"patterns": ["id: 'web'", "label: 'Web'"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/NodeGroupHeader.test.tsx",
|
|
"src/features/standalone/__tests__/AgentsMachinesTable.test.tsx",
|
|
"src/components/Workloads/__tests__/GuestRow.test.tsx",
|
|
"src/features/proxmox/__tests__/ProxmoxNodesTable.test.tsx",
|
|
"src/features/proxmox/__tests__/proxmoxHostTableModel.test.ts"
|
|
]
|
|
},
|
|
{
|
|
"id": "inline-detail-table-row-shell",
|
|
"category": "row-detail",
|
|
"summary": "Platform, workload, and infrastructure inline detail rows must compose the shared row/cell/content shell instead of recreating page-local surface-alt drawer rows.",
|
|
"canonical": {
|
|
"path": "src/components/shared/InlineDetailTableRow.tsx",
|
|
"export": "InlineDetailTableRow"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Infrastructure/UnifiedResourceHostTableCard.tsx" },
|
|
{ "path": "src/components/Infrastructure/UnifiedResourcePBSTableSection.tsx" },
|
|
{ "path": "src/components/Infrastructure/UnifiedResourcePMGTableSection.tsx" },
|
|
{ "path": "src/components/Workloads/WorkloadPanel.tsx" },
|
|
{ "path": "src/features/docker/DockerAlertsTable.tsx" },
|
|
{ "path": "src/features/docker/DockerHostsTable.tsx" },
|
|
{ "path": "src/features/docker/DockerNetworksTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesAlertsTable.tsx" },
|
|
{ "path": "src/features/platformPage/PlatformResourceDetailTableRow.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxCephTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxCoverageTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxNodesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASAlertsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASServicesTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereActivityTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereAlertsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Workloads/WorkloadPanel.tsx",
|
|
"patterns": [
|
|
"p-0 border-b border-border bg-surface-alt",
|
|
"border-b border-border bg-surface-alt p-0"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/platformPage/PlatformResourceDetailTableRow.tsx",
|
|
"patterns": ["border-b border-border bg-surface-alt p-0"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/InlineDetailTableRow.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "inline-detail-section-panel-shell",
|
|
"category": "row-detail",
|
|
"summary": "Inline platform detail content must compose DetailSectionTable or InlineDetailPanel for shared detail rows, value tones, section tables, and close actions instead of recreating local field grids or provider-named detail primitives.",
|
|
"canonical": {
|
|
"path": "src/components/shared/DetailSectionTable.tsx",
|
|
"export": "DetailSection"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx" },
|
|
{ "path": "src/components/Infrastructure/resourceDetailDrawerKubernetesModel.ts" },
|
|
{ "path": "src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts" },
|
|
{ "path": "src/features/docker/DockerAlertsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesAlertsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASAlertsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASServicesTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereActivityTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereAlertsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Infrastructure/resourceDetailDrawerKubernetesModel.ts",
|
|
"patterns": ["makeTrueNASDetailRow", "TrueNASDetailSection", "trueNASDetailTableModel"]
|
|
},
|
|
{
|
|
"path": "src/features/docker/DockerAlertsTable.tsx",
|
|
"patterns": ["const DetailField", "XIcon class=\"h-4 w-4\""]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesAlertsTable.tsx",
|
|
"patterns": ["const DetailField", "XIcon class=\"h-4 w-4\""]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereActivityTable.tsx",
|
|
"patterns": ["const DetailField", "XIcon class=\"h-4 w-4\""]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereAlertsTable.tsx",
|
|
"patterns": ["const DetailField", "XIcon class=\"h-4 w-4\""]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/DetailSectionTable.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "table-card-frame-shell",
|
|
"category": "table-card",
|
|
"summary": "Product table surfaces must compose TableCard for the shared card frame instead of recreating local overflow-hidden bordered table wrappers.",
|
|
"canonical": {
|
|
"path": "src/components/shared/TableCard.tsx",
|
|
"export": "TableCard"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Infrastructure/UnifiedResourceHostTableCard.tsx" },
|
|
{ "path": "src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx" },
|
|
{ "path": "src/components/Storage/StorageContentCard.tsx" },
|
|
{ "path": "src/components/Workloads/WorkloadsSurface.tsx" },
|
|
{ "path": "src/components/Workloads/WorkloadsTable.tsx" },
|
|
{ "path": "src/features/alerts/AlertHistoryTableSection.tsx" },
|
|
{ "path": "src/features/platformPage/sharedPlatformPage.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "table-card-header-shell",
|
|
"category": "table-card",
|
|
"summary": "Table-card headers must compose TableCardHeader for title/action/clear chrome instead of recreating local summary-table header aliases.",
|
|
"canonical": {
|
|
"path": "src/components/shared/TableCardHeader.tsx",
|
|
"export": "TableCardHeader"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Infrastructure/UnifiedResourceHostTableCard.tsx" },
|
|
{ "path": "src/components/Infrastructure/UnifiedResourceServiceInfrastructureCard.tsx" },
|
|
{ "path": "src/components/Storage/StorageContentCard.tsx" },
|
|
{ "path": "src/features/platformPage/sharedPlatformPage.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "compact-info-card-frame-shell",
|
|
"category": "info-card",
|
|
"summary": "Compact drawer, discovery, resource-detail, and shared overview cards must compose InfoCardFrame for the common bordered surface frame instead of recreating local card-shell classes.",
|
|
"canonical": {
|
|
"path": "src/components/shared/InfoCardFrame.tsx",
|
|
"export": "InfoCardFrame"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Discovery/DiscoveryTab.tsx" },
|
|
{ "path": "src/components/Infrastructure/ResourceActionHistory.tsx" },
|
|
{ "path": "src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx" },
|
|
{ "path": "src/components/shared/WebInterfaceUrlField.tsx" },
|
|
{ "path": "src/components/shared/cards/DisksCard.tsx" },
|
|
{ "path": "src/components/shared/cards/HardwareCard.tsx" },
|
|
{ "path": "src/components/shared/cards/NetworkInterfacesCard.tsx" },
|
|
{ "path": "src/components/shared/cards/RaidCard.tsx" },
|
|
{ "path": "src/components/shared/cards/RootDiskCard.tsx" },
|
|
{ "path": "src/components/shared/cards/SystemInfoCard.tsx" },
|
|
{ "path": "src/components/shared/cards/TemperaturesCard.tsx" },
|
|
{ "path": "src/components/Workloads/DrawerDiskListCard.tsx" },
|
|
{ "path": "src/components/Workloads/GuestDrawerOverview.tsx" },
|
|
{ "path": "src/components/Workloads/NodeDrawerOverview.tsx" },
|
|
{ "path": "src/features/docker/DockerHostDrawerOverview.tsx" },
|
|
{ "path": "src/features/storageBackups/detailPresentation.ts" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/InfoCardFrame.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "settings-callout-card-shell",
|
|
"category": "callout",
|
|
"summary": "Settings informational, warning, success, and danger callouts must compose CalloutCard for shared tone, density, icon, and copy layout instead of recreating local colored panel shells.",
|
|
"canonical": {
|
|
"path": "src/components/shared/CalloutCard.tsx",
|
|
"export": "CalloutCard"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/AIProviderConfigurationSection.tsx" },
|
|
{
|
|
"path": "src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx"
|
|
},
|
|
{ "path": "src/components/Settings/DiagnosticsResultsPanel.tsx" },
|
|
{ "path": "src/components/Settings/DiscoverySettingsForm.tsx" },
|
|
{ "path": "src/components/Settings/MonitoredSystemImpactPreview.tsx" },
|
|
{ "path": "src/components/Settings/ReportingPanel.tsx" },
|
|
{ "path": "src/components/Settings/SecurityAuthPanel.tsx" },
|
|
{ "path": "src/components/Settings/SecurityOverviewPanel.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/AIProviderConfigurationSection.tsx",
|
|
"patterns": ["rounded border border-red-200", "bg-red-50 dark:bg-red-900 px-2 py-1.5"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/DiagnosticsResultsPanel.tsx",
|
|
"patterns": [
|
|
"rounded-md border border-amber-200 bg-amber-50",
|
|
"rounded-md border border-red-200 bg-red-50"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/DiscoverySettingsForm.tsx",
|
|
"patterns": [
|
|
"rounded-md border border-amber-200 bg-amber-50/80",
|
|
"text-amber-900 dark:text-amber-100"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/CalloutCard.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "status-dot-shell",
|
|
"category": "status-indicator",
|
|
"summary": "Resource and health indicator dots must compose StatusDot for shared size, variant, pulse, title, and aria behavior instead of recreating local rounded status spans.",
|
|
"canonical": {
|
|
"path": "src/components/shared/StatusDot.tsx",
|
|
"export": "StatusDot"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/shared/StatusIndicatorBadge.tsx" },
|
|
{ "path": "src/components/Settings/ResourcePicker.tsx" },
|
|
{ "path": "src/components/Storage/StoragePoolDetail.tsx" },
|
|
{ "path": "src/components/shared/NodeGroupHeader.tsx" },
|
|
{ "path": "src/features/alerts/AlertOverviewStatsCards.tsx" },
|
|
{ "path": "src/features/docker/DockerHostsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesNodesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereHostsTable.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/StatusDot.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "loading-spinner-shell",
|
|
"category": "loading-indicator",
|
|
"summary": "Patrol, AI, and shared loading indicators must compose LoadingSpinner for shared size, tone, and accessibility behavior instead of recreating border-based animate-spin spans.",
|
|
"canonical": {
|
|
"path": "src/components/shared/LoadingSpinner.tsx",
|
|
"export": "LoadingSpinner"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/AI/FindingsPanel.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" },
|
|
{ "path": "src/components/patrol/RunToolCallTrace.tsx" },
|
|
{ "path": "src/features/patrol/PatrolIntelligenceHeader.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/AI/FindingsPanel.tsx",
|
|
"patterns": [
|
|
"border border-current border-t-transparent animate-spin",
|
|
"h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/ApprovalBanner.tsx",
|
|
"patterns": [
|
|
"h-3 w-3 border-2 border-white border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/ApprovalSection.tsx",
|
|
"patterns": [
|
|
"h-3 w-3 border-2 border-white border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/InvestigationMessages.tsx",
|
|
"patterns": [
|
|
"h-3 w-3 border-2 border-current border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/InvestigationSection.tsx",
|
|
"patterns": [
|
|
"h-3 w-3 border-2 border-current border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/RunToolCallTrace.tsx",
|
|
"patterns": [
|
|
"h-3 w-3 border-2 border-current border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/patrol/PatrolIntelligenceHeader.tsx",
|
|
"patterns": [
|
|
"animate-spin w-4 h-4 border-2 border-current border-t-transparent rounded-full"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/LoadingSpinner.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/AI/__tests__/FindingsPanel.test.ts",
|
|
"src/components/patrol/__tests__/ApprovalBanner.test.tsx",
|
|
"src/components/patrol/__tests__/ApprovalSection.test.tsx",
|
|
"src/components/patrol/__tests__/InvestigationSection.test.tsx",
|
|
"src/components/patrol/__tests__/RunToolCallTrace.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-command-shell",
|
|
"category": "action-button",
|
|
"summary": "Standard command buttons and button-styled route actions must compose Button or ButtonLink for shared variants, sizes, focus, disabled, loading, and safe-link behavior instead of recreating local Tailwind button shells.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "Button"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/AI/Chat/ChatMessages.tsx" },
|
|
{ "path": "src/components/Infrastructure/ResourceDetailDrawer.tsx" },
|
|
{ "path": "src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx" },
|
|
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
|
|
{ "path": "src/components/Settings/AvailabilitySettingsPanel.tsx" },
|
|
{ "path": "src/components/Settings/BillingAdminPanel.tsx" },
|
|
{ "path": "src/components/Settings/ConnectionEditor/AddressProbeStep.tsx" },
|
|
{ "path": "src/components/Settings/ConnectionEditor/ConnectionEditor.tsx" },
|
|
{
|
|
"path": "src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx"
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx"
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx"
|
|
},
|
|
{ "path": "src/components/Settings/CopyCommandBlock.tsx" },
|
|
{ "path": "src/components/Settings/DataHandlingPanel.tsx" },
|
|
{ "path": "src/components/Settings/GeneralSettingsPanel.tsx" },
|
|
{ "path": "src/components/Settings/InfrastructureAgentUpdatesDialog.tsx" },
|
|
{ "path": "src/components/Settings/InfrastructureDiscoverySettingsDialog.tsx" },
|
|
{ "path": "src/components/Settings/InfrastructureInstallerSection.tsx" },
|
|
{ "path": "src/components/Settings/InfrastructureSourceManager.tsx" },
|
|
{ "path": "src/components/Settings/InfrastructureWorkspace.tsx" },
|
|
{ "path": "src/components/Settings/ReportingPanel.tsx" },
|
|
{ "path": "src/components/Settings/ResourcePicker.tsx" },
|
|
{ "path": "src/components/Workloads/GuestDrawer.tsx" },
|
|
{ "path": "src/features/patrol/PatrolIntelligenceWorkspace.tsx" },
|
|
{ "path": "src/features/standalone/StandalonePageSurface.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/GeneralSettingsPanel.tsx",
|
|
"patterns": [
|
|
"inline-flex items-center rounded-md border border-border bg-surface px-3 py-2 text-xs font-medium text-base-content transition hover:bg-surface-hover"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/BillingAdminPanel.tsx",
|
|
"patterns": [
|
|
"w-full sm:w-auto px-3 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ResourcePicker.tsx",
|
|
"patterns": [
|
|
"hover:border-red-500 hover:text-red-400",
|
|
"w-full sm:w-auto min-h-10 sm:min-h-9 flex items-center justify-center gap-1.5 px-3 py-2.5 text-sm rounded-md border border-border"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ReportingPanel.tsx",
|
|
"patterns": [
|
|
"rounded-md border border-base-300 bg-base-100 px-4 py-2 text-sm font-medium text-base-content",
|
|
"flex w-full items-center justify-center gap-2 rounded-md px-6 py-3 font-semibold transition-all sm:w-auto",
|
|
"bg-blue-600 text-white hover:bg-blue-700",
|
|
"bg-emerald-600 text-white hover:bg-emerald-700"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/InfrastructureInstallerSection.tsx",
|
|
"patterns": [
|
|
"inline-flex items-center justify-center rounded-md bg-emerald-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-emerald-700",
|
|
"inline-flex items-center justify-center rounded-md border border-emerald-300 bg-white px-3 py-2 text-sm font-medium text-emerald-900 transition-colors hover:bg-emerald-100",
|
|
"inline-flex items-center justify-center rounded-md border border-emerald-300 bg-white px-3 py-2 text-sm font-medium text-emerald-900 hover:bg-emerald-100",
|
|
"inline-flex items-center justify-center rounded-md px-3 py-2 text-sm font-medium text-emerald-900 hover:bg-emerald-100"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "copy-value-action-shell",
|
|
"category": "copy-action",
|
|
"summary": "Copy-value icon and chip controls must compose CopyValueButton for shared copied-state iconography, disabled handling, focus, sizing, and aria behavior instead of recreating page-local copy buttons.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "CopyValueButton"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Discovery/DiscoveryTab.tsx" },
|
|
{ "path": "src/components/shared/WebInterfaceUrlField.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Discovery/DiscoveryTab.tsx",
|
|
"patterns": [
|
|
"interface CopyValueButtonProps",
|
|
"const CopyValueButton",
|
|
"CopyIcon class=\"h-3.5 w-3.5\"",
|
|
"CheckIcon class=\"h-3.5 w-3.5 text-emerald-600"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/shared/WebInterfaceUrlField.tsx",
|
|
"patterns": [
|
|
"CopyIcon class=\"h-3.5 w-3.5\"",
|
|
"CheckIcon class=\"h-3.5 w-3.5 text-emerald-600"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "upgrade-action-button-link",
|
|
"category": "upgrade-action",
|
|
"summary": "Button-styled upgrade CTAs must compose UpgradeButtonLink so destination routing, new-tab safety, opener preservation, tone, width, and shared button styling stay canonical.",
|
|
"canonical": {
|
|
"path": "src/components/shared/UpgradeLink.tsx",
|
|
"export": "UpgradeButtonLink"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
|
|
{ "path": "src/components/Settings/AuditLogPanel.tsx" },
|
|
{ "path": "src/components/Settings/ProLicensePlanSection.tsx" },
|
|
{ "path": "src/components/shared/FeatureGateSection.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/utils/upgradePresentation.ts",
|
|
"patterns": ["getUpgradeActionButtonClass"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AgentProfilesPanel.tsx",
|
|
"patterns": ["getUpgradeActionButtonClass"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AuditLogPanel.tsx",
|
|
"patterns": ["getUpgradeActionButtonClass"]
|
|
},
|
|
{
|
|
"path": "src/components/shared/FeatureGateSection.tsx",
|
|
"patterns": ["getUpgradeActionButtonClass"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ProLicensePlanSection.tsx",
|
|
"patterns": [
|
|
"inline-flex items-center gap-1 mt-3 min-h-10 sm:min-h-9 rounded-md border border-current/20 px-3 py-2 text-xs font-medium hover:bg-black/5 dark:hover:bg-white/5",
|
|
"inline-flex items-center gap-1 mt-4 min-h-10 sm:min-h-9 rounded-md border border-border px-3 py-2 text-xs font-medium text-base-content hover:bg-surface-hover"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/UpgradeLink.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "copyable-code-row-shell",
|
|
"category": "copy-action",
|
|
"summary": "Copyable code/value rows must compose CopyableCodeRow for the shared code surface and copy action instead of recreating local font-mono row shells.",
|
|
"canonical": {
|
|
"path": "src/components/shared/CopyableCodeRow.tsx",
|
|
"export": "CopyableCodeRow"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/Discovery/DiscoveryTab.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Discovery/DiscoveryTab.tsx",
|
|
"patterns": [
|
|
"const CopyableCodeRow",
|
|
"flex items-start gap-2 rounded bg-surface-alt px-2 py-1.5"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "form-select-shell",
|
|
"category": "form-control",
|
|
"summary": "Native select controls must compose FormSelect for shared label/id/help wiring, value synchronization, and select chrome instead of recreating labelled select wrappers locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FormSelect.tsx",
|
|
"export": "FormSelect"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/AI/FindingsPanel.tsx" },
|
|
{ "path": "src/components/Alerts/EmailProviderSelect.tsx" },
|
|
{ "path": "src/components/Alerts/WebhookConfigForm.tsx" },
|
|
{ "path": "src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx" },
|
|
{ "path": "src/components/Kubernetes/K8sDeploymentsDrawer.tsx" },
|
|
{ "path": "src/components/OrgSwitcher.tsx" },
|
|
{ "path": "src/components/Settings/AIChatMaintenanceSection.tsx" },
|
|
{ "path": "src/components/Settings/AIModelSelectionSection.tsx" },
|
|
{ "path": "src/components/Settings/AIRuntimeControlsSection.tsx" },
|
|
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
|
|
{ "path": "src/components/Settings/AuditLogPanel.tsx" },
|
|
{
|
|
"path": "src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx"
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx"
|
|
},
|
|
{ "path": "src/components/Settings/InfrastructureInstallerSection.tsx" },
|
|
{ "path": "src/components/Settings/NodeModalMonitoringSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationAccessManagementSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationAccessMembersSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationSharingCreateSection.tsx" },
|
|
{ "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" },
|
|
{ "path": "src/components/Workloads/GuestDrawerHistory.tsx" },
|
|
{ "path": "src/components/shared/FilterBar/AddFilterMenu.tsx" },
|
|
{ "path": "src/components/shared/FilterToolbar.tsx" },
|
|
{ "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" },
|
|
{ "path": "src/features/alerts/AlertEscalationSection.tsx" },
|
|
{ "path": "src/features/alerts/AlertQuietHoursSection.tsx" },
|
|
{ "path": "src/features/patrol/PatrolIntelligenceHeader.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/FormSelect.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "form-textarea-shell",
|
|
"category": "form-control",
|
|
"summary": "Native textarea controls must compose FormTextarea for shared label/id/help wiring, value synchronization, and textarea chrome instead of recreating labelled textarea wrappers locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FormTextarea.tsx",
|
|
"export": "FormTextarea"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Alerts/EmailProviderSelect.tsx" },
|
|
{ "path": "src/components/Alerts/IncidentTimelinePanel.tsx" },
|
|
{ "path": "src/components/Alerts/ThresholdsTableDockerIgnoredPrefixesSection.tsx" },
|
|
{ "path": "src/components/Alerts/WebhookConfigForm.tsx" },
|
|
{ "path": "src/components/Infrastructure/ReportMergeModal.tsx" },
|
|
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
|
|
{ "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx" },
|
|
{ "path": "src/components/Settings/SuggestProfileModal.tsx" },
|
|
{ "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/FormTextarea.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "toggle-shell",
|
|
"category": "toggle-control",
|
|
"summary": "Binary on/off controls must compose Toggle or TogglePrimitive for shared track/knob styling, disabled behavior, label/description wiring, and synthetic checked events instead of recreating local switch buttons.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Toggle.tsx",
|
|
"export": "Toggle"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Alerts/AlertResourceTableDesktop.tsx" },
|
|
{ "path": "src/components/Alerts/AlertResourceTableMobile.tsx" },
|
|
{ "path": "src/components/Alerts/AlertResourceTableRow.tsx" },
|
|
{ "path": "src/components/Alerts/ThresholdsTableDockerServiceGapSection.tsx" },
|
|
{ "path": "src/components/Alerts/ThresholdsTableProxmoxBackupsSection.tsx" },
|
|
{ "path": "src/components/Infrastructure/ResourceOperatorStateSection.tsx" },
|
|
{ "path": "src/components/Settings/AIRuntimeControlsSection.tsx" },
|
|
{ "path": "src/components/Settings/AISettings.tsx" },
|
|
{ "path": "src/components/Settings/AuditLogPanel.tsx" },
|
|
{ "path": "src/components/Settings/DiscoverySettingsForm.tsx" },
|
|
{ "path": "src/components/Settings/DockerRuntimeSettingsCard.tsx" },
|
|
{ "path": "src/components/Settings/GeneralSettingsPanel.tsx" },
|
|
{ "path": "src/components/Settings/NodeModalMonitoringSection.tsx" },
|
|
{ "path": "src/components/Settings/RelaySettingsPanel.tsx" },
|
|
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" },
|
|
{ "path": "src/components/Settings/SecurityAuthPanel.tsx" },
|
|
{ "path": "src/features/alerts/AlertAppriseDestinationsSection.tsx" },
|
|
{ "path": "src/features/alerts/AlertCooldownSection.tsx" },
|
|
{ "path": "src/features/alerts/AlertEmailDestinationsSection.tsx" },
|
|
{ "path": "src/features/alerts/AlertEscalationSection.tsx" },
|
|
{ "path": "src/features/alerts/AlertGroupingSection.tsx" },
|
|
{ "path": "src/features/alerts/AlertQuietHoursSection.tsx" },
|
|
{ "path": "src/features/alerts/AlertRecoverySection.tsx" },
|
|
{ "path": "src/features/patrol/PatrolIntelligenceHeader.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Toggle.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "status-indicator-badge-shell",
|
|
"category": "status-indicator",
|
|
"summary": "Read-only health/state badges must compose StatusIndicatorBadge for shared status-to-tone mapping, sizing, dot wiring, and label presentation instead of recreating local badge spans with getStatusIndicatorBadgeToneClasses.",
|
|
"canonical": {
|
|
"path": "src/components/shared/StatusIndicatorBadge.tsx",
|
|
"export": "StatusIndicatorBadge"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
|
|
{ "path": "src/components/Settings/DiagnosticsResultsPanel.tsx" },
|
|
{ "path": "src/components/patrol/RunHistoryEntry.tsx" },
|
|
{ "path": "src/components/patrol/PatrolStatusBar.tsx" },
|
|
{ "path": "src/features/patrol/PatrolIntelligenceSummary.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/patrol/RunHistoryEntry.tsx",
|
|
"patterns": ["runStatus.badgeClass"]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/PatrolStatusBar.tsx",
|
|
"patterns": [
|
|
"lastRunStatus.badgeClass",
|
|
"rounded border border-blue-200 bg-blue-50 px-1.5 py-0.5 text-[10px] font-medium text-blue-700"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/patrol/PatrolIntelligenceSummary.tsx",
|
|
"patterns": [
|
|
"runtimeShellPresentation().badgeClass",
|
|
"inline-flex items-center rounded-full border px-2.5 py-1 text-[11px] font-semibold uppercase tracking-[0.16em]"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/StatusIndicatorBadge.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-severity-indicator-shell",
|
|
"category": "status-indicator",
|
|
"summary": "Platform alert tables must compose AlertSeverityBadge and AlertSeverityDot for shared severity labels, tone mapping, and status-indicator rendering instead of recreating severity helpers locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/AlertSeverityBadge.tsx",
|
|
"export": "AlertSeverityBadge"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerAlertsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesAlertsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASAlertsTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereAlertsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerAlertsTable.tsx",
|
|
"patterns": ["severityVariant", "severityTextClass"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesAlertsTable.tsx",
|
|
"patterns": ["severityVariant", "severityTextClass"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASAlertsTable.tsx",
|
|
"patterns": ["severityVariant", "severityTextClass"]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereAlertsTable.tsx",
|
|
"patterns": ["severityVariant", "severityTextClass"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/AlertSeverityBadge.test.tsx",
|
|
"src/utils/__tests__/alertSeverityPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-severity-filter-options",
|
|
"category": "filter-options",
|
|
"summary": "Platform alert tables must get All/Critical/Warning/Info severity filter labels, tones, and leading dots from the shared platform alert severity filter-options primitive instead of recreating local option arrays.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/platformAlertSeverityFilterOptions.tsx",
|
|
"export": "getPlatformAlertSeverityFilterOptions"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerAlertsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesAlertsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASAlertsTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereAlertsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerAlertsTable.tsx",
|
|
"patterns": [
|
|
"filterChipStatusDot(",
|
|
"value: 'critical'",
|
|
"value: 'warning'",
|
|
"value: 'info'"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesAlertsTable.tsx",
|
|
"patterns": [
|
|
"filterChipStatusDot(",
|
|
"value: 'critical'",
|
|
"value: 'warning'",
|
|
"value: 'info'"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASAlertsTable.tsx",
|
|
"patterns": ["value: 'critical'", "value: 'warning'", "value: 'info'"]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereAlertsTable.tsx",
|
|
"patterns": [
|
|
"filterChipStatusDot(",
|
|
"value: 'critical'",
|
|
"value: 'warning'",
|
|
"value: 'info'"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/platformAlertSeverityFilterOptions.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-severity-detail-tone",
|
|
"category": "detail-tone",
|
|
"summary": "Platform alert detail rows must get severity-to-detail-row tone mapping from alertSeverityPresentation instead of defining table-local alertTone helpers.",
|
|
"canonical": {
|
|
"path": "src/utils/alertSeverityPresentation.ts",
|
|
"export": "getAlertSeverityDetailTone"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerAlertsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesAlertsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASAlertsTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereAlertsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerAlertsTable.tsx",
|
|
"patterns": ["const alertTone", "type AlertDetailTone"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesAlertsTable.tsx",
|
|
"patterns": ["const alertTone", "type AlertDetailTone"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASAlertsTable.tsx",
|
|
"patterns": ["const alertTone", "type AlertDetailTone"]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereAlertsTable.tsx",
|
|
"patterns": ["const alertTone", "type AlertDetailTone"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/utils/__tests__/alertSeverityPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-detail-formatters",
|
|
"category": "detail-formatting",
|
|
"summary": "Platform alert tables must get provider code, resource-type, entity-type, and timestamp labels from alertDetailPresentation instead of defining table-local detail formatter helpers.",
|
|
"canonical": {
|
|
"path": "src/utils/alertDetailPresentation.ts",
|
|
"export": "formatPlatformAlertCode"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerAlertsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesAlertsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASAlertsTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereAlertsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerAlertsTable.tsx",
|
|
"patterns": [
|
|
"const formatResourceType",
|
|
"const formatCode",
|
|
"const formatStartedAt",
|
|
"const detailDateTime"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesAlertsTable.tsx",
|
|
"patterns": [
|
|
"const formatResourceType",
|
|
"const formatCode",
|
|
"const formatStartedAt",
|
|
"const detailDateTime"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASAlertsTable.tsx",
|
|
"patterns": [
|
|
"const formatResourceType",
|
|
"const formatCode",
|
|
"const formatStartedAt",
|
|
"const detailDateTime"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereAlertsTable.tsx",
|
|
"patterns": [
|
|
"const formatResourceType",
|
|
"const formatEntityType",
|
|
"const formatCode",
|
|
"const formatStartedAt",
|
|
"const detailDateTime"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/utils/__tests__/alertDetailPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "metadata-badge-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Read-only metadata chips must compose MetadataBadge so size, shape, tone vocabulary, fit behavior, and whitespace handling stay shared instead of being copied as page-local pill spans.",
|
|
"canonical": {
|
|
"path": "src/components/shared/MetadataBadge.tsx",
|
|
"export": "MetadataBadge"
|
|
},
|
|
"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" },
|
|
{ "path": "src/components/patrol/RunToolCallTrace.tsx" },
|
|
{ "path": "src/components/patrol/PatrolStatusBar.tsx" },
|
|
{ "path": "src/features/proxmox/proxmoxBackupsTableShared.tsx" },
|
|
{ "path": "src/features/patrol/PatrolIntelligenceWorkspace.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/AI/FindingsPanel.tsx",
|
|
"patterns": [
|
|
"px-1.5 py-0.5 border text-[10px] font-medium rounded",
|
|
"getFindingStatusBadgeClasses",
|
|
"getFindingSourceBadgeClasses",
|
|
"getFindingLoopStateBadgeClasses",
|
|
"getInvestigationStatusBadgeClasses",
|
|
"getInvestigationOutcomeBadgeClasses",
|
|
"getInvestigationConfidenceBadgeClasses"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/ApprovalBanner.tsx",
|
|
"patterns": [
|
|
"px-1.5 py-0.5 text-[10px] font-medium rounded",
|
|
"firstApprovalRisk()!.badgeClass"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/ApprovalSection.tsx",
|
|
"patterns": [
|
|
"px-1.5 py-0.5 text-[10px] font-medium rounded",
|
|
"approvalRisk.badgeClass",
|
|
"fixRisk.badgeClass",
|
|
"bg-amber-100 text-amber-700 dark:bg-amber-900 dark:text-amber-300"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/InvestigationSection.tsx",
|
|
"patterns": [
|
|
"px-1.5 py-0.5 border text-[10px] font-medium rounded",
|
|
"getInvestigationStatusBadgeClasses",
|
|
"getInvestigationOutcomeBadgeClasses",
|
|
"rounded border border-border bg-surface px-1.5 py-0.5 text-[10px] font-medium text-muted",
|
|
"rounded bg-surface px-1.5 py-0.5 text-[10px] font-medium text-muted"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/RunHistoryEntry.tsx",
|
|
"patterns": [
|
|
"inline-flex items-center gap-1 px-2 py-0.5 rounded-full text-xs font-medium",
|
|
"inline-flex items-center gap-1 px-1.5 py-0.5 rounded bg-blue-50"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/RunToolCallTrace.tsx",
|
|
"patterns": [
|
|
"px-1.5 py-0.5 rounded text-[10px] font-medium",
|
|
"getToolCallResultBadgeClass"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/PatrolStatusBar.tsx",
|
|
"patterns": ["text-blue-600 dark:text-blue-400\">Findings snapshot unavailable"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/proxmoxBackupsTableShared.tsx",
|
|
"patterns": [
|
|
"inline-flex items-center rounded-sm px-1.5 py-0.5 text-[10px] font-semibold",
|
|
"presentation().badgeClassName",
|
|
"rounded-sm bg-violet-100 px-1.5 py-0.5 text-[10px] font-semibold",
|
|
"rounded-sm bg-amber-100 px-1.5 py-0.5 text-[10px] font-semibold",
|
|
"rounded-sm bg-emerald-100 px-1.5 py-0.5 text-[10px] font-semibold"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/patrol/PatrolIntelligenceWorkspace.tsx",
|
|
"patterns": [
|
|
"findingsBadgePresentation().toneClasses",
|
|
"ml-1.5 px-1.5 py-0.5 text-xs rounded-full",
|
|
"bg-surface-alt text-muted"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/MetadataBadge.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "organization-role-badge-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Organization role badges must compose OrganizationRoleBadge instead of each organization settings section mapping role tones and pill styling locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/OrganizationBadges.tsx",
|
|
"export": "OrganizationRoleBadge"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/OrganizationAccessInvitationsSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationAccessMembersSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationIncomingSharesSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationOutgoingSharesSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationOverviewMembersSection.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/OrganizationAccessInvitationsSection.tsx",
|
|
"patterns": ["roleBadgeClass"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationAccessMembersSection.tsx",
|
|
"patterns": ["roleBadgeClass"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationIncomingSharesSection.tsx",
|
|
"patterns": ["roleBadgeClass"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationOutgoingSharesSection.tsx",
|
|
"patterns": ["roleBadgeClass"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationOverviewMembersSection.tsx",
|
|
"patterns": ["roleBadgeClass"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/OrganizationBadges.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "organization-share-status-badge-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Organization share status badges must compose OrganizationShareStatusBadge so pending/active labels and tones stay canonical across incoming and outgoing share tables.",
|
|
"canonical": {
|
|
"path": "src/components/shared/OrganizationBadges.tsx",
|
|
"export": "OrganizationShareStatusBadge"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/OrganizationIncomingSharesSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationOutgoingSharesSection.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/OrganizationIncomingSharesSection.tsx",
|
|
"patterns": ["statusBadgeClass"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationOutgoingSharesSection.tsx",
|
|
"patterns": ["statusBadgeClass"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/OrganizationBadges.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-loading-state",
|
|
"category": "platform-table-loading-state",
|
|
"summary": "Platform page and table loading states must compose PlatformTableLoadingState instead of recreating local TableCard status rows.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableLoadingState"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerPageSurface.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesPageSurface.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxBackupsTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxPageSurface.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxReplicationTable.tsx" },
|
|
{ "path": "src/features/standalone/StandalonePageSurface.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASPageSurface.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
|
|
{ "path": "src/features/vmware/VmwarePageSurface.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-text-value-fallback",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared text-cell fallback helper for trimmed strings and canonical empty-cell markers instead of recreating local textValue helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableTextValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/kubernetes/KubernetesAutoscalingTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesClustersTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesConfigTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesControllersTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesDeploymentsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesEventsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesNetworkingTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesNodesTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesPodsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesPolicyTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesServicesTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesStorageTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesAutoscalingTable.tsx",
|
|
"patterns": ["const textValue"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesClustersTable.tsx",
|
|
"patterns": ["asTrimmedString(cluster.kubernetes?.context) || '—'"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesConfigTable.tsx",
|
|
"patterns": ["const textValue"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesControllersTable.tsx",
|
|
"patterns": ["const textValue"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesDeploymentsTable.tsx",
|
|
"patterns": ["asTrimmedString(deployment.kubernetes?.namespace) || '—'"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesEventsTable.tsx",
|
|
"patterns": ["const textValue"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesNetworkingTable.tsx",
|
|
"patterns": ["const textValue"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesNodesTable.tsx",
|
|
"patterns": ["asTrimmedString(meta()?.kubeletVersion) || '—'"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesPodsTable.tsx",
|
|
"patterns": ["const textValue"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesPolicyTable.tsx",
|
|
"patterns": ["const textValue"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesServicesTable.tsx",
|
|
"patterns": ["const textValue"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesStorageTable.tsx",
|
|
"patterns": ["const textValue"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-title-case-value-fallback",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared title-case fallback helper for status/state labels instead of recreating local titleCase helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableTitleCaseValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/truenas/TrueNASAppsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASNetworkSharesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASServicesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASStorageTopologyTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASVirtualMachinesTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/truenas/TrueNASAppsTable.tsx",
|
|
"patterns": ["const titleCase"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASNetworkSharesTable.tsx",
|
|
"patterns": ["const titleCase"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASServicesTable.tsx",
|
|
"patterns": ["const titleCase"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASStorageTopologyTable.tsx",
|
|
"patterns": ["const titleCase"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASVirtualMachinesTable.tsx",
|
|
"patterns": ["const titleCase"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-value-summary",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared compact value summary helper for trimmed list labels and full title text instead of recreating local compactList or summarizeValues helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "summarizePlatformTableValues"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/kubernetes/KubernetesAutoscalingTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesConfigTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesNetworkingTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesPolicyTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesServicesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASNetworkSharesTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereDatastoresTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereNetworksTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesAutoscalingTable.tsx",
|
|
"patterns": ["const compactList", "const summarizeValues"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesConfigTable.tsx",
|
|
"patterns": ["const compactList", "const summarizeValues"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesNetworkingTable.tsx",
|
|
"patterns": ["const compactList", "const summarizeValues"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesPolicyTable.tsx",
|
|
"patterns": ["const compactList", "const summarizeValues"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesServicesTable.tsx",
|
|
"patterns": ["const compactList", "const summarizeValues"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASNetworkSharesTable.tsx",
|
|
"patterns": ["const compactList", "const summarizeValues"]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereDatastoresTable.tsx",
|
|
"patterns": ["const compactList", "const summarizeValues"]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereNetworksTable.tsx",
|
|
"patterns": ["const compactList", "const summarizeValues"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-uptime-value-fallback",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared one-unit uptime fallback helper instead of recreating local formatUptime helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableUptimeValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerHostsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesNodesTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx" },
|
|
{ "path": "src/features/standalone/AgentsMachinesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerHostsTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesNodesTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
},
|
|
{
|
|
"path": "src/features/standalone/AgentsMachinesTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-byte-value-fallback",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared byte-size fallback helper for positive byte formatting and canonical empty-cell markers instead of recreating local formatBytes wrappers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableBytesValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerNativeTableShared.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesNodesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASStorageTopologyTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASVirtualMachinesTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerNativeTableShared.tsx",
|
|
"patterns": ["formatBytes(value)"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesNodesTable.tsx",
|
|
"patterns": ["const formatBytes", "value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASProtectionTable.tsx",
|
|
"patterns": ["formatBytes(point.sizeBytes)"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASStorageTopologyTable.tsx",
|
|
"patterns": [
|
|
"formatBytes(size)",
|
|
"formatBytes(row.resource.disk.used)",
|
|
"formatBytes(row.resource.disk.total)"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
|
|
"patterns": ["const formatBytes", "value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASVirtualMachinesTable.tsx",
|
|
"patterns": ["const formatBytes", "value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-number-value-fallback",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared number-value fallback primitive for finite numeric table cells, tabular styling, and canonical empty-cell markers instead of recreating local numberValue/countCell helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableNumberValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerNativeTableShared.tsx" },
|
|
{ "path": "src/features/docker/DockerServicesTable.tsx" },
|
|
{ "path": "src/components/Docker/SwarmServicesDrawer.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesAutoscalingTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesControllersTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesDeploymentsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesEventsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesPodsTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASStorageTopologyTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerNativeTableShared.tsx",
|
|
"patterns": [
|
|
"typeof value === 'number' ? <span class=\"tabular-nums\">{value}</span> : <span>—</span>"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/docker/DockerServicesTable.tsx",
|
|
"patterns": ["const replicaCount", "<span class=\"tabular-nums\">{value ?? 0}</span>"]
|
|
},
|
|
{
|
|
"path": "src/components/Docker/SwarmServicesDrawer.tsx",
|
|
"patterns": [
|
|
"<span class=\"tabular-nums\">{desired()}</span>",
|
|
"<span class=\"tabular-nums\">{running()}</span>"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesAutoscalingTable.tsx",
|
|
"patterns": [
|
|
"const numberValue",
|
|
"typeof value === 'number' ? <span class=\"tabular-nums\">{value}</span> : <span>—</span>"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesControllersTable.tsx",
|
|
"patterns": [
|
|
"const numberValue",
|
|
"typeof value === 'number' ? <span class=\"tabular-nums\">{value}</span> : <span>—</span>"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesDeploymentsTable.tsx",
|
|
"patterns": ["const replicaCount", "<span class=\"tabular-nums\">{value ?? 0}</span>"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesEventsTable.tsx",
|
|
"patterns": [
|
|
"const numberValue",
|
|
"typeof value === 'number' ? <span class=\"tabular-nums\">{value}</span> : <span>—</span>"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesPodsTable.tsx",
|
|
"patterns": [
|
|
"const numericValue",
|
|
"typeof value === 'number' ? <span class=\"tabular-nums\">{value}</span> : <span>—</span>"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx",
|
|
"patterns": ["const countCell", "value.toLocaleString() : '—'"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
|
|
"patterns": ["hidden text-base-content tabular-nums lg:table-cell"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASStorageTopologyTable.tsx",
|
|
"patterns": ["const diskCountLabel"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-count-ratio-value",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared count-ratio primitive for healthy/total or ready/total count cells instead of recreating local numerator/slash/denominator markup.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableCountRatioValue"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/features/kubernetes/KubernetesClustersTable.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesClustersTable.tsx",
|
|
"patterns": ["const childCountCell", "<span class=\"text-muted\">/{count.total}</span>"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-percent-value-fallback",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared percent-value primitive for one-decimal percent cells, tabular styling, and canonical empty-cell markers instead of recreating local formatPercent helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTablePercentValue"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
|
|
"patterns": [
|
|
"const formatPercent",
|
|
"<span class=\"tabular-nums\">{percent.toFixed(1)}%</span>"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-temperature-value-fallback",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared temperature-value primitive for positive Celsius cells, tabular styling, and canonical empty-cell markers instead of recreating local formatTemperature helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableTemperatureValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerHostsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASStorageTopologyTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerHostsTable.tsx",
|
|
"patterns": [
|
|
"const formatTemperature",
|
|
"<span class=\"tabular-nums\">{celsius.toFixed(1)}°C</span>"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASStorageTopologyTable.tsx",
|
|
"patterns": ["const temperatureLabel", "${Math.round(value)}C"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
|
|
"patterns": [
|
|
"const formatTemperature",
|
|
"<span class=\"tabular-nums\">{celsius.toFixed(1)}°C</span>"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-metric-fallback",
|
|
"category": "platform-table-metric",
|
|
"summary": "Platform tables must use shared metric fallback and finite-metric helpers instead of recreating local centered dash fallbacks and Number.isFinite wrappers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableMetricFallback"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerContainersTable.tsx" },
|
|
{ "path": "src/features/docker/DockerHostsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesClustersTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesNodesTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxNodesTable.tsx" },
|
|
{ "path": "src/features/standalone/AgentsMachinesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASAppsTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereDatastoresTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereHostsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerContainersTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
},
|
|
{
|
|
"path": "src/features/docker/DockerHostsTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesClustersTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesNodesTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxNodesTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
},
|
|
{
|
|
"path": "src/features/standalone/AgentsMachinesTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASAppsTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereDatastoresTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereHostsTable.tsx",
|
|
"patterns": ["const finiteMetric", "const metricFallback"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-error-state",
|
|
"category": "platform-error-state",
|
|
"summary": "Platform page and table load failures must compose PlatformErrorState instead of recreating a local EmptyState plus Refresh action.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformErrorState"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerPageSurface.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesPageSurface.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxBackupsTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxPageSurface.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxReplicationTable.tsx" },
|
|
{ "path": "src/features/standalone/StandalonePageSurface.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASPageSurface.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
|
|
{ "path": "src/features/vmware/VmwarePageSurface.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-section-tabs",
|
|
"category": "platform-section-tabs",
|
|
"summary": "Platform page workflow tabs must compose PlatformSectionTabs instead of recreating page-local nav tab bars.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformSectionTabs"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerPageSurface.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesPageSurface.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxPageSurface.tsx" },
|
|
{ "path": "src/features/standalone/StandalonePageSurface.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASPageSurface.tsx" },
|
|
{ "path": "src/features/vmware/VmwarePageSurface.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/platformOverviewLayout.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "filter-bar-catalog-shell",
|
|
"category": "filter-bar",
|
|
"summary": "Resource-list filter surfaces must compose the shared FilterBar catalog shell instead of recreating page-local filter rows or legacy PageControls decks.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FilterBar/FilterBar.tsx",
|
|
"export": "FilterBar"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Alerts/ThresholdsTable.tsx" },
|
|
{ "path": "src/components/Settings/AuditLogPanel.tsx" },
|
|
{ "path": "src/components/Storage/StoragePageControls.tsx" },
|
|
{ "path": "src/components/Workloads/WorkloadsFilter.tsx" },
|
|
{ "path": "src/features/alerts/AlertHistoryFiltersCard.tsx" },
|
|
{ "path": "src/features/platformPage/sharedPlatformPage.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxBackupsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Storage/StoragePageControls.tsx",
|
|
"patterns": [
|
|
"from '@/components/shared/PageControls'",
|
|
"<PageControls",
|
|
"<StorageControls",
|
|
"<StorageFilter",
|
|
"LabeledFilterSelect",
|
|
"FilterSegmentedControl"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Workloads/WorkloadsFilter.tsx",
|
|
"patterns": [
|
|
"from '@/components/shared/PageControls'",
|
|
"<PageControls",
|
|
"LabeledFilterSelect",
|
|
"LabeledFilterToggleGroup"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxBackupsTable.tsx",
|
|
"patterns": [
|
|
"from '@/components/shared/PageControls'",
|
|
"<PageControls",
|
|
"LabeledFilterSelect",
|
|
"LabeledFilterToggleGroup"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/FilterBar/FilterBar.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "search-field-shell",
|
|
"category": "search-control",
|
|
"summary": "Product surfaces with simple search fields must compose SearchField for the shared icon, clear button, keyboard, focus, aria, and input chrome instead of rendering native search inputs.",
|
|
"canonical": {
|
|
"path": "src/components/shared/SearchField.tsx",
|
|
"export": "SearchField"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/AI/Chat/AssistantCommandHelpDialog.tsx" },
|
|
{ "path": "src/components/AI/Chat/index.tsx" },
|
|
{ "path": "src/components/Settings/InfrastructureSourcePicker.tsx" },
|
|
{ "path": "src/components/Settings/ResourcePicker.tsx" },
|
|
{ "path": "src/components/Settings/UserAssignmentsPanel.tsx" },
|
|
{ "path": "src/components/shared/AIModelPicker.tsx" },
|
|
{ "path": "src/components/shared/CommandPaletteModal.tsx" },
|
|
{ "path": "src/components/shared/SearchInput.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/ResourcePicker.tsx",
|
|
"patterns": ["type=\"text\"", "onInput={(e) => setTagFilter"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/SearchField.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "search-input-shell",
|
|
"category": "search-control",
|
|
"summary": "Resource-list and drawer search boxes that need history, tips, or type-to-search behavior must compose SearchInput instead of rebuilding search-enhancement controls locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/SearchInput.tsx",
|
|
"export": "SearchInput"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Docker/SwarmServicesDrawer.tsx" },
|
|
{ "path": "src/components/Kubernetes/K8sDeploymentsDrawer.tsx" },
|
|
{ "path": "src/components/Kubernetes/K8sNamespacesDrawer.tsx" },
|
|
{ "path": "src/components/Settings/SettingsPageShell.tsx" },
|
|
{ "path": "src/components/shared/FilterBar/FilterBar.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/SearchInput.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "filter-button-group-shell",
|
|
"category": "segmented-selector",
|
|
"summary": "Settings and compact feature segmented selectors must compose FilterButtonGroup instead of recreating active-button group styling locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FilterButtonGroup.tsx",
|
|
"export": "FilterButtonGroup"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/GeneralSettingsPanel.tsx" },
|
|
{ "path": "src/components/Settings/ResourcePicker.tsx" },
|
|
{ "path": "src/components/Settings/ReportingPanel.tsx" },
|
|
{ "path": "src/features/patrol/PatrolIntelligenceHeader.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/ResourcePicker.tsx",
|
|
"patterns": [
|
|
"min-h-10 sm:min-h-9 min-w-10 px-3 py-2 rounded-md text-sm font-medium transition-all",
|
|
"<For each={RESOURCE_PICKER_TYPE_FILTERS}>"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/FilterButtonGroup.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "chart-visibility-toggle-button",
|
|
"category": "display-toggle",
|
|
"summary": "Summary-bearing filter surfaces must compose ChartVisibilityToggleButton for chart show/hide display actions instead of recreating one-off chart toggle buttons.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FilterToolbar.tsx",
|
|
"export": "ChartVisibilityToggleButton"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/Workloads/WorkloadsFilter.tsx" }],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/FilterToolbar.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "column-picker-shell",
|
|
"category": "column-picker",
|
|
"summary": "Table column visibility controls must compose ColumnPicker instead of recreating column chooser trigger, panel, reset, or hidden-count chrome locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/ColumnPicker.tsx",
|
|
"export": "ColumnPicker"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Workloads/WorkloadsFilter.tsx" },
|
|
{ "path": "src/features/standalone/AgentsMachinesTable.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/ColumnPicker.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "selection-card-group-shell",
|
|
"category": "selection-card-group",
|
|
"summary": "Selectable settings card groups must compose SelectionCardGroup instead of recreating compact/detail active-card styling and disabled selection behavior locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/SelectionCardGroup.tsx",
|
|
"export": "SelectionCardGroup"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/AISettingsDialogs.tsx" },
|
|
{ "path": "src/components/Settings/UpdatesSettingsPanel.tsx" }
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/SelectionCardGroup.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "grouped-table-mode-segmented-control",
|
|
"category": "table-mode-control",
|
|
"summary": "Grouped/list table-mode controls must compose GroupedTableModeSegmentedControl instead of recreating grouped/list labels, titles, or icons locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/GroupedTableModeSegmentedControl.tsx",
|
|
"export": "GroupedTableModeSegmentedControl"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/Workloads/WorkloadsFilter.tsx" }],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
}
|
|
],
|
|
"patternGuards": [
|
|
{
|
|
"id": "patrol-ai-local-loading-spinner-shell",
|
|
"category": "loading-indicator",
|
|
"summary": "Patrol and AI finding surfaces must not recreate border-based loading spinners locally; compose LoadingSpinner so size, tone, and accessibility behavior stay shared.",
|
|
"canonical": {
|
|
"path": "src/components/shared/LoadingSpinner.tsx",
|
|
"export": "LoadingSpinner"
|
|
},
|
|
"scopes": [
|
|
"src/components/patrol",
|
|
"src/features/patrol",
|
|
"src/components/AI/FindingsPanel.tsx"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["border-t-transparent", "animate-spin"],
|
|
"legacyReason": "Retired migration debt. Patrol action-pending, investigation-loading, header-saving, and AI finding-loading spinners compose LoadingSpinner for shared shell, sizing, tone, and accessible status behavior.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/LoadingSpinner.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-shell-local-frame",
|
|
"category": "platform-table-shell",
|
|
"summary": "Platform tables must compose the shared PlatformTableShell frame instead of recreating the TableCard/TableHeader/TableBody shell locally.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableShell"
|
|
},
|
|
"scopes": ["src/features", "src/components"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"TableCard class={PLATFORM_TABLE_CARD_CLASS}",
|
|
"TableRow class={PLATFORM_TABLE_HEADER_ROW_CLASS}",
|
|
"TableBody class={PLATFORM_TABLE_BODY_CLASS}"
|
|
],
|
|
"legacyReason": "Retired migration debt. Platform table frames must use PlatformTableShell; no local-frame exceptions are currently allowed.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-empty-state-local-empty-state",
|
|
"category": "platform-table-empty-state",
|
|
"summary": "Platform feature tables must compose PlatformTableEmptyState for their table/card empty-state shell instead of importing EmptyState directly.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableEmptyState"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["@/components/shared/EmptyState"],
|
|
"legacyReason": "Retired migration debt. Platform table empty states must use PlatformTableEmptyState; no platform feature exceptions are currently allowed.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "table-card-frame-local-wrapper",
|
|
"category": "table-card",
|
|
"summary": "Product table surfaces must not copy the retired local bordered overflow-hidden table wrapper owned by TableCard.",
|
|
"canonical": {
|
|
"path": "src/components/shared/TableCard.tsx",
|
|
"export": "TableCard"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["overflow-hidden border-border-subtle bg-surface"],
|
|
"legacyReason": "Retired migration debt. Product table frames must use TableCard; local bordered overflow-hidden table wrappers are not allowed.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "table-card-header-local-summary-header",
|
|
"category": "table-card",
|
|
"summary": "Product table surfaces must not reintroduce the retired SummaryTableCardHeader local alias.",
|
|
"canonical": {
|
|
"path": "src/components/shared/TableCardHeader.tsx",
|
|
"export": "TableCardHeader"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["SummaryTableCardHeader"],
|
|
"legacyReason": "Retired migration debt. Table-card title/action/clear chrome must use TableCardHeader; no local header aliases are allowed.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "compact-info-card-frame-local-shell",
|
|
"category": "info-card",
|
|
"summary": "Compact drawer, discovery, resource-detail, and shared overview cards must not copy the bordered bg-surface p-3 shadow shell owned by InfoCardFrame.",
|
|
"canonical": {
|
|
"path": "src/components/shared/InfoCardFrame.tsx",
|
|
"export": "InfoCardFrame"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".ts", ".tsx"],
|
|
"allPatterns": ["rounded border border-border bg-surface p-3 shadow-sm"],
|
|
"legacyReason": "Retired migration debt. Repeated compact information card frames must use InfoCardFrame; local card-shell copies are not allowed.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/InfoCardFrame.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-loading-state-local-status-row",
|
|
"category": "platform-table-loading-state",
|
|
"summary": "Platform feature loading states must compose PlatformTableLoadingState instead of recreating the compact TableCard status row locally.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableLoadingState"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["role=\"status\"", "px-3 py-2 text-xs text-muted"],
|
|
"legacyReason": "Retired migration debt. Platform table loading states must use PlatformTableLoadingState; no compact local status-row exceptions are currently allowed.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "kubernetes-platform-table-local-text-value-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Kubernetes platform tables must not recreate local textValue helpers for trimmed-string empty-cell fallback.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableTextValue"
|
|
},
|
|
"scopes": ["src/features/kubernetes"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const textValue", "asTrimmedString(value) || '—'"],
|
|
"legacyReason": "Retired migration debt. Kubernetes platform table text cells must use formatPlatformTableTextValue for the shared trimmed-string empty-cell marker.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "truenas-platform-table-local-title-case-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "TrueNAS platform tables must not recreate local titleCase helpers for status/state label fallback.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableTitleCaseValue"
|
|
},
|
|
"scopes": ["src/features/truenas"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const titleCase", "asTrimmedString(value)", "return 'Unknown'"],
|
|
"legacyReason": "Retired migration debt. TrueNAS platform table status/state labels must use formatPlatformTableTitleCaseValue for the shared title-case fallback.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-compact-list-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local compactList helpers for compact value summaries.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "summarizePlatformTableValues"
|
|
},
|
|
"scopes": ["src/features/kubernetes", "src/features/truenas", "src/features/vmware"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const compactList"],
|
|
"legacyReason": "Retired migration debt. Platform table compact list normalization must use summarizePlatformTableValues for shared trimming and normalized values.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-value-summary-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local summarizeValues helpers for compact value summaries.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "summarizePlatformTableValues"
|
|
},
|
|
"scopes": ["src/features/kubernetes", "src/features/truenas", "src/features/vmware"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const summarizeValues"],
|
|
"legacyReason": "Retired migration debt. Platform table compact value summaries must use summarizePlatformTableValues for shared labels, overflow counts, and title text.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-uptime-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local one-unit formatUptime helpers for uptime label fallback.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableUptimeValue"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone",
|
|
"src/features/truenas"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const formatUptime", "seconds / 86_400", "return `${mins}m`"],
|
|
"legacyReason": "Retired migration debt. Platform table uptime labels must use formatPlatformTableUptimeValue for the shared one-unit uptime fallback.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-bytes-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local formatBytes wrappers for positive byte-size empty-cell fallback.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableBytesValue"
|
|
},
|
|
"scopes": ["src/features/docker", "src/features/kubernetes", "src/features/truenas"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const formatBytes", "value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)"],
|
|
"legacyReason": "Retired migration debt. Platform table byte-size cells must use formatPlatformTableBytesValue for shared positive-byte formatting and empty-cell markers.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-format-bytes-import",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform feature tables must not import the generic byte formatter directly for dense table byte cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableBytesValue"
|
|
},
|
|
"scopes": ["src/features/truenas"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["import { formatBytes } from '@/utils/format';", "formatBytes("],
|
|
"legacyReason": "Retired migration debt. Dense platform table byte cells must use formatPlatformTableBytesValue so positive-byte formatting and empty markers stay shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-number-value-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local numberValue helpers for optional numeric table cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableNumberValue"
|
|
},
|
|
"scopes": ["src/features/docker", "src/features/kubernetes"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["<span class=\"tabular-nums\">{value}</span>", "<span>—</span>"],
|
|
"legacyReason": "Retired migration debt. Optional numeric platform table cells must use PlatformTableNumberValue for finite-number checks, tabular styling, and empty-cell markers.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-count-cell-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local countCell helpers for locale-formatted optional numeric table cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableNumberValue"
|
|
},
|
|
"scopes": ["src/features/proxmox"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const countCell", "value.toLocaleString() : '—'"],
|
|
"legacyReason": "Retired migration debt. Locale-formatted count cells must compose PlatformTableNumberValue with a caller-owned format function instead of owning fallback markup locally.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-replica-count-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local replicaCount helpers for scheduler/service count columns.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableNumberValue"
|
|
},
|
|
"scopes": ["src/features/docker", "src/features/kubernetes"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const replicaCount", "<span class=\"tabular-nums\">{value ?? 0}</span>"],
|
|
"legacyReason": "Retired migration debt. Scheduler and service count cells must compose PlatformTableNumberValue with caller-owned zero-default semantics instead of local tabular span helpers.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-swarm-count-spans",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Docker Swarm table drawers must not render desired/running service counts with local tabular spans.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableNumberValue"
|
|
},
|
|
"scopes": ["src/components/Docker"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["desiredTasks", "runningTasks", "class=\"tabular-nums\""],
|
|
"legacyReason": "Retired migration debt. Swarm desired/running count cells must route through PlatformTableNumberValue so drawer tables and platform tables share numeric cell rendering.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-count-ratio-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local healthy/total count-ratio helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableCountRatioValue"
|
|
},
|
|
"scopes": ["src/features/kubernetes"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const childCountCell", "<span class=\"text-muted\">/{count.total}</span>"],
|
|
"legacyReason": "Retired migration debt. Healthy/total and ready/total table count cells must compose PlatformTableCountRatioValue so ratio markup, tabular styling, muted denominator styling, and empty markers stay shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-truenas-count-cell-class",
|
|
"category": "platform-table-text-value",
|
|
"summary": "TrueNAS table scalar value cells must not put tabular-number styling on table cells instead of shared value primitives.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableNumberValue"
|
|
},
|
|
"scopes": ["src/features/truenas"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["TrueNAS", "hidden text-base-content tabular-nums lg:table-cell"],
|
|
"legacyReason": "Retired migration debt. TrueNAS share/service and adjacent scalar value cells must render through shared platform value primitives so finite-number checks, tabular styling, and empty markers stay shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-percent-value-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local formatPercent helpers for one-decimal percent cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTablePercentValue"
|
|
},
|
|
"scopes": ["src/features/truenas"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"const formatPercent",
|
|
"<span class=\"tabular-nums\">{percent.toFixed(1)}%</span>"
|
|
],
|
|
"legacyReason": "Retired migration debt. One-decimal percent cells must compose PlatformTablePercentValue so percent formatting, tabular styling, and empty markers stay shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-temperature-value-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local formatTemperature helpers for positive Celsius cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableTemperatureValue"
|
|
},
|
|
"scopes": ["src/features/docker", "src/features/truenas"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"const formatTemperature",
|
|
"<span class=\"tabular-nums\">{celsius.toFixed(1)}°C</span>"
|
|
],
|
|
"legacyReason": "Retired migration debt. Positive Celsius table cells must compose PlatformTableTemperatureValue so validity, formatting, tabular styling, and empty markers stay shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-temperature-label-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local temperatureLabel helpers for positive Celsius cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableTemperatureValue"
|
|
},
|
|
"scopes": ["src/features/truenas"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const temperatureLabel", "${Math.round(value)}C"],
|
|
"legacyReason": "Retired migration debt. Positive Celsius table cells must compose PlatformTableTemperatureValue so validity, formatting, tabular styling, and empty markers stay shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-metric-fallback-helper",
|
|
"category": "platform-table-metric",
|
|
"summary": "Platform tables must not recreate local finiteMetric and metricFallback helpers for metric cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableMetricFallback"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const finiteMetric", "const metricFallback", "Number.isFinite(value)"],
|
|
"legacyReason": "Retired migration debt. Platform table metric cells must use PlatformTableMetricFallback and getPlatformTableFiniteMetric instead of carrying page-local metric fallback helpers.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-local-metric-empty-marker",
|
|
"category": "platform-table-metric",
|
|
"summary": "Platform metric cells must not recreate centered muted dash fallback markup inline.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableMetricFallback"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["flex justify-center", "text-xs text-muted", "aria-hidden=\"true\""],
|
|
"legacyReason": "Retired migration debt. Platform metric cells must compose PlatformTableMetricFallback instead of carrying inline centered dash fallback markup.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-section-tabs-local-nav",
|
|
"category": "platform-section-tabs",
|
|
"summary": "Platform page surfaces must not recreate the active-link tab navigation owned by PlatformSectionTabs.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformSectionTabs"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["aria-current={", "border-b-2", "border-b border-border"],
|
|
"legacyReason": "Retired migration debt. Platform page section tabs must use PlatformSectionTabs; no page-local tab-bar exceptions are currently allowed.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "chart-visibility-local-toggle-labels",
|
|
"category": "display-toggle",
|
|
"summary": "Summary-bearing surfaces must not copy the Show charts / Hide charts display-toggle labels owned by ChartVisibilityToggleButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FilterToolbar.tsx",
|
|
"export": "ChartVisibilityToggleButton"
|
|
},
|
|
"scopes": ["src/components/Workloads", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["Show charts", "Hide charts"],
|
|
"legacyReason": "Retired migration debt. Chart visibility display actions must use ChartVisibilityToggleButton; local show/hide chart toggle copies are not allowed.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/Workloads/__tests__/WorkloadsFilter.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "column-picker-local-column-chooser",
|
|
"category": "column-picker",
|
|
"summary": "Table surfaces must not copy the column chooser title and panel copy owned by ColumnPicker.",
|
|
"canonical": {
|
|
"path": "src/components/shared/ColumnPicker.tsx",
|
|
"export": "ColumnPicker"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["Choose which columns to display", "Show Columns"],
|
|
"legacyReason": "Retired migration debt. Table column visibility controls must use ColumnPicker; local column chooser copies are not allowed.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/ColumnPicker.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "selection-card-group-local-active-card-styles",
|
|
"category": "selection-card-group",
|
|
"summary": "Settings and feature surfaces must not copy the compact/detail active-card styling owned by SelectionCardGroup.",
|
|
"canonical": {
|
|
"path": "src/components/shared/selectionCardGroupModel.ts",
|
|
"export": "getSelectionCardButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"p-4 rounded-md border-2 transition-all text-left",
|
|
"p-3 rounded-md border-2 transition-all text-center"
|
|
],
|
|
"legacyReason": "Retired migration debt. Selectable settings card groups must use SelectionCardGroup; local compact/detail active-card styling forks are not allowed.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "grouped-table-mode-local-segmented-control",
|
|
"category": "table-mode-control",
|
|
"summary": "Resource surfaces must not copy the grouped/list table-mode segmented-control labels owned by GroupedTableModeSegmentedControl.",
|
|
"canonical": {
|
|
"path": "src/components/shared/GroupedTableModeSegmentedControl.tsx",
|
|
"export": "GroupedTableModeSegmentedControl"
|
|
},
|
|
"scopes": ["src/components", "src/features"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["Grouped table view", "Flat list view"],
|
|
"legacyReason": "Retired migration debt. Grouped/list table-mode controls must use GroupedTableModeSegmentedControl; no local label/title copies are currently allowed.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "filter-bar-consumer-legacy-page-controls",
|
|
"category": "filter-bar",
|
|
"summary": "Page and feature filter surfaces must not import the legacy PageControls deck when they should be expressed as a FilterBar catalog.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FilterBar/FilterBar.tsx",
|
|
"export": "FilterBar"
|
|
},
|
|
"scopes": ["src/components", "src/features"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["@/components/shared/PageControls"],
|
|
"legacyReason": "Retired migration debt. Resource-list filter surfaces must compose FilterBar; no page-level filter exceptions are currently allowed.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "search-control-native-search-input",
|
|
"category": "search-control",
|
|
"summary": "Product surfaces must not render native type=search inputs; they must compose SearchField or SearchInput so search chrome, clear behavior, keyboard handling, and aria labels stay canonical.",
|
|
"canonical": {
|
|
"path": "src/components/shared/SearchField.tsx",
|
|
"export": "SearchField"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["type=\"search\""],
|
|
"legacyReason": "Retired migration debt. Search controls must use SearchField or SearchInput; native search inputs are not allowed in product surfaces.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "status-dot-local-linked-disk-health-dot",
|
|
"category": "status-indicator",
|
|
"summary": "Storage linked-disk health rows must not recreate the retired green/yellow raw dot classes owned by StatusDot.",
|
|
"canonical": {
|
|
"path": "src/components/shared/StatusDot.tsx",
|
|
"export": "StatusDot"
|
|
},
|
|
"scopes": ["src/components/Storage", "src/features/storageBackups"],
|
|
"extensions": [".ts", ".tsx"],
|
|
"allPatterns": ["h-2 w-2 rounded-full bg-yellow-500", "h-2 w-2 rounded-full bg-green-500"],
|
|
"legacyReason": "Retired migration debt. Linked-disk health indicators must use StatusDot variants instead of local green/yellow dot classes.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "status-indicator-badge-local-tone-helper",
|
|
"category": "status-indicator",
|
|
"summary": "Product components must compose StatusIndicatorBadge instead of calling getStatusIndicatorBadgeToneClasses directly for read-only health/state badges.",
|
|
"canonical": {
|
|
"path": "src/components/shared/StatusIndicatorBadge.tsx",
|
|
"export": "StatusIndicatorBadge"
|
|
},
|
|
"scopes": ["src/components", "src/features"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["getStatusIndicatorBadgeToneClasses("],
|
|
"legacyReason": "Retired migration debt. Read-only status badges must route through StatusIndicatorBadge; status utilities may still expose tone classes for the shared primitive and non-component tests.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/StatusIndicatorBadge.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-severity-local-helper",
|
|
"category": "status-indicator",
|
|
"summary": "Platform alert tables must not recreate severityVariant/severityTextClass helpers; alert severity tone and label presentation belongs to AlertSeverityBadge and alertSeverityPresentation.",
|
|
"canonical": {
|
|
"path": "src/components/shared/AlertSeverityBadge.tsx",
|
|
"export": "AlertSeverityBadge"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["severityVariant", "severityTextClass"],
|
|
"legacyReason": "Retired migration debt. Platform alert rows must compose AlertSeverityBadge and AlertSeverityDot instead of page-local severity tone helpers.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/AlertSeverityBadge.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-severity-local-detail-tone-helper",
|
|
"category": "detail-tone",
|
|
"summary": "Platform alert tables must not recreate alertTone severity detail-row helpers; detail tone mapping belongs to alertSeverityPresentation.",
|
|
"canonical": {
|
|
"path": "src/utils/alertSeverityPresentation.ts",
|
|
"export": "getAlertSeverityDetailTone"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const alertTone", "severityBucket"],
|
|
"legacyReason": "Retired migration debt. Platform alert detail rows must call getAlertSeverityDetailTone instead of carrying page-local severity tone helpers.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/utils/__tests__/alertSeverityPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-local-code-formatter-helper",
|
|
"category": "detail-formatting",
|
|
"summary": "Platform alert tables must not recreate provider-code formatter helpers; alert code labels belong to alertDetailPresentation.",
|
|
"canonical": {
|
|
"path": "src/utils/alertDetailPresentation.ts",
|
|
"export": "formatPlatformAlertCode"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const formatCode", "IncidentRow"],
|
|
"legacyReason": "Retired migration debt. Platform alert code labels must call formatPlatformAlertCode instead of carrying page-local prefix stripping and title casing.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/utils/__tests__/alertDetailPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-local-resource-type-formatter-helper",
|
|
"category": "detail-formatting",
|
|
"summary": "Platform alert tables must not recreate resource-type formatter helpers; provider-specific resource labels belong to alertDetailPresentation.",
|
|
"canonical": {
|
|
"path": "src/utils/alertDetailPresentation.ts",
|
|
"export": "formatPlatformAlertResourceType"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const formatResourceType", "IncidentRow"],
|
|
"legacyReason": "Retired migration debt. Platform alert resource-type labels must call formatPlatformAlertResourceType instead of carrying page-local switch maps.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/utils/__tests__/alertDetailPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-local-started-at-formatter-helper",
|
|
"category": "detail-formatting",
|
|
"summary": "Platform alert tables must not recreate started-at timestamp formatter helpers; alert timestamp labels belong to alertDetailPresentation.",
|
|
"canonical": {
|
|
"path": "src/utils/alertDetailPresentation.ts",
|
|
"export": "formatPlatformAlertStartedAt"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const formatStartedAt", "IncidentRow"],
|
|
"legacyReason": "Retired migration debt. Platform alert row timestamps must call formatPlatformAlertStartedAt instead of carrying page-local date formatting.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/utils/__tests__/alertDetailPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-local-detail-date-formatter-helper",
|
|
"category": "detail-formatting",
|
|
"summary": "Platform alert tables must not recreate detail timestamp formatter helpers; alert detail date labels belong to alertDetailPresentation.",
|
|
"canonical": {
|
|
"path": "src/utils/alertDetailPresentation.ts",
|
|
"export": "formatPlatformAlertDetailDateTime"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const detailDateTime", "IncidentRow"],
|
|
"legacyReason": "Retired migration debt. Platform alert detail timestamps must call formatPlatformAlertDetailDateTime instead of carrying page-local date formatting.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/utils/__tests__/alertDetailPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-local-entity-type-formatter-helper",
|
|
"category": "detail-formatting",
|
|
"summary": "vSphere alert tables must not recreate entity-type formatter helpers; vSphere alert entity labels belong to alertDetailPresentation.",
|
|
"canonical": {
|
|
"path": "src/utils/alertDetailPresentation.ts",
|
|
"export": "formatPlatformAlertEntityType"
|
|
},
|
|
"scopes": ["src/features/vmware"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const formatEntityType", "VmwareIncidentRow"],
|
|
"legacyReason": "Retired migration debt. vSphere alert entity labels must call formatPlatformAlertEntityType instead of carrying page-local normalization.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/utils/__tests__/alertDetailPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "patrol-finding-metadata-badge-local-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Patrol finding-row metadata chips must compose MetadataBadge instead of recreating local bordered xs badge spans.",
|
|
"canonical": {
|
|
"path": "src/components/shared/MetadataBadge.tsx",
|
|
"export": "MetadataBadge"
|
|
},
|
|
"scopes": ["src/components/AI"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["px-1.5 py-0.5 border text-[10px] font-medium rounded"],
|
|
"legacyReason": "Retired migration debt. Patrol finding row state, source, severity, investigation, confidence, and regression chips use MetadataBadge for shared shell, sizing, tone, and outline behavior.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/MetadataBadge.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "patrol-investigation-metadata-badge-local-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Patrol investigation metadata chips must compose MetadataBadge instead of recreating local bordered xs status, outcome, record, or tool badges.",
|
|
"canonical": {
|
|
"path": "src/components/shared/MetadataBadge.tsx",
|
|
"export": "MetadataBadge"
|
|
},
|
|
"scopes": ["src/components/patrol"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["px-1.5 py-0.5 border text-[10px] font-medium rounded"],
|
|
"legacyReason": "Retired migration debt. Patrol investigation status, outcome, durable-record, and tool chips use MetadataBadge for shared shell, sizing, tone, and outline behavior.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/patrol/__tests__/InvestigationSection.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "patrol-approval-tool-call-metadata-badge-local-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Patrol approval and tool-call metadata chips must compose MetadataBadge instead of recreating local xs risk, expiry, or result badges.",
|
|
"canonical": {
|
|
"path": "src/components/shared/MetadataBadge.tsx",
|
|
"export": "MetadataBadge"
|
|
},
|
|
"scopes": ["src/components/patrol"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["px-1.5 py-0.5", "text-[10px] font-medium rounded"],
|
|
"legacyReason": "Retired migration debt. Patrol approval risk, approval state, and tool-call result chips use MetadataBadge for shared shell, sizing, tone, and whitespace behavior.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/patrol/__tests__/ApprovalSection.test.tsx",
|
|
"src/components/patrol/__tests__/RunToolCallTrace.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "patrol-run-metadata-badge-local-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Patrol run-history metadata chips must compose MetadataBadge instead of recreating local rounded resource or outcome pill spans.",
|
|
"canonical": {
|
|
"path": "src/components/shared/MetadataBadge.tsx",
|
|
"export": "MetadataBadge"
|
|
},
|
|
"scopes": ["src/components/patrol"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["inline-flex items-center gap-1", "rounded-full text-xs font-medium"],
|
|
"legacyReason": "Retired migration debt. Patrol run-history resource and outcome chips use MetadataBadge for shared shell, sizing, tone, and whitespace behavior.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/MetadataBadge.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "proxmox-backup-metadata-badge-local-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Proxmox backup source and state metadata chips must compose MetadataBadge instead of recreating local rounded-sm xs badge spans.",
|
|
"canonical": {
|
|
"path": "src/components/shared/MetadataBadge.tsx",
|
|
"export": "MetadataBadge"
|
|
},
|
|
"scopes": ["src/features/proxmox"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["rounded-sm px-1.5 py-0.5 text-[10px] font-semibold"],
|
|
"legacyReason": "Retired migration debt. Proxmox backup source/state chips use MetadataBadge for shared shell, sizing, tone vocabulary, and whitespace behavior.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/proxmox/__tests__/proxmoxBackupsTableShared.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "organization-role-badge-local-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Organization settings sections must not call roleBadgeClass directly; compose OrganizationRoleBadge so role tone mapping and badge shell stay canonical.",
|
|
"canonical": {
|
|
"path": "src/components/shared/OrganizationBadges.tsx",
|
|
"export": "OrganizationRoleBadge"
|
|
},
|
|
"scopes": ["src/components/Settings"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["roleBadgeClass("],
|
|
"legacyReason": "Retired migration debt. Organization role badges belong to OrganizationRoleBadge and MetadataBadge.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/OrganizationBadges.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "organization-share-status-badge-local-shell",
|
|
"category": "metadata-badge",
|
|
"summary": "Organization share tables must not recreate local statusBadgeClass helpers; compose OrganizationShareStatusBadge for canonical pending/active labels and tones.",
|
|
"canonical": {
|
|
"path": "src/components/shared/OrganizationBadges.tsx",
|
|
"export": "OrganizationShareStatusBadge"
|
|
},
|
|
"scopes": ["src/components/Settings"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["statusBadgeClass("],
|
|
"legacyReason": "Retired migration debt. Organization share status badges belong to OrganizationShareStatusBadge and MetadataBadge.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/OrganizationBadges.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "form-select-local-native-select",
|
|
"category": "form-control",
|
|
"summary": "Product and shared filter components must compose FormSelect instead of recreating labelled native select shells locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FormSelect.tsx",
|
|
"export": "FormSelect"
|
|
},
|
|
"scopes": ["src/components", "src/features"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["<select"],
|
|
"legacyReason": "Retired migration debt. Native select controls must use FormSelect for label/id/select chrome wiring and dynamic option value synchronization.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Infrastructure/__tests__/ResourceDetailDrawer.docker-container.test.tsx",
|
|
"src/components/Infrastructure/__tests__/ResourceDetailDrawer.machine-history.test.tsx",
|
|
"src/components/shared/FilterToolbar.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "form-textarea-local-alert-fields",
|
|
"category": "form-control",
|
|
"summary": "Alert destination and incident timeline textareas must compose FormTextarea; only the resource note editors remain as bounded residuals until their editor-specific migration.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FormTextarea.tsx",
|
|
"export": "FormTextarea"
|
|
},
|
|
"scopes": ["src/components/Alerts", "src/features/alerts"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["<textarea"],
|
|
"legacyReason": "Bounded residual. Alert resource note editors still own row/mobile-specific editing behavior; new alert textareas must use FormTextarea.",
|
|
"allowedPaths": [
|
|
"src/components/Alerts/AlertResourceTableMobile.tsx",
|
|
"src/components/Alerts/AlertResourceTableRow.tsx"
|
|
],
|
|
"ignoredPaths": [
|
|
"src/components/Alerts/EmailProviderSelect.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "form-textarea-local-settings-fields",
|
|
"category": "form-control",
|
|
"summary": "Settings textareas must compose FormTextarea; the SSO provider editor remains as bounded residual until its multi-field provider metadata editor is migrated.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FormTextarea.tsx",
|
|
"export": "FormTextarea"
|
|
},
|
|
"scopes": ["src/components/Settings"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["<textarea"],
|
|
"legacyReason": "Bounded residual. SSO provider metadata textareas still require a dedicated editor migration; new settings textareas must use FormTextarea.",
|
|
"allowedPaths": ["src/components/Settings/SSOProvidersPanel.tsx"],
|
|
"ignoredPaths": [
|
|
"src/components/Settings/__tests__/ProLicensePanel.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "form-textarea-local-infrastructure-fields",
|
|
"category": "form-control",
|
|
"summary": "Infrastructure modal textareas must compose FormTextarea instead of recreating labelled native textarea wrappers locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FormTextarea.tsx",
|
|
"export": "FormTextarea"
|
|
},
|
|
"scopes": ["src/components/Infrastructure"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["<textarea"],
|
|
"legacyReason": "Retired migration debt. Infrastructure form textareas must use FormTextarea for label/id/help wiring.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "toggle-local-role-switch",
|
|
"category": "toggle-control",
|
|
"summary": "Binary switch-looking controls must compose Toggle or TogglePrimitive instead of hand-rolling local role=switch buttons with aria-checked and custom track/knob classes.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Toggle.tsx",
|
|
"export": "Toggle"
|
|
},
|
|
"scopes": ["src/components", "src/features"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["role=\"switch\"", "aria-checked"],
|
|
"legacyReason": "Retired migration debt. Local switch role buttons must route through the shared Toggle primitive; radios, checkboxes, and selection controls are governed separately.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Alerts/ResourceTable.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Toggle.test.tsx"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Toggle.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "filter-chip-status-dot-local-factory",
|
|
"category": "filter-bar",
|
|
"summary": "Filter option status-dot leading icons must use the FilterBar presentation helper instead of page-local span factories.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FilterBar/filterOptionPresentation.tsx",
|
|
"export": "filterChipStatusDot"
|
|
},
|
|
"scopes": ["src/components", "src/features"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["h-2 w-2 rounded-full ${className}"],
|
|
"legacyReason": "Retired migration debt. Filter chip leading-dot presentation is owned by the FilterBar primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "filter-button-group-local-segmented-control-styles",
|
|
"category": "segmented-selector",
|
|
"summary": "Settings and feature surfaces must not copy the compact active-button segmented-control styling owned by FilterButtonGroup.",
|
|
"canonical": {
|
|
"path": "src/components/shared/filterButtonGroupModel.ts",
|
|
"export": "getFilterButtonGroupButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"flex items-center bg-base rounded-md p-1 border shadow-inner",
|
|
"flex-1 py-1.5 px-2 text-xs font-semibold rounded-md transition-all duration-200"
|
|
],
|
|
"legacyReason": "Retired migration debt. Settings and compact feature segmented selectors must use FilterButtonGroup; local active-button selector styling forks are not allowed.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-secondary-command-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Product surfaces must not copy the standard secondary command-button shell; use Button for buttons and ButtonLink for route/link actions.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"rounded-md border border-border bg-surface px-3 py-1.5 text-xs font-medium text-base-content"
|
|
],
|
|
"legacyReason": "Retired migration debt. Standard secondary command buttons belong to the shared Button primitive family, including button-styled links.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-command-copy-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Command-copy controls must not recreate the absolute top-right icon-button shell; use CommandCopyButton from the shared Button primitive family.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "CommandCopyButton"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"absolute right-2 top-2",
|
|
"items-center justify-center",
|
|
"bg-surface-hover p-2"
|
|
],
|
|
"legacyReason": "Retired migration debt. Command-copy icon buttons belong to CommandCopyButton so position, sizing, icon, focus, disabled, and aria behavior stay canonical.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-drawer-header-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Drawer header command actions must compose DrawerHeaderActionButton for shared height, density, focus, disabled, and hover styling instead of copying drawer-local button classes.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "DrawerHeaderActionButton"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex h-8 items-center gap-1.5 rounded border border-border bg-surface px-2 text-xs font-medium text-base-content transition-colors hover:bg-surface-hover focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
|
],
|
|
"legacyReason": "Retired migration debt. Drawer header Assistant and context-copy actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-drawer-header-icon-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Drawer header icon actions must compose DrawerHeaderIconButton for shared close/action chrome instead of copying drawer-local icon button classes.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "DrawerHeaderIconButton"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex h-8 w-8 items-center justify-center rounded-md hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500"
|
|
],
|
|
"legacyReason": "Retired migration debt. Drawer header close and icon actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "upgrade-action-local-class-helper",
|
|
"category": "upgrade-action",
|
|
"summary": "Product surfaces must not use a class helper to make upgrade CTAs look like buttons; compose UpgradeButtonLink instead.",
|
|
"canonical": {
|
|
"path": "src/components/shared/UpgradeLink.tsx",
|
|
"export": "UpgradeButtonLink"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["getUpgradeActionButtonClass"],
|
|
"legacyReason": "Retired migration debt. Upgrade action button shape, routing, and link safety belong to UpgradeButtonLink.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/UpgradeLink.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "upgrade-action-local-button-shell",
|
|
"category": "upgrade-action",
|
|
"summary": "Product surfaces must not style UpgradeLink as a local button; use UpgradeButtonLink for button-styled upgrade actions and reserve UpgradeLink for inline text links.",
|
|
"canonical": {
|
|
"path": "src/components/shared/UpgradeLink.tsx",
|
|
"export": "UpgradeButtonLink"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"<UpgradeLink",
|
|
"min-h-10 sm:min-h-9 rounded-md border",
|
|
"text-xs font-medium"
|
|
],
|
|
"legacyReason": "Retired migration debt. Button-styled upgrade actions must use UpgradeButtonLink; inline copy links may continue using UpgradeLink.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Settings/AIRuntimeControlsSection.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/UpgradeLink.test.tsx"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/UpgradeLink.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "copy-value-neutral-local-button-shell",
|
|
"category": "copy-action",
|
|
"summary": "Copy-value controls must not copy the neutral bordered icon-button shell; use CopyValueButton with the neutral variant.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "CopyValueButton"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex min-h-7 min-w-7 shrink-0 items-center justify-center rounded border border-border bg-surface px-2 text-muted transition-colors hover:bg-surface-hover hover:text-base-content",
|
|
"fallback={<CopyIcon class=\"h-3.5 w-3.5\" />}"
|
|
],
|
|
"legacyReason": "Retired migration debt. Neutral copy-value icon buttons belong to CopyValueButton.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "copy-value-muted-local-button-shell",
|
|
"category": "copy-action",
|
|
"summary": "Copy-value controls must not copy the muted icon-button shell; use CopyValueButton with the ghost variant.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "CopyValueButton"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex min-h-8 min-w-8 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content",
|
|
"fallback={<CopyIcon class=\"h-3.5 w-3.5\" />}"
|
|
],
|
|
"legacyReason": "Retired migration debt. Muted copy-value icon buttons belong to CopyValueButton.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "copy-value-accent-local-button-shell",
|
|
"category": "copy-action",
|
|
"summary": "Copy-value controls must not copy the blue suggested-value icon-button shell; use CopyValueButton with the accent variant.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "CopyValueButton"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex min-h-7 min-w-7 shrink-0 items-center justify-center rounded text-blue-700 transition-colors hover:bg-blue-100 dark:text-blue-200 dark:hover:bg-blue-950",
|
|
"fallback={<CopyIcon class=\"h-3.5 w-3.5\" />}"
|
|
],
|
|
"legacyReason": "Retired migration debt. Accent copy-value icon buttons belong to CopyValueButton.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "copyable-code-row-local-shell",
|
|
"category": "copy-action",
|
|
"summary": "Copyable code/value rows must not copy the local surface-alt font-mono row shell; use CopyableCodeRow.",
|
|
"canonical": {
|
|
"path": "src/components/shared/CopyableCodeRow.tsx",
|
|
"export": "CopyableCodeRow"
|
|
},
|
|
"scopes": ["src/components", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"flex items-start gap-2 rounded bg-surface-alt px-2 py-1.5",
|
|
"break-all font-mono text-xs text-base-content"
|
|
],
|
|
"legacyReason": "Retired migration debt. Copyable code rows belong to CopyableCodeRow so code styling and copy behavior stay canonical.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "inline-detail-local-field-grid-shell",
|
|
"category": "row-detail",
|
|
"summary": "Inline platform detail content must not recreate local DetailField grids; use DetailSectionTable or InlineDetailPanel.",
|
|
"canonical": {
|
|
"path": "src/components/shared/DetailSectionTable.tsx",
|
|
"export": "InlineDetailPanel"
|
|
},
|
|
"scopes": [
|
|
"src/components/Infrastructure",
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"const DetailField",
|
|
"font-semibold uppercase tracking-wide text-muted",
|
|
"grid gap-3 sm:grid-cols-2 lg:grid-cols-3"
|
|
],
|
|
"legacyReason": "Retired migration debt. Inline detail field grids belong to DetailSectionTable / InlineDetailPanel so detail sections, value tones, truncation, and close actions stay canonical.",
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/DetailSectionTable.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "inline-detail-provider-named-primitive-import",
|
|
"category": "row-detail",
|
|
"summary": "Platform-neutral detail section tables must not import the retired provider-named TrueNAS detail primitive.",
|
|
"canonical": {
|
|
"path": "src/components/shared/DetailSectionTable.tsx",
|
|
"export": "DetailSectionTable"
|
|
},
|
|
"scopes": ["src/components", "src/features"],
|
|
"extensions": [".ts", ".tsx"],
|
|
"allPatterns": ["TrueNASDetailTable"],
|
|
"legacyReason": "Retired migration debt. Provider-specific names must not own platform-neutral detail tables; use DetailSectionTable / detailSectionModel.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/DetailSectionTable.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-compact-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and hosted-admin surfaces must not copy compact xs action-button shells; use Button with the settingsActionXs or sm size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"rounded-md border border-border bg-surface px-3 py-2 text-xs font-medium text-base-content"
|
|
],
|
|
"legacyReason": "Retired migration debt. Compact settings and admin actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-secondary-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy the standard secondary px-3 py-2 action-button shell; use Button or ButtonLink with the mdCompact size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"rounded-md border border-border bg-surface px-3 py-2 text-sm font-medium text-base-content"
|
|
],
|
|
"legacyReason": "Retired migration debt. Standard settings-sized secondary command buttons belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-outline-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy the bordered px-3 py-2 action-button shell; use Button or ButtonLink with the outline variant and settingsAction size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"rounded-md border border-border px-3 py-2 text-sm font-medium text-base-content"
|
|
],
|
|
"legacyReason": "Retired migration debt. Standard bordered settings actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-primary-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy the blue px-3 py-2 primary action-button shell; use Button with the primary variant and settingsAction size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white"],
|
|
"legacyReason": "Retired migration debt. Primary settings actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-success-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy the emerald px-3 py-2 success action-button shell; use Button with the success variant and mdCompact or settingsAction size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["rounded-md bg-emerald-600 px-3 py-2 text-sm font-medium text-white"],
|
|
"legacyReason": "Retired migration debt. Positive settings actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-success-outline-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy the emerald outlined px-3 py-2 success action-button shell; use Button with the successOutline variant and mdCompact or settingsAction size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"rounded-md border border-emerald-300 bg-white px-3 py-2 text-sm font-medium text-emerald-900"
|
|
],
|
|
"legacyReason": "Retired migration debt. Positive outlined settings actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-success-ghost-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy the emerald ghost px-3 py-2 success action-button shell; use Button with the successGhost variant and mdCompact or settingsAction size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["rounded-md px-3 py-2 text-sm font-medium text-emerald-900"],
|
|
"legacyReason": "Retired migration debt. Positive ghost settings actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-large-reporting-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Reporting and similar settings CTA rows must not copy the large full-width action-button shell; use Button with the lg size and contextual variant.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"flex w-full items-center justify-center gap-2 rounded-md px-6 py-3 font-semibold transition-all sm:w-auto"
|
|
],
|
|
"legacyReason": "Retired migration debt. Large settings/report actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-danger-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy the rose px-3 py-2 destructive action-button shell; use Button with the danger variant and settingsAction size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["rounded-md bg-rose-600 px-3 py-2 text-sm font-medium text-white"],
|
|
"legacyReason": "Retired migration debt. Confirmed destructive settings actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-danger-outline-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy the rose outlined px-3 py-2 destructive action-button shell; use Button with the dangerOutline variant and settingsAction size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"rounded-md border border-rose-300 px-3 py-2 text-sm font-medium text-rose-700"
|
|
],
|
|
"legacyReason": "Retired migration debt. Unconfirmed destructive settings actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-outline-settings-row-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings row and inline actions must not copy the compact bordered px-2.5 py-1 action shell; use Button or ButtonLink with the outline variant and xs size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"rounded-md border border-border px-2.5 py-1 text-xs font-medium text-base-content"
|
|
],
|
|
"legacyReason": "Retired migration debt. Compact settings row actions belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "button-outline-settings-dialog-close-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings dialog close buttons must not copy the h-9 w-9 bordered icon shell; use Button with the outline variant and iconMd size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"h-9 w-9 items-center justify-center rounded-md border border-border text-base-content transition-colors hover:bg-surface-hover"
|
|
],
|
|
"legacyReason": "Retired migration debt. Settings dialog close buttons belong to the shared Button primitive family.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
}
|
|
],
|
|
"requiredPatternGuards": [
|
|
{
|
|
"id": "platform-row-detail-toggle-shared-state",
|
|
"category": "row-disclosure",
|
|
"summary": "Platform rows using shared resource-detail expansion state must render the canonical disclosure toggle instead of relying on row-only click behavior or local chevrons.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/PlatformResourceDetailTableRow.tsx",
|
|
"export": "PlatformResourceDetailToggleButton"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"triggerPatterns": ["createPlatformResourceDetailState"],
|
|
"requiredPatterns": ["PlatformResourceDetailToggleButton"],
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-severity-indicator-required",
|
|
"category": "status-indicator",
|
|
"summary": "Feature alert tables with severityBucket-driven platform rows must compose AlertSeverityBadge and AlertSeverityDot so new platform alert tables cannot invent local severity indicators.",
|
|
"canonical": {
|
|
"path": "src/components/shared/AlertSeverityBadge.tsx",
|
|
"export": "AlertSeverityBadge"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"triggerPatterns": [
|
|
"severityBucket",
|
|
"IncidentRow",
|
|
"getPlatformTableCellClassForKind('badge')"
|
|
],
|
|
"requiredPatterns": ["AlertSeverityBadge", "AlertSeverityDot"],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/AlertSeverityBadge.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-severity-filter-options-required",
|
|
"category": "filter-options",
|
|
"summary": "Feature alert tables with severityBucket-backed PlatformTableToolbar status filters must compose the shared platform alert severity filter-options primitive.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/platformAlertSeverityFilterOptions.tsx",
|
|
"export": "getPlatformAlertSeverityFilterOptions"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"triggerPatterns": ["severityBucket", "PlatformTableToolbar", "statusOptions"],
|
|
"requiredPatterns": ["getPlatformAlertSeverityFilterOptions"],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/platformAlertSeverityFilterOptions.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-alert-detail-formatters-required",
|
|
"category": "detail-formatting",
|
|
"summary": "Feature alert tables with incident-row detail code and started-at fields must compose the shared platform alert detail formatter primitive.",
|
|
"canonical": {
|
|
"path": "src/utils/alertDetailPresentation.ts",
|
|
"export": "formatPlatformAlertCode"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"triggerPatterns": ["IncidentRow", "incident.code", "incident.startedAt"],
|
|
"requiredPatterns": [
|
|
"formatPlatformAlertCode",
|
|
"formatPlatformAlertStartedAt",
|
|
"formatPlatformAlertDetailDateTime"
|
|
],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/utils/__tests__/alertDetailPresentation.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-row-detail-toggle-inline-detail",
|
|
"category": "row-disclosure",
|
|
"summary": "Platform tables that render page-local inline detail rows must compose the canonical disclosure toggle in the owning row.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/PlatformResourceDetailTableRow.tsx",
|
|
"export": "PlatformResourceDetailToggleButton"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"triggerPatterns": ["data-inline", "aria-expanded"],
|
|
"requiredPatterns": ["PlatformResourceDetailToggleButton"],
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "inline-detail-table-row-shared-shell",
|
|
"category": "row-detail",
|
|
"summary": "Files that render inline/detail table rows in platform, workload, or infrastructure surfaces must use InlineDetailTableRow for the shared row/cell/content shell.",
|
|
"canonical": {
|
|
"path": "src/components/shared/InlineDetailTableRow.tsx",
|
|
"export": "InlineDetailTableRow"
|
|
},
|
|
"scopes": [
|
|
"src/components/Infrastructure",
|
|
"src/components/Workloads",
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/platformPage",
|
|
"src/features/proxmox",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"triggerPatterns": ["data-inline", "TableRow"],
|
|
"requiredPatterns": ["InlineDetailTableRow"],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Workloads/__tests__/WorkloadsSurface.performance.contract.test.tsx"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-error-state-required",
|
|
"category": "platform-error-state",
|
|
"summary": "Platform feature files that surface load-failure copy must compose PlatformErrorState for the canonical retry shell.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformErrorState"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"triggerPatterns": ["title=\"Could not load"],
|
|
"requiredPatterns": ["PlatformErrorState"],
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "named-detail-table-row-shared-shell",
|
|
"category": "row-detail",
|
|
"summary": "Named platform detail rows such as Docker network detail rows must use InlineDetailTableRow even when their row attribute is not data-inline-*.",
|
|
"canonical": {
|
|
"path": "src/components/shared/InlineDetailTableRow.tsx",
|
|
"export": "InlineDetailTableRow"
|
|
},
|
|
"scopes": [
|
|
"src/components/Infrastructure",
|
|
"src/components/Workloads",
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/platformPage",
|
|
"src/features/proxmox",
|
|
"src/features/truenas",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"triggerPatterns": ["detail-row", "TableRow"],
|
|
"requiredPatterns": ["InlineDetailTableRow"],
|
|
"allowedPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
}
|
|
]
|
|
}
|