mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 18:45:53 +00:00
47d6ad94db
recoveryDatePresentation: locale-independent date assertion (en-GB
systems produce '9 Mar 2026' not 'Mar 9, 2026')
quickstartCopyContract: update 'tools' to 'capabilities' matching
docs/AI.md copy change
AIIntelligence: update stale empty-state text to full current copy
SharedPrimitives.guardrails: align guardrail + registry with
d79640c41 migration of ProxmoxBackupsTable from FilterButtonGroup to
FilterSegmentedControl
Full suite: 659 files, 6719 tests, 0 failures.
6497 lines
293 KiB
JSON
6497 lines
293 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"
|
|
},
|
|
{
|
|
"path": "src/components/Alerts/AlertResourceGroupHeader.tsx"
|
|
},
|
|
{
|
|
"path": "src/components/Alerts/AlertResourceTableRow.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'"]
|
|
},
|
|
{
|
|
"path": "src/components/Alerts/AlertResourceGroupHeader.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noopener noreferrer\""]
|
|
},
|
|
{
|
|
"path": "src/components/Alerts/AlertResourceTableRow.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noopener noreferrer\""]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/WebInterfaceNameLink.test.tsx",
|
|
"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",
|
|
"src/components/Alerts/ResourceTable.test.tsx"
|
|
]
|
|
},
|
|
{
|
|
"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": "toast-notification-shell",
|
|
"category": "notification",
|
|
"summary": "Toast notification status icons and dismiss actions must compose library icons and ActionIconButton so every page inherits shared notification chrome instead of raw SVG glyphs or local close-button classes.",
|
|
"canonical": {
|
|
"path": "src/components/Toast/Toast.tsx",
|
|
"export": "Toast"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/Toast/Toast.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Toast/Toast.tsx",
|
|
"patterns": [
|
|
"<svg",
|
|
"const icons = {",
|
|
"flex-shrink-0 text-muted hover:text-base-content hover:bg-surface rounded-md p-1.5 transition-all duration-200"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/Toast/__tests__/Toast.test.tsx",
|
|
"src/components/shared/Button.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/components/Infrastructure/resourceDetailDrawerVmwareModel.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\""]
|
|
},
|
|
{
|
|
"path": "src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx",
|
|
"patterns": ["vmwareRowToneClass", "<For each={drawer.vmwareDetailSections()}"]
|
|
},
|
|
{
|
|
"path": "src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts",
|
|
"patterns": [
|
|
"ResourceDetailDrawerVMwareRow",
|
|
"ResourceDetailDrawerVMwareSection",
|
|
"ResourceDetailDrawerVMwareRowTone",
|
|
"filterNonEmptyRows"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/DetailSectionTable.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "resource-detail-numeric-value-formatting",
|
|
"category": "row-detail",
|
|
"summary": "Resource detail drawer models must use the shared detail-section numeric formatting helpers for byte scaling, integer labels, and count pluralization instead of recreating provider-local byte/count helpers.",
|
|
"canonical": {
|
|
"path": "src/components/shared/detailSectionModel.ts",
|
|
"export": "formatDetailBytesValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Infrastructure/resourceDetailDrawerKubernetesModel.ts" },
|
|
{ "path": "src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts" },
|
|
{ "path": "src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Infrastructure/resourceDetailDrawerKubernetesModel.ts",
|
|
"patterns": ["const formatBytes", "const units = ['B', 'KB', 'MB', 'GB', 'TB']"]
|
|
},
|
|
{
|
|
"path": "src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts",
|
|
"patterns": [
|
|
"const formatInteger",
|
|
"const formatBytes",
|
|
"const formatCount",
|
|
"new Intl.NumberFormat().format"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts",
|
|
"patterns": [
|
|
"const formatCount",
|
|
"const formatCapacityBytes",
|
|
"const units = ['B', 'KB', 'MB', 'GB', 'TB', 'PB']"
|
|
]
|
|
}
|
|
],
|
|
"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/AddressProbeStep.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/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/ConnectionEditor/AddressProbeStep.tsx",
|
|
"patterns": [
|
|
"rounded-md border border-red-300 bg-red-50",
|
|
"rounded-md border border-amber-300 bg-amber-50"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ConnectionEditor/CredentialSlots/AvailabilityTargetSlot.tsx",
|
|
"patterns": [
|
|
"border-green-300 bg-green-50",
|
|
"rounded-md border border-rose-300 bg-rose-50",
|
|
"testToneClass"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ConnectionEditor/CredentialSlots/TrueNASCredentialSlot.tsx",
|
|
"patterns": [
|
|
"rounded-md border border-amber-300 bg-amber-50",
|
|
"rounded-md border border-rose-300 bg-rose-50"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ConnectionEditor/CredentialSlots/VMwareCredentialSlot.tsx",
|
|
"patterns": [
|
|
"rounded-md border border-amber-300 bg-amber-50",
|
|
"rounded-md border border-rose-300 bg-rose-50"
|
|
]
|
|
},
|
|
{
|
|
"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": "error-boundary-callout-shell",
|
|
"category": "callout",
|
|
"summary": "Shared error-boundary fallback notices must compose CalloutCard and library icons for tone, spacing, and dark-mode styling instead of recreating local red panels or inline SVG alert glyphs.",
|
|
"canonical": {
|
|
"path": "src/components/shared/CalloutCard.tsx",
|
|
"export": "CalloutCard"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/ErrorBoundary.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/ErrorBoundary.tsx",
|
|
"patterns": [
|
|
"<svg",
|
|
"bg-red-50 dark:bg-red-900 border border-red-200",
|
|
"p-4 bg-red-50 dark:bg-red-900 border border-red-200"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/__tests__/ErrorBoundary.test.tsx",
|
|
"src/components/shared/CalloutCard.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "update-modal-action-callout-shell",
|
|
"category": "callout",
|
|
"summary": "Update confirmation and progress modals must compose CalloutCard, Button, ActionIconButton, LoadingSpinner, and library icons for shared status, warning, restart, and command chrome instead of recreating local colored panels, raw SVG glyphs, or modal-local button classes.",
|
|
"canonical": {
|
|
"path": "src/components/shared/CalloutCard.tsx",
|
|
"export": "CalloutCard"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/UpdateConfirmationModal.tsx" },
|
|
{ "path": "src/components/UpdateProgressModal.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/UpdateConfirmationModal.tsx",
|
|
"patterns": [
|
|
"<svg",
|
|
"bg-blue-50 dark:bg-blue-900 border border-blue-200",
|
|
"bg-yellow-50 dark:bg-yellow-900 border border-yellow-200",
|
|
"rounded-md p-4 border",
|
|
"px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/UpdateProgressModal.tsx",
|
|
"patterns": [
|
|
"<svg",
|
|
"bg-red-50 dark:bg-red-900 border border-red-200",
|
|
"bg-blue-50 dark:bg-blue-900 border border-blue-200",
|
|
"bg-yellow-50 dark:bg-yellow-900 border border-yellow-200",
|
|
"mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/__tests__/UpdateConfirmationModal.test.tsx",
|
|
"src/components/shared/CalloutCard.test.tsx",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-inline-notice-shell",
|
|
"category": "callout",
|
|
"summary": "Platform inline notices must compose InlineNotice for shared dense notice tone, icon/content layout, and action-link chrome instead of copying platform-local amber notice shells.",
|
|
"canonical": {
|
|
"path": "src/components/shared/InlineNotice.tsx",
|
|
"export": "InlineNotice"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/platformPage/PlatformOutdatedAgentNotice.tsx" },
|
|
{ "path": "src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/platformPage/PlatformOutdatedAgentNotice.tsx",
|
|
"patterns": [
|
|
"rounded-lg border border-amber-300 bg-amber-50",
|
|
"text-amber-900 underline-offset-2"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/platformPage/PlatformOutdatedSensorSetupNotice.tsx",
|
|
"patterns": [
|
|
"rounded-lg border border-amber-300 bg-amber-50",
|
|
"text-amber-900 underline-offset-2"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/InlineNotice.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "demo-banner-notice-shell",
|
|
"category": "callout",
|
|
"summary": "Dismissible global demo-mode notices must compose InlineNotice banner layout and shared dismiss chrome instead of copying local blue notice shells, raw SVG icons, or page-local close buttons.",
|
|
"canonical": {
|
|
"path": "src/components/shared/InlineNotice.tsx",
|
|
"export": "InlineNotice"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/DemoBanner.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/DemoBanner.tsx",
|
|
"patterns": [
|
|
"<svg",
|
|
"<button",
|
|
"bg-blue-50 dark:bg-blue-900 border-b border-blue-200 dark:border-blue-800",
|
|
"p-1 hover:bg-blue-100 dark:hover:bg-blue-800 rounded text-blue-600"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/__tests__/DemoBanner.test.tsx",
|
|
"src/components/shared/InlineNotice.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "commercial-migration-banner-notice-shell",
|
|
"category": "callout",
|
|
"summary": "Global commercial migration notices must compose InlineNotice banner layout, library icons, shared action slots, and shared dismiss chrome instead of copying local paid-state tone shells, raw SVGs, or page-local buttons.",
|
|
"canonical": {
|
|
"path": "src/components/shared/InlineNotice.tsx",
|
|
"export": "InlineNotice"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/CommercialMigrationBanner.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/CommercialMigrationBanner.tsx",
|
|
"patterns": [
|
|
"<svg",
|
|
"<button",
|
|
"toneClasses",
|
|
"buttonClasses",
|
|
"bg-amber-50 dark:bg-amber-900 border-b border-amber-200",
|
|
"bg-red-50 dark:bg-red-900 border-b border-red-200",
|
|
"p-1 rounded transition-colors opacity-70 hover:opacity-100"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/__tests__/CommercialMigrationBanner.test.tsx",
|
|
"src/components/shared/InlineNotice.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "github-star-banner-action-shell",
|
|
"category": "action-button",
|
|
"summary": "The GitHub star prompt must compose Button and ActionIconButton for its primary, defer, and dismiss actions instead of carrying local floating-prompt button shells.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "Button"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/GitHubStarBanner.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/GitHubStarBanner.tsx",
|
|
"patterns": [
|
|
"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content",
|
|
"inline-flex min-h-9 items-center justify-center gap-2 rounded-md bg-blue-600 px-3 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700",
|
|
"inline-flex min-h-9 items-center justify-center rounded-md px-3 py-2 text-sm text-muted transition-colors hover:bg-surface-hover hover:text-base-content"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/__tests__/GitHubStarBanner.test.tsx",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "sso-provider-settings-action-shell",
|
|
"category": "action-button",
|
|
"summary": "SSO provider settings actions must compose the shared Button, ActionIconButton, and CopyValueButton primitives instead of restoring panel-local action buttons.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "Button"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/Settings/SSOProvidersPanel.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/SSOProvidersPanel.tsx",
|
|
"patterns": [
|
|
"<button",
|
|
"lucide-solid/icons/copy",
|
|
"min-h-10 sm:min-h-9 px-3 py-2.5 text-sm font-medium bg-blue-600",
|
|
"p-2 text-slate-500 hover:text-blue-600",
|
|
"text-blue-600 hover:underline flex items-center gap-1"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/Settings/__tests__/settingsArchitecture.test.ts",
|
|
"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": "Shared primitives, Login, Settings, 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/Login.tsx" },
|
|
{ "path": "src/components/Settings/AISettings.tsx" },
|
|
{ "path": "src/components/Settings/AISettingsDialogs.tsx" },
|
|
{ "path": "src/components/Settings/APITokenManager.tsx" },
|
|
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
|
|
{ "path": "src/components/Settings/RolesPanel.tsx" },
|
|
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" },
|
|
{ "path": "src/components/Settings/UpdateInstallGuide.tsx" },
|
|
{ "path": "src/components/Settings/UpdatesSettingsPanel.tsx" },
|
|
{ "path": "src/components/Settings/UserAssignmentsDialog.tsx" },
|
|
{ "path": "src/components/Settings/UserAssignmentsPanel.tsx" },
|
|
{ "path": "src/components/UpdateProgressModal.tsx" },
|
|
{ "path": "src/components/shared/Button.tsx" },
|
|
{ "path": "src/components/shared/DiscoveryLoadingFallback.tsx" },
|
|
{ "path": "src/components/shared/HistoryChartOverlay.tsx" },
|
|
{ "path": "src/components/shared/PulseDataGrid.tsx" },
|
|
{ "path": "src/components/patrol/ApprovalBanner.tsx" },
|
|
{ "path": "src/components/patrol/ApprovalSection.tsx" },
|
|
{ "path": "src/components/patrol/InvestigationMessages.tsx" },
|
|
{ "path": "src/components/patrol/InvestigationSection.tsx" },
|
|
{ "path": "src/components/patrol/RunToolCallTrace.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/Login.tsx",
|
|
"patterns": [
|
|
"animate-spin h-12 w-12 border-4 border-blue-500 border-t-transparent rounded-full mx-auto mb-4",
|
|
"class=\"animate-spin -ml-1 mr-3 h-5 w-5 text-white\""
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AISettings.tsx",
|
|
"patterns": [
|
|
"h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AISettingsDialogs.tsx",
|
|
"patterns": [
|
|
"h-4 w-4 border-2 border-white border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/APITokenManager.tsx",
|
|
"patterns": ["<svg class=\"h-4 w-4 animate-spin\""]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AgentProfilesPanel.tsx",
|
|
"patterns": ["animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/RolesPanel.tsx",
|
|
"patterns": ["animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/SSOProvidersPanel.tsx",
|
|
"patterns": [
|
|
"h-4 w-4 border-2 border-current border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/UpdateInstallGuide.tsx",
|
|
"patterns": [
|
|
"h-4 w-4 animate-spin rounded-full border-2 border-white border-t-transparent"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/UpdatesSettingsPanel.tsx",
|
|
"patterns": [
|
|
"animate-spin h-4 w-4 border-2 border-white border-t-transparent rounded-full"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/UserAssignmentsDialog.tsx",
|
|
"patterns": ["animate-spin rounded-full h-4 w-4 border-b-2 border-blue-500"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/UserAssignmentsPanel.tsx",
|
|
"patterns": ["animate-spin rounded-full h-6 w-6 border-b-2 border-blue-500"]
|
|
},
|
|
{
|
|
"path": "src/components/shared/Button.tsx",
|
|
"patterns": ["class=\"animate-spin -ml-1 mr-2 h-4 w-4 text-current\""]
|
|
},
|
|
{
|
|
"path": "src/components/shared/HistoryChartOverlay.tsx",
|
|
"patterns": [
|
|
"w-6 h-6 border-2 border-blue-500 border-t-transparent rounded-full animate-spin"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/shared/PulseDataGrid.tsx",
|
|
"patterns": [
|
|
"w-4 h-4 rounded-full border-2 border-slate-300 border-t-blue-600 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"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/LoadingSpinner.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/shared/DiscoveryLoadingFallback.test.tsx",
|
|
"src/components/shared/__tests__/HistoryChart.test.tsx",
|
|
"src/components/shared/__tests__/PulseDataGrid.test.tsx",
|
|
"src/components/AI/__tests__/FindingsPanel.test.ts",
|
|
"src/components/__tests__/Login.test.tsx",
|
|
"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": "discovery-loading-fallback",
|
|
"category": "loading-indicator",
|
|
"summary": "Discovery tab Suspense fallbacks in resource and workload drawers must compose DiscoveryLoadingFallback for the centered loading row, canonical spinner, status semantics, and discovery loading copy instead of recreating local spinner/text markup.",
|
|
"canonical": {
|
|
"path": "src/components/shared/DiscoveryLoadingFallback.tsx",
|
|
"export": "DiscoveryLoadingFallback"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Infrastructure/ResourceDetailDrawer.tsx" },
|
|
{ "path": "src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx" },
|
|
{ "path": "src/components/Workloads/GuestDrawer.tsx" },
|
|
{ "path": "src/components/Workloads/NodeDrawer.tsx" },
|
|
{ "path": "src/features/docker/DockerHostDrawer.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Infrastructure/ResourceDetailDrawer.tsx",
|
|
"patterns": [
|
|
"h-6 w-6 animate-spin rounded-full border-2 border-blue-500 border-t-transparent"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx",
|
|
"patterns": [
|
|
"animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Workloads/GuestDrawer.tsx",
|
|
"patterns": [
|
|
"animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Workloads/NodeDrawer.tsx",
|
|
"patterns": [
|
|
"animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/docker/DockerHostDrawer.tsx",
|
|
"patterns": [
|
|
"animate-spin h-6 w-6 border-2 border-blue-500 border-t-transparent rounded-full"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/DiscoveryLoadingFallback.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "settings-loading-skeleton-shell",
|
|
"category": "loading-skeleton",
|
|
"summary": "Settings loading skeletons must compose SettingsLoadingSkeleton for shared pulse animation, skeleton color, metric-grid, progress-card, and table-row shells instead of recreating page-local skeleton blocks.",
|
|
"canonical": {
|
|
"path": "src/components/shared/SettingsLoadingSkeleton.tsx",
|
|
"export": "SettingsLoadingSkeleton"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/DataHandlingPanel.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationAccessLoadingState.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationBillingLoadingState.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationOverviewLoadingState.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationSharingLoadingState.tsx" },
|
|
{ "path": "src/components/Settings/SecurityOverviewPanel.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/DataHandlingPanel.tsx",
|
|
"patterns": ["animate-pulse"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationAccessLoadingState.tsx",
|
|
"patterns": ["animate-pulse", "bg-surface-hover", "bg-surface-alt"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationBillingLoadingState.tsx",
|
|
"patterns": ["animate-pulse", "bg-surface-hover", "bg-surface-alt"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationOverviewLoadingState.tsx",
|
|
"patterns": ["animate-pulse", "bg-surface-hover", "bg-surface-alt"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationSharingLoadingState.tsx",
|
|
"patterns": ["animate-pulse", "bg-surface-hover", "bg-surface-alt"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/SecurityOverviewPanel.tsx",
|
|
"patterns": ["animate-pulse", "bg-slate-300"]
|
|
},
|
|
{
|
|
"path": "src/components/UpdateProgressModal.tsx",
|
|
"patterns": [
|
|
"<svg class=\"w-12 h-12 text-blue-500 animate-spin\"",
|
|
"class=\"animate-spin h-4 w-4\""
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SettingsLoadingSkeleton.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"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/patrol/ApprovalBanner.tsx" },
|
|
{ "path": "src/components/patrol/ApprovalSection.tsx" },
|
|
{ "path": "src/components/ErrorBoundary.tsx" },
|
|
{ "path": "src/components/Infrastructure/ResourceDetailDrawer.tsx" },
|
|
{ "path": "src/components/Infrastructure/ResourceDetailDrawerDebugTab.tsx" },
|
|
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
|
|
{ "path": "src/components/Settings/APIAccessPanel.tsx" },
|
|
{ "path": "src/components/Settings/AvailabilitySettingsPanel.tsx" },
|
|
{ "path": "src/components/Settings/BillingAdminPanel.tsx" },
|
|
{ "path": "src/components/Settings/BillingAdminOrganizationsTable.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/OrganizationAccessInvitationsSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationAccessManagementSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationAccessMembersSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationIncomingSharesSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationOutgoingSharesSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationOverviewDetailsSection.tsx" },
|
|
{ "path": "src/components/Settings/OrganizationSharingCreateSection.tsx" },
|
|
{ "path": "src/components/Settings/ProLicensePanel.tsx" },
|
|
{ "path": "src/components/Settings/ProLicensePlanSection.tsx" },
|
|
{ "path": "src/components/Settings/ReportingPanel.tsx" },
|
|
{ "path": "src/components/Settings/ResourcePicker.tsx" },
|
|
{ "path": "src/components/Settings/RolesPanel.tsx" },
|
|
{ "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx" },
|
|
{ "path": "src/components/Settings/SecurityAuthPanel.tsx" },
|
|
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" },
|
|
{ "path": "src/components/Settings/UserAssignmentsPanel.tsx" },
|
|
{ "path": "src/components/UpdateConfirmationModal.tsx" },
|
|
{ "path": "src/components/UpdateProgressModal.tsx" },
|
|
{ "path": "src/components/Workloads/GuestDrawer.tsx" },
|
|
{ "path": "src/features/patrol/PatrolIntelligenceWorkspace.tsx" },
|
|
{ "path": "src/features/standalone/StandalonePageSurface.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/patrol/ApprovalBanner.tsx",
|
|
"patterns": [
|
|
"px-3 py-1.5 bg-green-600 hover:bg-green-700",
|
|
"px-3 py-1.5 bg-surface-alt hover:bg-surface-hover",
|
|
"px-3 py-1.5 bg-amber-600 hover:bg-amber-700"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/ApprovalSection.tsx",
|
|
"patterns": [
|
|
"px-3 py-1.5 bg-green-600 hover:bg-green-700",
|
|
"px-3 py-1.5 bg-amber-600 hover:bg-amber-700",
|
|
"px-3 py-1.5 bg-blue-600 hover:bg-blue-700",
|
|
"px-3 py-1.5 hover:bg-surface-hover disabled:opacity-50 text-muted"
|
|
]
|
|
},
|
|
{
|
|
"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/BillingAdminOrganizationsTable.tsx",
|
|
"patterns": [
|
|
"px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50",
|
|
"px-2 py-1 text-xs rounded-md border border-border bg-surface hover:bg-surface-hover"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/APIAccessPanel.tsx",
|
|
"patterns": [
|
|
"inline-flex min-h-10 sm:min-h-10 w-fit items-center gap-2 rounded-md border border-blue-200 bg-blue-50 px-3 py-2 text-sm font-semibold text-blue-700"
|
|
]
|
|
},
|
|
{
|
|
"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/RolesPanel.tsx",
|
|
"patterns": [
|
|
"inline-flex w-full sm:w-auto min-h-10 sm:min-h-9 items-center justify-center gap-2 rounded-md bg-blue-600 px-4 py-2.5 text-sm font-medium text-white transition-colors hover:bg-blue-700"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationAccessInvitationsSection.tsx",
|
|
"patterns": [
|
|
"inline-flex items-center justify-center rounded-md bg-blue-600 px-3 py-1.5 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60",
|
|
"inline-flex items-center justify-center rounded-md border border-border bg-surface px-3 py-1.5 text-sm font-medium text-base-content transition-colors hover:border-red-300 hover:text-red-600 disabled:cursor-not-allowed disabled:opacity-60"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationAccessManagementSection.tsx",
|
|
"patterns": [
|
|
"inline-flex w-full sm:w-auto items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationAccessMembersSection.tsx",
|
|
"patterns": [
|
|
"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-900 disabled:cursor-not-allowed disabled:opacity-60"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationIncomingSharesSection.tsx",
|
|
"patterns": [
|
|
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-emerald-700 hover:bg-emerald-50 dark:text-emerald-300 dark:hover:bg-emerald-900 disabled:cursor-not-allowed disabled:opacity-60",
|
|
"inline-flex items-center rounded-md px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-900 disabled:cursor-not-allowed disabled:opacity-60"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationOutgoingSharesSection.tsx",
|
|
"patterns": [
|
|
"inline-flex items-center gap-1 rounded-md px-2 py-1 text-xs font-medium text-red-600 hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-900 disabled:cursor-not-allowed disabled:opacity-60"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationOverviewDetailsSection.tsx",
|
|
"patterns": [
|
|
"inline-flex w-full sm:w-auto items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/OrganizationSharingCreateSection.tsx",
|
|
"patterns": [
|
|
"text-xs font-medium text-blue-700 hover:text-blue-800 dark:text-blue-300 dark:hover:text-blue-200",
|
|
"inline-flex w-full sm:w-auto items-center justify-center rounded-md bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-60"
|
|
]
|
|
},
|
|
{
|
|
"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/ProLicensePanel.tsx",
|
|
"patterns": [
|
|
"inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-2 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/ProLicensePlanSection.tsx",
|
|
"patterns": [
|
|
"mt-2 inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-2 text-xs font-medium rounded-md border border-amber-300 dark:border-amber-700 text-amber-800 dark:text-amber-200 hover:bg-amber-100 dark:hover:bg-amber-800 transition-colors disabled:opacity-60"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx",
|
|
"patterns": [
|
|
"min-h-10 sm:min-h-9 px-4 py-2.5 text-sm font-medium rounded-md bg-blue-600 text-white hover:bg-blue-700 transition-colors disabled:opacity-60 disabled:cursor-not-allowed",
|
|
"min-h-10 sm:min-h-9 px-4 py-2.5 text-sm font-medium rounded-md border border-border text-base-content hover:bg-surface-hover transition-colors disabled:opacity-60 disabled:cursor-not-allowed"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/SecurityAuthPanel.tsx",
|
|
"patterns": [
|
|
"w-full sm:w-auto px-3 py-2 text-xs font-medium rounded-md border border-amber-300 text-amber-800 bg-amber-100 hover:bg-amber-200 transition-colors dark:border-amber-700 dark:text-amber-200 dark:bg-amber-900 dark:hover:bg-amber-800",
|
|
"w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors",
|
|
"w-full sm:w-auto min-h-10 sm:min-h-10 px-4 py-2.5 text-sm font-medium border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/SSOProvidersPanel.tsx",
|
|
"patterns": [
|
|
"min-h-10 sm:min-h-9 px-3 py-2.5 text-sm font-medium bg-blue-600 text-white rounded-md hover:bg-blue-700 transition-colors flex items-center gap-1.5",
|
|
"min-h-10 sm:min-h-9 px-3 py-2.5 text-sm font-medium border border-border text-base-content rounded-md hover:bg-surface-hover transition-colors flex items-center gap-1.5",
|
|
"px-3 py-2 text-sm font-medium bg-surface-hover text-base-content rounded-md hover:bg-surface-hover disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap",
|
|
"px-3 py-2 text-sm font-medium bg-surface-hover text-base-content rounded-md hover:bg-surface-hover disabled:opacity-50 disabled:cursor-not-allowed whitespace-nowrap flex items-center gap-1",
|
|
"px-4 py-2 text-sm font-medium text-base-content border border-border rounded-md hover:bg-surface-hover",
|
|
"px-4 py-2 text-sm font-medium bg-blue-600 text-white rounded-md hover:bg-blue-700 disabled:opacity-50",
|
|
"px-4 py-2 text-sm font-medium bg-red-600 text-white rounded-md hover:bg-red-700",
|
|
"px-2 py-1 text-xs font-medium text-muted bg-surface-hover rounded hover:bg-surface-hover flex items-center gap-1"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/UserAssignmentsPanel.tsx",
|
|
"patterns": [
|
|
"inline-flex min-h-10 sm:min-h-9 items-center gap-2 px-3 py-1.5 rounded-md text-sm font-medium text-base-content hover:bg-surface-hover transition-colors"
|
|
]
|
|
},
|
|
{
|
|
"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"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/ErrorBoundary.tsx",
|
|
"patterns": [
|
|
"px-4 py-2 bg-blue-600 text-white rounded hover:bg-blue-700",
|
|
"px-4 py-2 bg-slate-600 text-white rounded hover:bg-slate-700",
|
|
"text-xs px-2 py-1 bg-red-600 text-white rounded hover:bg-red-700"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/UpdateConfirmationModal.tsx",
|
|
"patterns": [
|
|
"px-4 py-2 text-sm font-medium text-base-content hover:bg-surface-hover rounded-md transition-colors",
|
|
"px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/UpdateProgressModal.tsx",
|
|
"patterns": [
|
|
"mt-2 px-3 py-1.5 text-xs font-medium text-white bg-blue-600 hover:bg-blue-700 rounded",
|
|
"px-4 py-2 text-sm font-medium text-white bg-blue-600 hover:bg-blue-700 rounded-md transition-colors"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/__tests__/ErrorBoundary.test.tsx",
|
|
"src/components/shared/Button.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "panel-empty-state-shell",
|
|
"category": "empty-state",
|
|
"summary": "Embedded settings panel empty states must compose EmptyState for shared compact spacing, icon treatment, text hierarchy, and action slots instead of recreating local centered icon stacks.",
|
|
"canonical": {
|
|
"path": "src/components/shared/EmptyState.tsx",
|
|
"export": "EmptyState"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/AgentProfilesPanel.tsx" },
|
|
{ "path": "src/components/Settings/AuditWebhookPanel.tsx" },
|
|
{ "path": "src/components/Settings/AuditLogPanel.tsx" },
|
|
{ "path": "src/components/Settings/AvailabilitySettingsPanel.tsx" },
|
|
{ "path": "src/components/Settings/DiagnosticsResultsPanel.tsx" },
|
|
{ "path": "src/components/patrol/RunHistoryPanel.tsx" },
|
|
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/AgentProfilesPanel.tsx",
|
|
"patterns": ["text-center py-8 text-muted"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AuditWebhookPanel.tsx",
|
|
"patterns": [
|
|
"py-10 flex flex-col items-center justify-center text-muted border-2 border-dashed border-border rounded-md"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AuditLogPanel.tsx",
|
|
"patterns": [
|
|
"text-center py-12 px-4 bg-surface-alt rounded-md border border-dashed border-border"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AvailabilitySettingsPanel.tsx",
|
|
"patterns": ["flex flex-col items-center justify-center gap-3 px-4 py-12 text-center"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/DiagnosticsResultsPanel.tsx",
|
|
"patterns": [
|
|
"Activity class=\"mx-auto mb-4 h-12 w-12 text-muted\"",
|
|
"inline-flex min-h-10 items-center gap-2 rounded-md bg-blue-600"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/patrol/RunHistoryPanel.tsx",
|
|
"patterns": ["text-center py-8"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/SSOProvidersPanel.tsx",
|
|
"patterns": ["text-center py-8 text-muted"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/EmptyState.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "action-icon-button-shell",
|
|
"category": "action-button",
|
|
"summary": "Compact icon-only row and inline actions must compose ActionIconButton so size, tone, focus, disabled, title, aria-label, and icon behavior stay canonical instead of recreating page-local button and SVG shells.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Alerts/AlertResourceTableDesktop.tsx" },
|
|
{ "path": "src/components/Alerts/AlertResourceTableMobile.tsx" },
|
|
{ "path": "src/components/Alerts/AlertResourceTableRow.tsx" },
|
|
{ "path": "src/components/Alerts/ResourceTable.tsx" },
|
|
{ "path": "src/components/AI/Chat/index.tsx" },
|
|
{ "path": "src/components/AI/Chat/MessageItem.tsx" },
|
|
{ "path": "src/components/Settings/RolesPanel.tsx" },
|
|
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" },
|
|
{ "path": "src/features/standalone/AgentsMachinesTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Alerts/AlertResourceTableDesktop.tsx",
|
|
"patterns": ["<svg", "class=\"p-1 hover:text-muted", "class=\"p-1 text-red-600"]
|
|
},
|
|
{
|
|
"path": "src/components/Alerts/AlertResourceTableMobile.tsx",
|
|
"patterns": [
|
|
"<svg",
|
|
"class=\"p-1.5 bg-blue-50",
|
|
"class=\"p-1.5 bg-surface-hover",
|
|
"class=\"p-1.5 bg-green-50"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Alerts/AlertResourceTableRow.tsx",
|
|
"patterns": [
|
|
"<svg",
|
|
"class=\"p-1 hover:text-muted",
|
|
"class=\"p-1 text-blue-600",
|
|
"class=\"p-1 hover:text-base-content"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Alerts/ResourceTable.tsx",
|
|
"patterns": [
|
|
"<svg",
|
|
"text-slate-400 hover:text-white bg-surface hover:bg-slate-700 rounded-full p-1.5"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/AI/Chat/index.tsx",
|
|
"patterns": [
|
|
"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content disabled:cursor-not-allowed disabled:opacity-50 disabled:hover:bg-surface disabled:hover:text-muted",
|
|
"flex-shrink-0 p-2 hover:text-base-content rounded-md hover:bg-surface-hover transition-colors",
|
|
"inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 disabled:cursor-wait disabled:opacity-70 dark:text-blue-200 dark:hover:bg-blue-900/60",
|
|
"rounded p-1 text-muted opacity-0 transition-opacity hover:bg-blue-100 hover:text-blue-600 focus:opacity-100 group-hover:opacity-100 group-focus-within:opacity-100 dark:hover:bg-blue-900 dark:hover:text-blue-300",
|
|
"order-2 flex h-9 w-9 flex-shrink-0 items-center justify-center rounded-md hover:text-base-content hover:bg-surface-hover transition-colors sm:order-none",
|
|
"flex h-7 w-7 items-center justify-center rounded-md border border-amber-200 bg-surface text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:border-amber-800 dark:bg-amber-950/60 dark:text-amber-200 dark:hover:bg-amber-900",
|
|
"flex h-7 w-7 items-center justify-center rounded-md text-amber-700 transition-colors hover:bg-amber-100 hover:text-amber-900 dark:text-amber-200 dark:hover:bg-amber-900",
|
|
"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 dark:text-blue-200 dark:hover:bg-blue-900/60",
|
|
"flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700 disabled:cursor-not-allowed disabled:opacity-45",
|
|
"flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/AI/Chat/MessageItem.tsx",
|
|
"patterns": [
|
|
"inline-flex h-5 w-5 items-center justify-center rounded text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 focus:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/standalone/AgentsMachinesTable.tsx",
|
|
"patterns": [
|
|
"inline-flex h-7 w-7 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/SSOProvidersPanel.tsx",
|
|
"patterns": [
|
|
"p-2 text-slate-500 hover:text-blue-600 hover:bg-surface-hover rounded-md transition-colors",
|
|
"p-2 text-slate-500 hover:text-red-600 hover:bg-red-50 dark:hover:bg-red-900 rounded-md transition-colors",
|
|
"class=\"text-slate-400 hover:text-base-content\""
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/RolesPanel.tsx",
|
|
"patterns": [
|
|
"p-1.5 rounded-md text-slate-500 hover:text-blue-600 hover:bg-surface-hover dark:hover:text-blue-300",
|
|
"p-1.5 rounded-md text-slate-500 hover:text-red-600 hover:bg-red-50 dark:hover:text-red-400 dark:hover:bg-red-900"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/Alerts/ResourceTable.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/AI/Chat/MessageItem.tsx" },
|
|
{ "path": "src/components/AI/Chat/ToolExecutionBlock.tsx" },
|
|
{ "path": "src/components/Discovery/DiscoveryTab.tsx" },
|
|
{ "path": "src/components/Settings/SSOProvidersPanel.tsx" },
|
|
{ "path": "src/components/shared/WebInterfaceUrlField.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/AI/Chat/MessageItem.tsx",
|
|
"patterns": [
|
|
"lucide-solid/icons/copy",
|
|
"lucide-solid/icons/check';",
|
|
"mt-1 inline-flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border-subtle bg-surface text-muted opacity-0 shadow-sm transition-opacity hover:text-base-content",
|
|
"ml-auto inline-flex h-7 w-7 items-center justify-center rounded-md border border-border-subtle bg-surface text-muted opacity-0 shadow-sm transition-opacity hover:text-base-content"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/components/AI/Chat/ToolExecutionBlock.tsx",
|
|
"patterns": [
|
|
"lucide-solid/icons/copy",
|
|
"lucide-solid/icons/check';",
|
|
"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border border-border-subtle bg-surface text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
]
|
|
},
|
|
{
|
|
"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/Settings/SSOProvidersPanel.tsx",
|
|
"patterns": [
|
|
"text-blue-600 hover:underline flex items-center gap-1",
|
|
"Copy class=\"w-3 h-3\""
|
|
]
|
|
},
|
|
{
|
|
"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": "settings-external-text-link-shell",
|
|
"category": "link",
|
|
"summary": "Inline external documentation links in settings must compose ExternalTextLink for shared new-tab safety, text-link tone, compact action variants, and focus styling instead of hand-coding anchor rel/target and Tailwind link shells.",
|
|
"canonical": {
|
|
"path": "src/components/shared/ExternalTextLink.tsx",
|
|
"export": "ExternalTextLink"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Settings/AIProviderConfigurationSection.tsx" },
|
|
{ "path": "src/components/Settings/AIRuntimeControlsSection.tsx" },
|
|
{ "path": "src/components/Settings/AISettingsDialogs.tsx" },
|
|
{ "path": "src/components/Settings/APITokenManager.tsx" },
|
|
{ "path": "src/components/Settings/AgentIntegrationsPanel.tsx" },
|
|
{ "path": "src/components/Settings/GeneralSettingsPanel.tsx" },
|
|
{ "path": "src/components/Settings/SecurityOverviewPanel.tsx" },
|
|
{ "path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/AIProviderConfigurationSection.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noopener"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AIRuntimeControlsSection.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noopener noreferrer\""]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AISettingsDialogs.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noopener"]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/APITokenManager.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noreferrer\""]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/AgentIntegrationsPanel.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noreferrer\""]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/GeneralSettingsPanel.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noopener noreferrer\""]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/SecurityOverviewPanel.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noreferrer\""]
|
|
},
|
|
{
|
|
"path": "src/components/Settings/SelfHostedCommercialRecoverySection.tsx",
|
|
"patterns": ["target=\"_blank\"", "rel=\"noopener noreferrer\""]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/ExternalTextLink.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"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" }
|
|
],
|
|
"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" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/patrol/RunHistoryEntry.tsx",
|
|
"patterns": ["runStatus.badgeClass"]
|
|
}
|
|
],
|
|
"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/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/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/ProxmoxBackupServersTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxMailGatewayDrawer.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxNodesTable.tsx" },
|
|
{ "path": "src/features/standalone/AgentsMachinesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereHostsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerHostsTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesNodesTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxBackupServersTable.tsx",
|
|
"patterns": ["formatUptime(row.uptimeSeconds ?? 0)"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxMailGatewayDrawer.tsx",
|
|
"patterns": [
|
|
"function formatUptime",
|
|
"const days = Math.floor(seconds / 86_400);",
|
|
"return `${mins}m`"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxNodesTable.tsx",
|
|
"patterns": ["formatUptime(seconds)"]
|
|
},
|
|
{
|
|
"path": "src/features/standalone/AgentsMachinesTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
},
|
|
{
|
|
"path": "src/features/truenas/TrueNASSystemsTable.tsx",
|
|
"patterns": ["const formatUptime"]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereHostsTable.tsx",
|
|
"patterns": ["formatUptime(host.uptime"]
|
|
}
|
|
],
|
|
"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/docker/DockerStorageUsageTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesNodesTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesStorageTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxBackupServersTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxCephTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxCoverageTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxRecoverableTable.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/docker/DockerStorageUsageTable.tsx",
|
|
"patterns": ["formatBytes("]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesNodesTable.tsx",
|
|
"patterns": ["const formatBytes", "value.toFixed(value >= 100 ? 0 : value >= 10 ? 1 : 2)"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesStorageTable.tsx",
|
|
"patterns": ["formatBytes("]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxBackupServersTable.tsx",
|
|
"patterns": ["formatBytes("]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxCephTable.tsx",
|
|
"patterns": ["formatBytes("]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxCoverageTable.tsx",
|
|
"patterns": ["formatBytes("]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxRecoverableTable.tsx",
|
|
"patterns": ["formatBytes("]
|
|
},
|
|
{
|
|
"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-date-time-value",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared compact date-time value primitive for timestamp cells, tabular styling, and canonical empty-cell markers instead of recreating local compact toLocaleString helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableDateTimeValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/truenas/TrueNASProtectionTable.tsx" },
|
|
{ "path": "src/features/vmware/VsphereActivityTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/truenas/TrueNASProtectionTable.tsx",
|
|
"patterns": ["const formatPointTime", "formatPointTime(point)"]
|
|
},
|
|
{
|
|
"path": "src/features/vmware/VsphereActivityTable.tsx",
|
|
"patterns": [
|
|
"const formatActivityDate",
|
|
"formatActivityDate(activity.occurredAt || activity.observedAt)"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-relative-time-value",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared relative-time value primitive for dense timestamp-age cells, tabular styling, compact labels, and canonical empty-cell markers instead of calling formatRelativeTime directly.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableRelativeTimeValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/DockerVolumesTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesDeploymentsTable.tsx" },
|
|
{ "path": "src/features/kubernetes/KubernetesEventsTable.tsx" },
|
|
{ "path": "src/features/proxmox/proxmoxBackupsTableShared.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxReplicationTable.tsx" },
|
|
{ "path": "src/features/standalone/AvailabilityChecksTable.tsx" },
|
|
{ "path": "src/features/standalone/AgentsMachinesTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/DockerVolumesTable.tsx",
|
|
"patterns": ["formatRelativeTime(parsed)"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesDeploymentsTable.tsx",
|
|
"patterns": ["formatRelativeTime(createdAt, { compact: true, emptyText: '—' })"]
|
|
},
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesEventsTable.tsx",
|
|
"patterns": [
|
|
"formatRelativeTime(observedTimestamp(resource), { compact: true, emptyText: '—' })"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/proxmoxBackupsTableShared.tsx",
|
|
"patterns": ["formatRelativeTime(props.artifact.createdAt, { compact: true })"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxReplicationTable.tsx",
|
|
"patterns": [
|
|
"formatRelativeTime(job.lastSyncUnix * 1000, { compact: true })",
|
|
"formatRelativeTime(job.lastSyncTime, { compact: true })"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/standalone/AgentsMachinesTable.tsx",
|
|
"patterns": ["const formatLastSeen", "Math.floor((Date.now() - timestampMillis) / 1000)"]
|
|
},
|
|
{
|
|
"path": "src/features/standalone/AvailabilityChecksTable.tsx",
|
|
"patterns": ["formatRelativeTime(availability?.lastChecked", "const formatChecked"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-duration-value",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared duration value primitive for elapsed duration and polling interval cells, tabular styling, fallback labels, and canonical empty-cell markers instead of declaring local seconds/minutes helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableDurationValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/proxmox/ProxmoxReplicationTable.tsx" },
|
|
{ "path": "src/features/standalone/AvailabilityChecksTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxReplicationTable.tsx",
|
|
"patterns": ["function formatDuration", "seconds < 3600"]
|
|
},
|
|
{
|
|
"path": "src/features/standalone/AvailabilityChecksTable.tsx",
|
|
"patterns": ["const formatInterval", "toFixed(1)}m"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-weighted-column-width-style",
|
|
"category": "platform-table-layout",
|
|
"summary": "Responsive platform table models must use the shared weighted column-width helper for visible-column percentage normalization instead of declaring local percentage-format helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "getPlatformTableWeightedColumnWidthStyle"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/docker/dockerContainerTableModel.ts" },
|
|
{ "path": "src/features/proxmox/proxmoxHostTableModel.ts" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/docker/dockerContainerTableModel.ts",
|
|
"patterns": ["const formatPercentage", "value.toFixed(4)"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/proxmoxHostTableModel.ts",
|
|
"patterns": ["const formatPercentage", "value.toFixed(4)"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/features/platformPage/__tests__/sharedPlatformPage.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-table-integer-value-format",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables and drawers must use the shared integer formatter with PlatformTableNumberValue for locale-formatted count cells instead of recreating local toLocaleString or Math.round helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableIntegerValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/components/Kubernetes/K8sNamespacesDrawer.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxBackupServersTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxCephClusterDrawer.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxMailGatewayDrawer.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Kubernetes/K8sNamespacesDrawer.tsx",
|
|
"patterns": ["const formatInteger", "Math.round(n).toLocaleString()"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxBackupServersTable.tsx",
|
|
"patterns": ["row.backupCount.toLocaleString()"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxCephClusterDrawer.tsx",
|
|
"patterns": ["pool.objects.toLocaleString()"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxMailGatewayDrawer.tsx",
|
|
"patterns": ["function formatNumber", "Math.round(value).toLocaleString()"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxMailGatewayTable.tsx",
|
|
"patterns": ["const formatLocaleCount", "value.toLocaleString()"]
|
|
}
|
|
],
|
|
"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-count-ratio-label",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must use the shared count-ratio formatter for ready/total string labels instead of recreating local ready/total template literals.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableCountRatioValue"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/features/kubernetes/KubernetesNetworkingTable.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/kubernetes/KubernetesNetworkingTable.tsx",
|
|
"patterns": [
|
|
"`${readyValue}/${totalValue} ready`",
|
|
"`${ready ?? 0}/${total ?? ready ?? 0} ready`"
|
|
]
|
|
}
|
|
],
|
|
"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 and drawers must use the shared percent-value primitive/helper for one-decimal percent cells, overlay labels, tabular styling, ratio normalization, clamping, and canonical empty-cell markers instead of recreating local formatPercent helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTablePercentValue"
|
|
},
|
|
"requiredConsumers": [
|
|
{ "path": "src/features/proxmox/ProxmoxBackupServersTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxCephClusterDrawer.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxCephTable.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxMailGatewayDrawer.tsx" },
|
|
{ "path": "src/features/proxmox/ProxmoxNodesTable.tsx" },
|
|
{ "path": "src/features/truenas/TrueNASSystemsTable.tsx" }
|
|
],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxBackupServersTable.tsx",
|
|
"patterns": [
|
|
"Math.round(row.cpuPercent ?? 0)}%",
|
|
"Math.round(row.memoryPercent ?? 0)}%",
|
|
"Math.round(pct() ?? 0)}%"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxCephClusterDrawer.tsx",
|
|
"patterns": ["clamped.toFixed(1)}%"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxCephTable.tsx",
|
|
"patterns": ["pct.toFixed(1)}%"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxMailGatewayDrawer.tsx",
|
|
"patterns": ["share.toFixed(1)}%"]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxNodesTable.tsx",
|
|
"patterns": ["const formatPercentLabel", "`${Math.round(Math.max(0, normalized))}%`"]
|
|
},
|
|
{
|
|
"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-table-finite-metric-normalization",
|
|
"category": "platform-table-metric",
|
|
"summary": "Platform table models must use the shared finite-metric normalization helper instead of carrying model-local Number.isFinite wrappers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "getPlatformTableFiniteMetric"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/features/standalone/agentMachineTableModel.ts" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/features/standalone/agentMachineTableModel.ts",
|
|
"patterns": ["const finiteMetric"]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.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}>"
|
|
]
|
|
},
|
|
{
|
|
"path": "src/features/proxmox/ProxmoxBackupsTable.tsx",
|
|
"patterns": [
|
|
"const viewButtonClass",
|
|
"inline-flex items-center gap-1 rounded-md border border-border bg-surface p-1",
|
|
"inline-flex min-h-8 items-center gap-1.5 rounded-sm px-3 text-xs font-medium transition-colors"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/FilterButtonGroup.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "selectable-pill-button-shell",
|
|
"category": "selectable-pill-button",
|
|
"summary": "Pressed/unpressed rounded pill selectors must compose SelectablePillButton so active tone, disabled treatment, focus ring, and aria-pressed semantics stay shared instead of being recreated inside settings surfaces.",
|
|
"canonical": {
|
|
"path": "src/components/shared/SelectablePillButton.tsx",
|
|
"export": "SelectablePillButton"
|
|
},
|
|
"requiredConsumers": [{ "path": "src/components/Settings/APITokenManager.tsx" }],
|
|
"forbiddenPatterns": [
|
|
{
|
|
"path": "src/components/Settings/APITokenManager.tsx",
|
|
"patterns": [
|
|
"inline-flex min-h-10 sm:min-h-10 items-center rounded-full border px-3 py-2 text-sm font-semibold transition",
|
|
"min-h-10 sm:min-h-10 rounded-full border px-3 py-2 text-sm font-semibold transition",
|
|
"border-blue-500 bg-blue-600 text-white shadow-sm",
|
|
"hover:border-blue-400 hover:text-blue-600 dark:hover:border-blue-400 dark:hover:text-blue-200"
|
|
]
|
|
}
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/SelectablePillButton.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": "alert-resource-action-local-svg-button-shell",
|
|
"category": "action-button",
|
|
"summary": "Alert resource threshold action controls must not recreate icon-only buttons with inline SVGs locally; compose ActionIconButton and lucide icons for row, mobile, and global actions.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"scopes": ["src/components/Alerts"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["<button", "<svg", "Edit thresholds"],
|
|
"legacyReason": "Retired migration debt. Alert resource threshold action buttons compose ActionIconButton for shared size, tone, focus, and accessible label behavior.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Alerts/ResourceTable.test.tsx",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/Alerts/ResourceTable.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "standalone-agent-machine-action-icon-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Standalone machine row action triggers must not recreate local muted h-7 icon-button shells; compose ActionIconButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"scopes": ["src/features/standalone/AgentsMachinesTable.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex h-7 w-7 items-center justify-center rounded-md text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus-visible:ring-2 focus-visible:ring-blue-500/60"
|
|
],
|
|
"legacyReason": "Retired migration debt. Standalone machine action triggers compose ActionIconButton for shared icon-action size, tone, focus, title, and accessible-label behavior.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/features/standalone/__tests__/AgentsMachinesTable.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "ai-chat-message-queued-action-icon-local-shell",
|
|
"category": "action-button",
|
|
"summary": "AI Chat queued message edit/remove actions must not recreate local 20px blue icon-button shells; compose ActionIconButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"scopes": ["src/components/AI/Chat/MessageItem.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex h-5 w-5 items-center justify-center rounded text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950 focus:bg-blue-100 focus:outline-none focus:ring-2 focus:ring-blue-500/30"
|
|
],
|
|
"legacyReason": "Retired migration debt. AI Chat queued message icon actions belong to ActionIconButton.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/AI/Chat/__tests__/MessageItem.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "ai-chat-action-icon-header-local-shell",
|
|
"category": "action-button",
|
|
"summary": "AI Chat header icon actions must not recreate local h-8 bordered icon-button shells; compose ActionIconButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"scopes": ["src/components/AI/Chat"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"flex h-8 w-8 flex-shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content"
|
|
],
|
|
"legacyReason": "Retired migration debt. AI Chat header icon actions belong to ActionIconButton.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"src/components/shared/Button.test.tsx"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "ai-chat-action-icon-accent-local-shell",
|
|
"category": "action-button",
|
|
"summary": "AI Chat queued follow-up and session accent icon actions must not recreate local blue h-6/h-7 icon-button shells; compose ActionIconButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"scopes": ["src/components/AI/Chat"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded-md text-blue-700 transition-colors hover:bg-blue-100 hover:text-blue-950"
|
|
],
|
|
"legacyReason": "Retired migration debt. AI Chat accent icon actions belong to ActionIconButton.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"src/components/shared/Button.test.tsx"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "ai-chat-action-icon-primary-send-local-shell",
|
|
"category": "action-button",
|
|
"summary": "AI Chat composer send action must not recreate the local primary icon-button shell; compose ActionIconButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"scopes": ["src/components/AI/Chat"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"flex h-9 w-9 items-center justify-center rounded-md bg-blue-600 text-white shadow-sm transition-colors hover:bg-blue-700"
|
|
],
|
|
"legacyReason": "Retired migration debt. AI Chat primary icon actions belong to ActionIconButton.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"src/components/shared/Button.test.tsx"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "ai-chat-action-icon-warning-local-shell",
|
|
"category": "action-button",
|
|
"summary": "AI Chat transcript fallback warning icon actions must not recreate local amber icon-button shells; compose ActionIconButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"scopes": ["src/components/AI/Chat"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"flex h-7 w-7 items-center justify-center rounded-md border border-amber-200 bg-surface text-amber-700 transition-colors hover:bg-amber-100"
|
|
],
|
|
"legacyReason": "Retired migration debt. AI Chat warning icon actions belong to ActionIconButton.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"src/components/shared/Button.test.tsx"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "ai-chat-action-icon-footer-local-shell",
|
|
"category": "action-button",
|
|
"summary": "AI Chat composer footer icon actions must not recreate local bordered h-7 icon-button shells; compose ActionIconButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"scopes": ["src/components/AI/Chat"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"flex h-7 w-7 shrink-0 items-center justify-center rounded-md border border-border bg-surface text-muted transition-colors hover:border-border hover:bg-surface-hover hover:text-base-content"
|
|
],
|
|
"legacyReason": "Retired migration debt. AI Chat footer icon actions belong to ActionIconButton.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"src/components/shared/Button.test.tsx"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/AI/Chat/__tests__/AIChat.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "ai-chat-message-copy-value-local-shell",
|
|
"category": "copy-action",
|
|
"summary": "AI Chat user and assistant message copy controls must not recreate local copied-state icon shells; compose CopyValueButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "CopyValueButton"
|
|
},
|
|
"scopes": ["src/components/AI/Chat/MessageItem.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex h-7 w-7",
|
|
"border border-border-subtle bg-surface text-muted opacity-0 shadow-sm transition-opacity hover:text-base-content",
|
|
"CopyIcon class=\"h-3.5 w-3.5\""
|
|
],
|
|
"legacyReason": "Retired migration debt. AI Chat message copy actions belong to CopyValueButton.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/AI/Chat/__tests__/MessageItem.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "ai-chat-tool-copy-value-local-shell",
|
|
"category": "copy-action",
|
|
"summary": "AI Chat tool command and detail copy controls must not recreate local copied-state icon shells; compose CopyValueButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "CopyValueButton"
|
|
},
|
|
"scopes": ["src/components/AI/Chat/ToolExecutionBlock.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex h-6 w-6 shrink-0 items-center justify-center rounded border border-border-subtle bg-surface text-muted transition-colors hover:bg-surface-hover hover:text-base-content focus:outline-none focus:ring-2 focus:ring-blue-500/30",
|
|
"CopyIcon class=\"h-3 w-3\""
|
|
],
|
|
"legacyReason": "Retired migration debt. AI Chat tool copy actions belong to CopyValueButton.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/Button.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/AI/Chat/__tests__/ToolExecutionBlock.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "settings-connection-editor-local-warning-callout-shell",
|
|
"category": "callout",
|
|
"summary": "Connection editor settings code must not recreate amber warning callout shells locally; compose CalloutCard compact notices so tone, spacing, and dark-mode styling stay shared.",
|
|
"canonical": {
|
|
"path": "src/components/shared/CalloutCard.tsx",
|
|
"export": "CalloutCard"
|
|
},
|
|
"scopes": ["src/components/Settings/ConnectionEditor"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["rounded-md border", "bg-amber-50"],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/CalloutCard.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "settings-connection-editor-local-danger-callout-shell",
|
|
"category": "callout",
|
|
"summary": "Connection editor settings code must not recreate red or rose danger callout shells locally; compose CalloutCard compact notices so alert tone, spacing, and dark-mode styling stay shared.",
|
|
"canonical": {
|
|
"path": "src/components/shared/CalloutCard.tsx",
|
|
"export": "CalloutCard"
|
|
},
|
|
"scopes": ["src/components/Settings/ConnectionEditor"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["rounded-md border", "bg-rose-50"],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/CalloutCard.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "settings-connection-editor-local-red-callout-shell",
|
|
"category": "callout",
|
|
"summary": "Connection editor settings code must not recreate red danger callout shells locally; compose CalloutCard compact notices so alert tone, spacing, and dark-mode styling stay shared.",
|
|
"canonical": {
|
|
"path": "src/components/shared/CalloutCard.tsx",
|
|
"export": "CalloutCard"
|
|
},
|
|
"scopes": ["src/components/Settings/ConnectionEditor"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["rounded-md border", "bg-red-50"],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Settings/ConnectionEditor/__tests__/ConnectionEditor.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/CalloutCard.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "settings-local-loading-skeleton-block-shell",
|
|
"category": "loading-skeleton",
|
|
"summary": "Settings components must not recreate local pulse skeleton blocks with surface-hover fills; compose SettingsLoadingSkeleton so loading placeholders stay canonical.",
|
|
"canonical": {
|
|
"path": "src/components/shared/SettingsLoadingSkeleton.tsx",
|
|
"export": "SettingsLoadingSkeleton"
|
|
},
|
|
"scopes": ["src/components/Settings"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["animate-pulse", "bg-surface-hover"],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SettingsLoadingSkeleton.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "platform-inline-notice-local-amber-shell",
|
|
"category": "callout",
|
|
"summary": "Platform page code must not recreate the dense amber inline notice shell locally; compose InlineNotice so notice tone, spacing, icon layout, and action-link chrome stay shared.",
|
|
"canonical": {
|
|
"path": "src/components/shared/InlineNotice.tsx",
|
|
"export": "InlineNotice"
|
|
},
|
|
"scopes": ["src/features/platformPage"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"rounded-lg border",
|
|
"border-amber-300 bg-amber-50",
|
|
"dark:border-amber-800/60 dark:bg-amber-900/20"
|
|
],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/InlineNotice.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "settings-external-text-link-local-anchor",
|
|
"category": "link",
|
|
"summary": "Settings surfaces must not hand-code raw new-tab anchors for external documentation links; compose ExternalTextLink for inline/text links or ButtonLink for button-styled actions.",
|
|
"canonical": {
|
|
"path": "src/components/shared/ExternalTextLink.tsx",
|
|
"export": "ExternalTextLink"
|
|
},
|
|
"scopes": ["src/components/Settings"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["<a", "target=\"_blank\""],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Settings/__tests__/ProLicensePanel.test.tsx",
|
|
"src/components/Settings/__tests__/settingsArchitecture.test.ts"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/ExternalTextLink.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"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": "settings-local-border-loading-spinner-shell",
|
|
"category": "loading-indicator",
|
|
"summary": "Settings surfaces must not recreate border-top-transparent 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/Settings"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["border-t-transparent", "animate-spin"],
|
|
"legacyReason": "Retired migration debt. Settings AI, SSO, API token, and update loading states compose LoadingSpinner instead of local border-based spinner shells.",
|
|
"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": "settings-local-border-bottom-loading-spinner-shell",
|
|
"category": "loading-indicator",
|
|
"summary": "Settings surfaces must not recreate border-bottom 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/Settings"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["border-b-2", "animate-spin"],
|
|
"legacyReason": "Retired migration debt. Settings RBAC and agent profile loading states compose LoadingSpinner instead of local border-bottom spinner shells.",
|
|
"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": "shared-component-local-loading-spinner-shell",
|
|
"category": "loading-indicator",
|
|
"summary": "Shared primitive internals must not recreate local animate-spin loading shells; compose LoadingSpinner so pages inherit the canonical size, tone, and accessibility behavior by construction.",
|
|
"canonical": {
|
|
"path": "src/components/shared/LoadingSpinner.tsx",
|
|
"export": "LoadingSpinner"
|
|
},
|
|
"scopes": [
|
|
"src/components/shared/Button.tsx",
|
|
"src/components/shared/HistoryChartOverlay.tsx",
|
|
"src/components/shared/PulseDataGrid.tsx"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["animate-spin"],
|
|
"legacyReason": "Retired migration debt. Button loading state, PulseDataGrid loading rows, and HistoryChart overlays compose LoadingSpinner instead of local SVG or border spinner shells.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/LoadingSpinner.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/Button.test.tsx",
|
|
"src/components/shared/__tests__/HistoryChart.test.tsx",
|
|
"src/components/shared/__tests__/PulseDataGrid.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "drawer-discovery-local-loading-fallback",
|
|
"category": "loading-indicator",
|
|
"summary": "Resource and workload drawer discovery Suspense fallbacks must not recreate local border spinner rows; compose DiscoveryLoadingFallback so all drawer loading rows inherit the same spinner, spacing, copy, and status semantics.",
|
|
"canonical": {
|
|
"path": "src/components/shared/DiscoveryLoadingFallback.tsx",
|
|
"export": "DiscoveryLoadingFallback"
|
|
},
|
|
"scopes": [
|
|
"src/components/Infrastructure/ResourceDetailDrawer.tsx",
|
|
"src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx",
|
|
"src/components/Workloads/GuestDrawer.tsx",
|
|
"src/components/Workloads/NodeDrawer.tsx",
|
|
"src/features/docker/DockerHostDrawer.tsx"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["border-t-transparent", "animate-spin"],
|
|
"legacyReason": "Retired migration debt. Discovery tab fallbacks in drawers compose DiscoveryLoadingFallback instead of local centered border-spinner rows.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/DiscoveryLoadingFallback.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "login-local-border-loading-spinner-shell",
|
|
"category": "loading-indicator",
|
|
"summary": "Login and first-session auth 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/Login.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["border-t-transparent", "animate-spin"],
|
|
"legacyReason": "Retired migration debt. Login auth checks, setup fallback, and submit-pending states compose LoadingSpinner instead of local border or SVG spinner shells.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/components/shared/SharedPrimitives.guardrails.test.ts"],
|
|
"proof": [
|
|
"src/components/shared/LoadingSpinner.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/__tests__/Login.test.tsx",
|
|
"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": "settings-panel-local-centered-empty-state",
|
|
"category": "empty-state",
|
|
"summary": "Migrated settings panels must not recreate centered text/icon empty-state shells.",
|
|
"canonical": {
|
|
"path": "src/components/shared/EmptyState.tsx",
|
|
"export": "EmptyState"
|
|
},
|
|
"scopes": [
|
|
"src/components/Settings/AgentProfilesPanel.tsx",
|
|
"src/components/Settings/SSOProvidersPanel.tsx"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["text-center py-8 text-muted"],
|
|
"legacyReason": "Retired migration debt. Embedded settings panel empty states must use EmptyState variant=\"panel\" instead of local centered icon/text stacks.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/EmptyState.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "settings-panel-local-action-empty-state",
|
|
"category": "empty-state",
|
|
"summary": "Availability settings must not recreate the centered action empty-state shell.",
|
|
"canonical": {
|
|
"path": "src/components/shared/EmptyState.tsx",
|
|
"export": "EmptyState"
|
|
},
|
|
"scopes": ["src/components/Settings/AvailabilitySettingsPanel.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["flex flex-col items-center justify-center gap-3 px-4 py-12 text-center"],
|
|
"legacyReason": "Retired migration debt. Availability settings empty states must use EmptyState variant=\"panel\" with a Button action slot.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/EmptyState.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "settings-panel-local-dashed-empty-state",
|
|
"category": "empty-state",
|
|
"summary": "Audit Log must not recreate a local dashed empty-state frame.",
|
|
"canonical": {
|
|
"path": "src/components/shared/EmptyState.tsx",
|
|
"export": "EmptyState"
|
|
},
|
|
"scopes": ["src/components/Settings/AuditLogPanel.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"text-center py-12 px-4 bg-surface-alt rounded-md border border-dashed border-border"
|
|
],
|
|
"legacyReason": "Retired migration debt. Audit Log empty states must use EmptyState so framed panel spacing, icon treatment, and action slots stay shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/EmptyState.test.tsx",
|
|
"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",
|
|
"src/features/vmware"
|
|
],
|
|
"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-uptime-function-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables and drawers must not recreate function-based 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",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"function formatUptime",
|
|
"const days = Math.floor(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/proxmox",
|
|
"src/features/truenas"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"pathIncludes": ["Table"],
|
|
"pathExcludes": ["__tests__"],
|
|
"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/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/truenas"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"pathIncludes": ["Table"],
|
|
"pathExcludes": ["__tests__"],
|
|
"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-format-uptime-import",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform table files must not call the generic formatUptime utility directly for uptime cells.",
|
|
"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",
|
|
"src/features/vmware"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"pathIncludes": ["Table"],
|
|
"pathExcludes": ["__tests__"],
|
|
"allPatterns": ["formatUptime("],
|
|
"legacyReason": "Retired migration debt. Dense platform table uptime cells must compose formatPlatformTableUptimeValue so compact/full labels 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-integer-format-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables and drawers must not recreate local integer format helpers for count labels.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableIntegerValue"
|
|
},
|
|
"scopes": ["src/components/Kubernetes", "src/features/proxmox"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["Math.round(", ".toLocaleString()"],
|
|
"legacyReason": "Retired migration debt. Dense platform integer count labels must use formatPlatformTableIntegerValue so rounding, locale 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-integer-count-tolocalestring",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables and drawers must not call toLocaleString directly for count cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableIntegerValue"
|
|
},
|
|
"scopes": ["src/components/Kubernetes", "src/features/proxmox"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["toLocaleString()"],
|
|
"legacyReason": "Retired migration debt. Dense platform integer count cells must use formatPlatformTableIntegerValue and PlatformTableNumberValue instead of local count formatting.",
|
|
"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-ready-count-ratio-label",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Kubernetes networking tables must not recreate ready/total endpoint count labels with local template strings.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTableCountRatioValue"
|
|
},
|
|
"scopes": ["src/features/kubernetes/KubernetesNetworkingTable.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["readyEndpointCount", "/${", " ready`"],
|
|
"legacyReason": "Retired migration debt. Ready/total endpoint labels must compose formatPlatformTableCountRatioValue so zero-default, suffix, and empty-label behavior stay shared with count-ratio cells.",
|
|
"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-date-time-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "TrueNAS protection tables must not recreate compact completed-time helpers for timestamp cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableDateTimeValue"
|
|
},
|
|
"scopes": ["src/features/truenas/TrueNASProtectionTable.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const formatPointTime", "toLocaleString(undefined, {", "minute: '2-digit'"],
|
|
"legacyReason": "Retired migration debt. Compact platform timestamp cells must compose PlatformTableDateTimeValue so date parsing, empty markers, and tabular styling 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-vsphere-activity-date-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "vSphere activity tables must not recreate compact activity-date helpers for timestamp cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableDateTimeValue"
|
|
},
|
|
"scopes": ["src/features/vmware/VsphereActivityTable.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"const formatActivityDate",
|
|
"toLocaleString(undefined, {",
|
|
"minute: '2-digit'"
|
|
],
|
|
"legacyReason": "Retired migration debt. Compact platform timestamp cells must compose PlatformTableDateTimeValue so date parsing, empty markers, minimum-year handling, and tabular styling 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-relative-time-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not call formatRelativeTime directly for dense timestamp-age cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableRelativeTimeValue"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker",
|
|
"src/features/kubernetes",
|
|
"src/features/proxmox",
|
|
"src/features/standalone"
|
|
],
|
|
"extensions": [".tsx"],
|
|
"pathIncludes": ["Table"],
|
|
"pathExcludes": ["__tests__"],
|
|
"allPatterns": ["formatRelativeTime("],
|
|
"legacyReason": "Retired migration debt. Platform table relative timestamp cells must compose PlatformTableRelativeTimeValue so compact labels, empty markers, and tabular styling 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-duration-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local elapsed-duration helpers for seconds/minutes duration cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableDurationValue"
|
|
},
|
|
"scopes": ["src/features/proxmox/ProxmoxReplicationTable.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["function formatDuration", "seconds < 3600", "return `${h}h ${m}m`"],
|
|
"legacyReason": "Retired migration debt. Platform duration cells must compose PlatformTableDurationValue so elapsed-duration labels, fallback text, empty markers, and tabular styling 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-interval-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Platform tables must not recreate local interval helpers for polling interval cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "PlatformTableDurationValue"
|
|
},
|
|
"scopes": ["src/features/standalone/AvailabilityChecksTable.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["const formatInterval", "pollIntervalSeconds", "toFixed(1)}m"],
|
|
"legacyReason": "Retired migration debt. Platform interval cells must compose PlatformTableDurationValue so seconds/minutes labels, empty markers, and tabular styling 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-weighted-width-helper",
|
|
"category": "platform-table-layout",
|
|
"summary": "Responsive platform table models must not recreate local weighted-width percentage helpers.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "getPlatformTableWeightedColumnWidthStyle"
|
|
},
|
|
"scopes": [
|
|
"src/features/docker/dockerContainerTableModel.ts",
|
|
"src/features/proxmox/proxmoxHostTableModel.ts"
|
|
],
|
|
"extensions": [".ts"],
|
|
"allPatterns": [
|
|
"const formatPercentage",
|
|
"value.toFixed(4)",
|
|
"return { width: formatPercentage(width) }"
|
|
],
|
|
"legacyReason": "Retired migration debt. Responsive platform table width normalization must use getPlatformTableWeightedColumnWidthStyle so percentage rounding and zero-width fallback 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-proxmox-percent-tofixed-label",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Proxmox platform tables and drawers must not recreate one-decimal percent labels with toFixed.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTablePercentValue"
|
|
},
|
|
"scopes": ["src/features/proxmox"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["toFixed(1)}%"],
|
|
"legacyReason": "Retired migration debt. Proxmox platform percent labels must compose formatPlatformTablePercentValue or PlatformTablePercentValue so formatting stays 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-proxmox-percent-label-helper",
|
|
"category": "platform-table-text-value",
|
|
"summary": "Proxmox platform tables must not recreate local percent label helpers for metric cells.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "formatPlatformTablePercentValue"
|
|
},
|
|
"scopes": ["src/features/proxmox"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["formatPercentLabel"],
|
|
"legacyReason": "Retired migration debt. Proxmox platform percent metric labels must compose formatPlatformTablePercentValue so ratio normalization, clamping, 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-table-model-local-finite-metric-helper",
|
|
"category": "platform-table-metric",
|
|
"summary": "Platform table models must not recreate local finiteMetric helpers for metric normalization.",
|
|
"canonical": {
|
|
"path": "src/features/platformPage/sharedPlatformPage.tsx",
|
|
"export": "getPlatformTableFiniteMetric"
|
|
},
|
|
"scopes": ["src/features/standalone/agentMachineTableModel.ts"],
|
|
"extensions": [".ts"],
|
|
"allPatterns": ["const finiteMetric"],
|
|
"legacyReason": "Retired migration debt. Platform table model-side metric normalization must use getPlatformTableFiniteMetric so table presentation validity remains shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.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 textareas must compose FormTextarea so labels, help text, controlled values, and multiline field styling stay canonical.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FormTextarea.tsx",
|
|
"export": "FormTextarea"
|
|
},
|
|
"scopes": ["src/components/Alerts", "src/features/alerts"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["<textarea"],
|
|
"legacyReason": "Retired migration debt. Alert multiline fields belong to FormTextarea so row, mobile, destination, and timeline editors use the same labelled primitive.",
|
|
"allowedPaths": [],
|
|
"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 so labels, help text, controlled values, and multiline field styling stay canonical.",
|
|
"canonical": {
|
|
"path": "src/components/shared/FormTextarea.tsx",
|
|
"export": "FormTextarea"
|
|
},
|
|
"scopes": ["src/components/Settings"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["<textarea"],
|
|
"legacyReason": "Retired migration debt. Settings multiline fields belong to FormTextarea, including provider metadata and access mapping editors.",
|
|
"allowedPaths": [],
|
|
"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": "filter-button-group-local-feature-view-toggle-styles",
|
|
"category": "segmented-selector",
|
|
"summary": "Feature table view toggles must compose FilterButtonGroup instead of copying the compact bordered group and active view-button styling locally.",
|
|
"canonical": {
|
|
"path": "src/components/shared/filterButtonGroupModel.ts",
|
|
"export": "getFilterButtonGroupButtonClass"
|
|
},
|
|
"scopes": ["src/features"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"inline-flex items-center gap-1 rounded-md border border-border bg-surface p-1",
|
|
"inline-flex min-h-8 items-center gap-1.5 rounded-sm px-3 text-xs font-medium transition-colors"
|
|
],
|
|
"legacyReason": "Retired migration debt. Feature table view toggles must use FilterButtonGroup; local bordered segmented-view selector forks are not allowed.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": ["src/features/proxmox/__tests__/ProxmoxBackupsTable.test.tsx"],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "selectable-pill-button-local-scope-selector-styles",
|
|
"category": "selectable-pill-button",
|
|
"summary": "Settings and feature surfaces must not copy the rounded active/inactive selector pill styling owned by SelectablePillButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/selectablePillModel.ts",
|
|
"export": "getSelectablePillButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings", "src/features", "src/pages"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"rounded-full border px-3 py-2 text-sm font-semibold transition",
|
|
"border-blue-500 bg-blue-600 text-white shadow-sm",
|
|
"hover:border-blue-400 hover:text-blue-600"
|
|
],
|
|
"legacyReason": "Retired migration debt. Pressed/unpressed pill selectors belong to SelectablePillButton.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [
|
|
"src/components/Settings/__tests__/APITokenManager.test.tsx",
|
|
"src/components/shared/SelectablePillButton.test.tsx"
|
|
],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/SelectablePillButton.test.tsx",
|
|
"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": "resource-detail-local-provider-row-model",
|
|
"category": "row-detail",
|
|
"summary": "Provider resource detail drawer models must not define provider-local row and section primitives.",
|
|
"canonical": {
|
|
"path": "src/components/shared/detailSectionModel.ts",
|
|
"export": "DetailSection"
|
|
},
|
|
"scopes": ["src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts"],
|
|
"extensions": [".ts"],
|
|
"allPatterns": ["ResourceDetailDrawerVMwareRow", "ResourceDetailDrawerVMwareSection"],
|
|
"legacyReason": "Retired migration debt. Provider detail drawers own fields and labels, while row and section shape belongs to DetailSection / DetailRow in detailSectionModel.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "resource-detail-local-vmware-detail-shell",
|
|
"category": "row-detail",
|
|
"summary": "The resource detail drawer must not render VMware detail sections with a local vSphere card/table shell.",
|
|
"canonical": {
|
|
"path": "src/components/shared/DetailSectionTable.tsx",
|
|
"export": "DetailSectionTable"
|
|
},
|
|
"scopes": ["src/components/Infrastructure/ResourceDetailDrawerOverviewTab.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["vmwareRowToneClass", "<For each={drawer.vmwareDetailSections()}"],
|
|
"legacyReason": "Retired migration debt. VMware detail rows share the same DetailSectionTable renderer and tone classes as other provider detail drawers.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/Infrastructure/__tests__/resourceDetailDrawerVmwareModel.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "resource-detail-local-byte-format-helper",
|
|
"category": "row-detail",
|
|
"summary": "Resource detail drawer models must not recreate local byte-scaling helpers.",
|
|
"canonical": {
|
|
"path": "src/components/shared/detailSectionModel.ts",
|
|
"export": "formatDetailBytesValue"
|
|
},
|
|
"scopes": [
|
|
"src/components/Infrastructure/resourceDetailDrawerKubernetesModel.ts",
|
|
"src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts"
|
|
],
|
|
"extensions": [".ts"],
|
|
"allPatterns": ["const formatBytes", "const units = ['B'"],
|
|
"legacyReason": "Retired migration debt. Detail drawer byte labels must use formatDetailBytesValue so byte precision, zero handling, and unit scaling stay shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/DetailSectionTable.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "resource-detail-local-capacity-byte-format-helper",
|
|
"category": "row-detail",
|
|
"summary": "Resource detail drawer models must not recreate compact local capacity byte helpers.",
|
|
"canonical": {
|
|
"path": "src/components/shared/detailSectionModel.ts",
|
|
"export": "formatDetailBytesValue"
|
|
},
|
|
"scopes": ["src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts"],
|
|
"extensions": [".ts"],
|
|
"allPatterns": ["const formatCapacityBytes", "const units = ['B'"],
|
|
"legacyReason": "Retired migration debt. Compact detail drawer capacity labels must use formatDetailBytesValue with compact precision instead of local byte scaling.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/DetailSectionTable.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "resource-detail-local-count-format-helper",
|
|
"category": "row-detail",
|
|
"summary": "Resource detail drawer models must not recreate local count pluralization helpers.",
|
|
"canonical": {
|
|
"path": "src/components/shared/detailSectionModel.ts",
|
|
"export": "formatDetailCountValue"
|
|
},
|
|
"scopes": [
|
|
"src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts",
|
|
"src/components/Infrastructure/resourceDetailDrawerVmwareModel.ts"
|
|
],
|
|
"extensions": [".ts"],
|
|
"allPatterns": ["const formatCount"],
|
|
"legacyReason": "Retired migration debt. Detail drawer count labels must use formatDetailCountValue so integer formatting and pluralization stay shared.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"src/components/shared/__tests__/DetailSectionTable.test.tsx",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"id": "resource-detail-local-integer-format-helper",
|
|
"category": "row-detail",
|
|
"summary": "Resource detail drawer models must not recreate local integer formatters.",
|
|
"canonical": {
|
|
"path": "src/components/shared/detailSectionModel.ts",
|
|
"export": "formatDetailIntegerValue"
|
|
},
|
|
"scopes": ["src/components/Infrastructure/resourceDetailDrawerTrueNASModel.ts"],
|
|
"extensions": [".ts"],
|
|
"allPatterns": ["const formatInteger", "new Intl.NumberFormat().format"],
|
|
"legacyReason": "Retired migration debt. Detail drawer integer labels must use formatDetailIntegerValue so count-like numeric rows share locale formatting.",
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"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-patrol-approval-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Patrol approval surfaces must not copy green execute-action button shells; use Button with the success variant and sm size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/patrol", "src/features/patrol"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"px-3 py-1.5 bg-green-600 hover:bg-green-700",
|
|
"text-white text-xs font-medium rounded"
|
|
],
|
|
"legacyReason": "Retired migration debt. Patrol approval execute 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-warning-solid-patrol-approval-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Patrol approval surfaces must not copy amber solid review or reapprove button shells; use Button with the warningSolid variant and sm size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/patrol", "src/features/patrol"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"px-3 py-1.5 bg-amber-600 hover:bg-amber-700",
|
|
"text-white text-xs font-medium rounded"
|
|
],
|
|
"legacyReason": "Retired migration debt. Patrol approval warning 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-patrol-approval-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Patrol approval surfaces must not copy blue assistant-action button shells; use Button with the primary variant and sm size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/patrol", "src/features/patrol"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"px-3 py-1.5 bg-blue-600 hover:bg-blue-700",
|
|
"text-white text-xs font-medium rounded"
|
|
],
|
|
"legacyReason": "Retired migration debt. Patrol Assistant handoff 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-neutral-patrol-approval-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Patrol approval surfaces must not copy neutral deny button shells; use Button with the secondary or ghost variant and sm size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/patrol", "src/features/patrol"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"px-3 py-1.5 bg-surface-alt hover:bg-surface-hover",
|
|
"text-base-content text-xs font-medium rounded-md"
|
|
],
|
|
"legacyReason": "Retired migration debt. Patrol approval deny 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-success-ghost-settings-row-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy emerald ghost row-action shells; use Button with the successGhost 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 px-2 py-1 text-xs font-medium text-emerald-700",
|
|
"hover:bg-emerald-50 dark:text-emerald-300 dark:hover:bg-emerald-900"
|
|
],
|
|
"legacyReason": "Retired migration debt. Positive 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-info-settings-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy the blue documentation action-button shell; use Button or ButtonLink with the info 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-blue-200 bg-blue-50 px-3 py-2 text-sm font-semibold text-blue-700"
|
|
],
|
|
"legacyReason": "Retired migration debt. Blue documentation action links 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-danger-ghost-settings-row-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Settings and infrastructure surfaces must not copy red ghost row-action shells; use Button with the dangerGhost 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 px-2 py-1 text-xs font-medium text-red-600",
|
|
"hover:bg-red-50 dark:text-red-300 dark:hover:bg-red-900"
|
|
],
|
|
"legacyReason": "Retired migration debt. Destructive 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-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"
|
|
]
|
|
},
|
|
{
|
|
"id": "billing-admin-organization-row-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Billing admin organization Suspend and Activate row actions must not recreate the compact hosted-billing button shell; use Button with the secondary variant and sm size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings/BillingAdminOrganizationsTable.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"px-2.5 py-1.5 text-xs font-medium rounded-md border border-border bg-surface hover:bg-surface-hover disabled:opacity-50"
|
|
],
|
|
"legacyReason": "Retired migration debt. Hosted billing organization 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": "billing-admin-organization-reload-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "Billing admin organization expanded-row Reload actions must not recreate the compact hosted-billing button shell; use Button with the secondary variant and xs size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings/BillingAdminOrganizationsTable.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"px-2 py-1 text-xs rounded-md border border-border bg-surface hover:bg-surface-hover"
|
|
],
|
|
"legacyReason": "Retired migration debt. Hosted billing expanded-row reload 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": "sso-provider-primary-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "SSO provider primary add/save actions must not recreate local blue settings button shells; use Button with the primary variant and settingsAction or md size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings/SSOProvidersPanel.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"min-h-10 sm:min-h-9 px-3 py-2.5 text-sm font-medium bg-blue-600 text-white rounded-md",
|
|
"hover:bg-blue-700 transition-colors"
|
|
],
|
|
"legacyReason": "Retired migration debt. SSO provider primary 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": "sso-provider-secondary-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "SSO provider secondary add/test/preview actions must not recreate local bordered or surface-hover settings button shells; use Button with the outline or secondary variant.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings/SSOProvidersPanel.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"text-sm font-medium border border-border text-base-content rounded-md",
|
|
"hover:bg-surface-hover transition-colors"
|
|
],
|
|
"legacyReason": "Retired migration debt. SSO provider secondary 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": "sso-provider-inline-form-action-local-shell",
|
|
"category": "action-button",
|
|
"summary": "SSO provider inline Test and Preview form actions must not recreate local surface-hover button shells; use Button with the secondary variant and mdCompact size.",
|
|
"canonical": {
|
|
"path": "src/components/shared/buttonModel.ts",
|
|
"export": "getButtonClass"
|
|
},
|
|
"scopes": ["src/components/Settings/SSOProvidersPanel.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": [
|
|
"px-3 py-2 text-sm font-medium bg-surface-hover text-base-content rounded-md",
|
|
"disabled:cursor-not-allowed whitespace-nowrap"
|
|
],
|
|
"legacyReason": "Retired migration debt. SSO provider inline form 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": "sso-provider-action-icon-local-shell",
|
|
"category": "action-button",
|
|
"summary": "SSO provider edit, delete, and close icon actions must not recreate local icon-button shells; use ActionIconButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "ActionIconButton"
|
|
},
|
|
"scopes": ["src/components/Settings/SSOProvidersPanel.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["p-2 text-slate-500", "rounded-md transition-colors"],
|
|
"legacyReason": "Retired migration debt. SSO provider icon actions belong to ActionIconButton.",
|
|
"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": "sso-provider-copy-link-local-shell",
|
|
"category": "copy-action",
|
|
"summary": "SSO provider metadata copy controls must not recreate local link-like copy buttons; use CopyValueButton.",
|
|
"canonical": {
|
|
"path": "src/components/shared/Button.tsx",
|
|
"export": "CopyValueButton"
|
|
},
|
|
"scopes": ["src/components/Settings/SSOProvidersPanel.tsx"],
|
|
"extensions": [".tsx"],
|
|
"allPatterns": ["text-blue-600 hover:underline flex items-center gap-1"],
|
|
"legacyReason": "Retired migration debt. SSO provider metadata copy actions belong to CopyValueButton.",
|
|
"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": "settings-loading-state-shared-skeleton-required",
|
|
"category": "loading-skeleton",
|
|
"summary": "Settings LoadingState components must compose SettingsLoadingSkeleton, so new settings panels cannot introduce local loading skeleton templates.",
|
|
"canonical": {
|
|
"path": "src/components/shared/SettingsLoadingSkeleton.tsx",
|
|
"export": "SettingsLoadingSkeleton"
|
|
},
|
|
"scopes": ["src/components/Settings"],
|
|
"extensions": [".tsx"],
|
|
"pathIncludes": ["LoadingState"],
|
|
"triggerPatterns": ["LoadingState"],
|
|
"requiredPatterns": ["SettingsLoadingSkeleton"],
|
|
"allowedPaths": [],
|
|
"ignoredPaths": [],
|
|
"proof": [
|
|
"src/components/shared/SettingsLoadingSkeleton.test.tsx",
|
|
"src/components/shared/SharedPrimitives.guardrails.test.ts",
|
|
"scripts/shared-template-audit.mjs"
|
|
]
|
|
},
|
|
{
|
|
"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"
|
|
]
|
|
}
|
|
]
|
|
}
|