diff --git a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md index f9315ab7e9..17b29aa310 100644 --- a/docs/release-control/v6/internal/subsystems/agent-lifecycle.md +++ b/docs/release-control/v6/internal/subsystems/agent-lifecycle.md @@ -34,16 +34,26 @@ management, and fleet control surfaces. 10. `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts` 11. `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx` 12. `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx` -13. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` -14. `frontend-modern/src/components/Settings/UnifiedAgents.tsx` -15. `frontend-modern/src/components/Settings/NodeModal.tsx` -16. `frontend-modern/src/components/Settings/nodeModalModel.ts` -17. `frontend-modern/src/components/Settings/useNodeModalState.ts` -18. `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx` -19. `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx` -20. `frontend-modern/src/utils/agentProfilesPresentation.ts` -21. `frontend-modern/src/utils/agentInstallCommand.ts` -22. `frontend-modern/src/api/nodes.ts` +13. `frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx` +14. `frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx` +15. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx` +16. `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx` +17. `frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx` +18. `frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx` +19. `frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx` +20. `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx` +21. `frontend-modern/src/components/Settings/SettingsSectionNav.tsx` +22. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx` +23. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts` +24. `frontend-modern/src/components/Settings/UnifiedAgents.tsx` +25. `frontend-modern/src/components/Settings/NodeModal.tsx` +26. `frontend-modern/src/components/Settings/nodeModalModel.ts` +27. `frontend-modern/src/components/Settings/useNodeModalState.ts` +28. `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx` +29. `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx` +30. `frontend-modern/src/utils/agentProfilesPresentation.ts` +31. `frontend-modern/src/utils/agentInstallCommand.ts` +32. `frontend-modern/src/api/nodes.ts` ## Shared Boundaries @@ -70,7 +80,7 @@ management, and fleet control surfaces. 3. Add or change runtime-side Unified Agent startup, first-report assembly, and enroll/runtime continuity through `internal/hostagent/`. 4. Keep legacy Unified Agent compatibility names explicitly secondary when touching shared `internal/api/` runtime helpers: the legacy host-route family and `host-agent:*` scope names may remain as ingress or migration aliases, but they must not retake primary ownership in router state, live runtime scope checks, handler commentary, or operator-facing guidance. 5. Add or change installer flags, persisted service arguments, or upgrade-safe re-entry behavior through `scripts/install.sh` and `scripts/install.ps1`. -6. Add or change profile management, the extracted agent profiles runtime owner, the pure unified-agent inventory/install model, shared frontend install-command assembly, Proxmox setup/install API transport, setup-completion install handoff transport, deploy-fallback manual install transport, and fleet-control presentation through `frontend-modern/src/api/agentProfiles.ts`, `frontend-modern/src/api/nodes.ts`, `frontend-modern/src/components/Settings/AgentProfilesPanel.tsx`, `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`, `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/NodeModal.tsx`, `frontend-modern/src/components/Settings/nodeModalModel.ts`, `frontend-modern/src/components/Settings/useNodeModalState.ts`, `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`, `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`. +6. Add or change profile management, the extracted agent profiles runtime owner, the pure unified-agent inventory/install model, the direct Proxmox workspace shell and section owners, shared frontend install-command assembly, Proxmox setup/install API transport, setup-completion install handoff transport, deploy-fallback manual install transport, and fleet-control presentation through `frontend-modern/src/api/agentProfiles.ts`, `frontend-modern/src/api/nodes.ts`, `frontend-modern/src/components/Settings/AgentProfilesPanel.tsx`, `frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts`, `frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx`, `frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx`, `frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx`, `frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx`, `frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx`, `frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx`, `frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx`, `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`, `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`, `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`, `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`, `frontend-modern/src/components/Settings/NodeModal.tsx`, `frontend-modern/src/components/Settings/nodeModalModel.ts`, `frontend-modern/src/components/Settings/useNodeModalState.ts`, `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`, `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`, and `frontend-modern/src/utils/agentInstallCommand.ts`. ## Forbidden Paths @@ -137,6 +147,15 @@ reintroducing local array or JSON parsing rules. The owned backend API surfaces must preserve the exact-release installer fallback, canonical /api/auto-register behavior, and hosted org install-command contracts instead of leaving those guarantees implied by generic API ownership. +The direct Proxmox fallback workspace now also lives explicitly inside this +lifecycle boundary: `InfrastructureWorkspace.tsx`, +`InfrastructureInstallPanel.tsx`, `InfrastructureReportingPanel.tsx`, +`ProxmoxSettingsPanel.tsx`, `ProxmoxDirectConnectionsCard.tsx`, +`ProxmoxDiscoveryResultsCard.tsx`, `ProxmoxDeleteNodeDialog.tsx`, +`ConfiguredNodeTables.tsx`, `SettingsSectionNav.tsx`, and +`useInfrastructureSettingsState.ts` own the fallback install/direct/reporting +operator flow instead of leaving those panels ungoverned beside the canonical +unified-agent install path. That canonical /api/auto-register behavior now also includes hostname/IP continuity: reruns that arrive through a different canonical host form must reuse the same Pulse-managed node record and token instead of forking duplicate fleet entries. diff --git a/docs/release-control/v6/internal/subsystems/registry.json b/docs/release-control/v6/internal/subsystems/registry.json index 587f861cf3..5e86e3746d 100644 --- a/docs/release-control/v6/internal/subsystems/registry.json +++ b/docs/release-control/v6/internal/subsystems/registry.json @@ -368,13 +368,23 @@ "frontend-modern/src/api/nodes.ts", "frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx", "frontend-modern/src/components/Settings/AgentProfilesPanel.tsx", + "frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx", + "frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx", "frontend-modern/src/components/Settings/InfrastructureOperationsController.tsx", "frontend-modern/src/components/Settings/infrastructureOperationsModel.tsx", + "frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx", + "frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx", "frontend-modern/src/components/Settings/NodeModal.tsx", "frontend-modern/src/components/Settings/nodeModalModel.ts", + "frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx", + "frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx", + "frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx", + "frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx", + "frontend-modern/src/components/Settings/SettingsSectionNav.tsx", "frontend-modern/src/components/Settings/UnifiedAgents.tsx", "frontend-modern/src/components/Settings/useAgentProfilesPanelState.ts", "frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx", + "frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts", "frontend-modern/src/components/Settings/useNodeModalState.ts", "frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx", "frontend-modern/src/utils/agentInstallCommand.ts", @@ -587,6 +597,32 @@ "frontend-modern/src/components/Settings/__tests__/InfrastructureOperationsModel.test.tsx", "frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx" ] + }, + { + "id": "direct-proxmox-workspace-surface", + "label": "direct Proxmox workspace lifecycle proof", + "match_prefixes": [], + "match_files": [ + "frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx", + "frontend-modern/src/components/Settings/InfrastructureInstallPanel.tsx", + "frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx", + "frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx", + "frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx", + "frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx", + "frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx", + "frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx", + "frontend-modern/src/components/Settings/SettingsSectionNav.tsx", + "frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts" + ], + "allow_same_subsystem_tests": false, + "test_prefixes": [], + "exact_files": [ + "frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx", + "frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts", + "frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts", + "frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx", + "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts" + ] } ] } diff --git a/frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx b/frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx new file mode 100644 index 0000000000..48f4987f8a --- /dev/null +++ b/frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx @@ -0,0 +1,89 @@ +import type { Component } from 'solid-js'; +import { Show } from 'solid-js'; +import { Card } from '@/components/shared/Card'; +import { Dialog } from '@/components/shared/Dialog'; +import { SectionHeader } from '@/components/shared/SectionHeader'; + +interface ProxmoxDeleteNodeDialogProps { + deleteNodeLoading: boolean; + nodePendingDeleteHost: string; + nodePendingDeleteLabel: string; + nodePendingDeleteType: string; + nodePendingDeleteTypeLabel: string; + onCancel: () => void; + onDelete: () => Promise; +} + +export const ProxmoxDeleteNodeDialog: Component = (props) => { + return ( + + + +
+

+ Removing this {props.nodePendingDeleteTypeLabel.toLowerCase()} also scrubs the Pulse + footprint on the host - the proxy service, SSH key, API token, and bind mount are all + cleaned up automatically. +

+
+

What happens next

+
    +
  • Pulse removes the node entry and clears related alerts.
  • +
  • + {props.nodePendingDeleteHost ? ( + <> + The host {props.nodePendingDeleteHost}{' '} + loses the proxy service, SSH key, and API token. + + ) : ( + 'The host loses the proxy service, SSH key, and API token.' + )} +
  • +
  • + If the host comes back later, rerunning the setup script reinstalls everything with + a fresh key. +
  • + +
  • + Backup user tokens on the PBS are removed, so jobs referencing them will no + longer authenticate until the node is re-added. +
  • +
    + +
  • + Mail gateway tokens are removed as part of the cleanup; re-enroll to restore + outbound telemetry. +
  • +
    +
+
+
+ +
+ + +
+
+
+ ); +}; diff --git a/frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx b/frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx new file mode 100644 index 0000000000..cf349e3bd7 --- /dev/null +++ b/frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx @@ -0,0 +1,144 @@ +import type { Component, JSX } from 'solid-js'; +import { Dynamic } from 'solid-js/web'; +import { Show } from 'solid-js'; +import { Card } from '@/components/shared/Card'; +import { Toggle } from '@/components/shared/Toggle'; +import type { ToggleChangeEvent } from '@/components/shared/Toggle'; +import { getSettingsConfigurationLoadingState } from '@/utils/settingsShellPresentation'; +import type { ProxmoxVariantPresentation } from '@/utils/proxmoxSettingsPresentation'; +import type { NodeConfigWithStatus } from '@/types/nodes'; +import type { DiscoveredServer, NodeType } from './useInfrastructureSettingsState'; + +interface ProxmoxDirectConnectionsCardProps { + activeAgent: NodeType; + activeConfig: ProxmoxVariantPresentation; + activeConfiguredNodes: NodeConfigWithStatus[]; + activeDiscoveredNodes: DiscoveredServer[]; + configuredTable: JSX.Element; + discoveryEnabled: boolean; + envOverrides: Record; + initialLoadComplete: boolean; + onDiscoveryToggle: (event: ToggleChangeEvent) => Promise; + onOpenCreateNode: (type: NodeType) => void; + onRefreshDiscovery: () => Promise; + savingDiscoverySettings: boolean; +} + +export const ProxmoxDirectConnectionsCard: Component = ( + props, +) => { + return ( + <> + +
+ {getSettingsConfigurationLoadingState().text} +
+
+ + + +
+
+
+

{props.activeConfig.title}

+
+ + {props.activeConfiguredNodes.length} configured + + + + {props.activeDiscoveredNodes.length} discovered + + +
+
+
+
+ Discovery + + {props.discoveryEnabled ? 'On' : 'Off'} + + } + /> +
+ + + + + + +
+
+ + 0}>{props.configuredTable} + + +
+
+ +
+

+ {props.activeConfig.emptyTitle} +

+

{props.activeConfig.emptyDescription}

+
+
+
+
+
+ + ); +}; diff --git a/frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx b/frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx new file mode 100644 index 0000000000..145646e0e3 --- /dev/null +++ b/frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx @@ -0,0 +1,140 @@ +import type { Component } from 'solid-js'; +import { For, Show } from 'solid-js'; +import Loader from 'lucide-solid/icons/loader'; +import { Card } from '@/components/shared/Card'; +import { formatRelativeTime } from '@/utils/format'; +import type { ProxmoxVariantPresentation } from '@/utils/proxmoxSettingsPresentation'; +import type { DiscoveryScanStatus, DiscoveredServer } from './useInfrastructureSettingsState'; + +interface ProxmoxDiscoveryResultsCardProps { + activeConfig: ProxmoxVariantPresentation; + activeDiscoveredNodes: DiscoveredServer[]; + discoveryScanStatus: DiscoveryScanStatus; + hasDiscoveryTimeouts: boolean; + onOpenDiscoveredNode: (server: DiscoveredServer) => void; +} + +export const ProxmoxDiscoveryResultsCard: Component = ( + props, +) => { + return ( + +
+
+
+

Discovery results

+

+ Found servers that can be turned into direct connections for this Proxmox type. +

+
+
+ + + + {props.activeConfig.scanningLabel} + + + + + Last scan{' '} + {formatRelativeTime( + props.discoveryScanStatus.lastResultAt ?? + props.discoveryScanStatus.lastScanStartedAt, + { compact: true }, + )} + + +
+
+ + 0 + } + > +
+ Discovery issues: +
    + {(error) =>
  • {error}
  • }
    +
+ +

+ Large networks can time out in auto mode. Switch to a custom subnet for faster, + targeted scans. +

+
+
+
+ + +
+ + + Waiting for responses… this can take up to a minute depending on your network size. + +
+
+ + 0} + fallback={ +
+ No discovery matches for this Proxmox type yet. You can still add a direct + connection manually. +
+ } + > +
+ + {(server) => ( + + )} + +
+
+
+
+ ); +}; diff --git a/frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx b/frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx index 1a9d22fe00..450ba5594d 100644 --- a/frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx +++ b/frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx @@ -1,27 +1,22 @@ -import { Component, Accessor, Setter, Show, For, createMemo, createSignal } from 'solid-js'; +import { Component, Accessor, Setter, Show, createMemo, createSignal } from 'solid-js'; import { useNavigate } from '@solidjs/router'; -import { Dynamic } from 'solid-js/web'; -import Loader from 'lucide-solid/icons/loader'; import type { Resource } from '@/types/resource'; import type { PBSInstance, PMGInstance } from '@/types/api'; import type { NodeConfig, NodeConfigWithStatus } from '@/types/nodes'; import type { SecurityStatus as SecurityStatusInfo } from '@/types/config'; import { notificationStore } from '@/stores/notifications'; -import { formatRelativeTime } from '@/utils/format'; -import { Card } from '@/components/shared/Card'; import { CalloutCard } from '@/components/shared/CalloutCard'; -import { Dialog } from '@/components/shared/Dialog'; -import { SectionHeader } from '@/components/shared/SectionHeader'; -import { Toggle } from '@/components/shared/Toggle'; import type { ToggleChangeEvent } from '@/components/shared/Toggle'; import { NodeModal } from './NodeModal'; import { PveNodesTable, PbsNodesTable, PmgNodesTable } from './ConfiguredNodeTables'; +import { ProxmoxDeleteNodeDialog } from './ProxmoxDeleteNodeDialog'; +import { ProxmoxDirectConnectionsCard } from './ProxmoxDirectConnectionsCard'; +import { ProxmoxDiscoveryResultsCard } from './ProxmoxDiscoveryResultsCard'; import { SettingsSectionNav } from './SettingsSectionNav'; import { buildProxmoxDiscoveryPrefillNode, getProxmoxVariantPresentation, } from '@/utils/proxmoxSettingsPresentation'; -import { getSettingsConfigurationLoadingState } from '@/utils/settingsShellPresentation'; import type { DiscoveredServer, DiscoveryScanStatus, @@ -265,329 +260,43 @@ export const ProxmoxSettingsPanel: Component = (props
- -
- {getSettingsConfigurationLoadingState().text} -
-
- - - -
-
-
-

- {activeConfig().title} -

-
- - {activeConfiguredNodes().length} configured - - - - {activeDiscoveredNodes().length} discovered - - -
-
-
-
- Discovery - - {props.discoveryEnabled() ? 'On' : 'Off'} - - } - /> -
- - - - - - -
-
- - 0}>{renderConfiguredTable()} - - -
-
- -
-

- {activeConfig().emptyTitle} -

-

{activeConfig().emptyDescription}

-
-
-
-
-
+ - -
-
-
-

Discovery results

-

- Found servers that can be turned into direct connections for this Proxmox - type. -

-
-
- - - - {activeConfig().scanningLabel} - - - - - Last scan{' '} - {formatRelativeTime( - props.discoveryScanStatus().lastResultAt ?? - props.discoveryScanStatus().lastScanStartedAt, - { compact: true }, - )} - - -
-
- - -
- Discovery issues: -
    - - {(error) =>
  • {error}
  • } -
    -
- -

- Large networks can time out in auto mode. Switch to a custom subnet for - faster, targeted scans. -

-
-
-
- - -
- - - Waiting for responses… this can take up to a minute depending on your network - size. - -
-
- - 0} - fallback={ -
- No discovery matches for this Proxmox type yet. You can still add a direct - connection manually. -
- } - > -
- - {(server) => ( - - )} - -
-
-
-
+
- - - -
-

- Removing this {props.nodePendingDeleteTypeLabel().toLowerCase()} also scrubs the - Pulse footprint on the host — the proxy service, SSH key, API token, and bind mount - are all cleaned up automatically. -

-
-

What happens next

-
    -
  • Pulse removes the node entry and clears related alerts.
  • -
  • - {props.nodePendingDeleteHost() ? ( - <> - The host {props.nodePendingDeleteHost()}{' '} - loses the proxy service, SSH key, and API token. - - ) : ( - 'The host loses the proxy service, SSH key, and API token.' - )} -
  • -
  • - If the host comes back later, rerunning the setup script reinstalls everything - with a fresh key. -
  • - -
  • - Backup user tokens on the PBS are removed, so jobs referencing them will no - longer authenticate until the node is re-added. -
  • -
    - -
  • - Mail gateway tokens are removed as part of the cleanup; re-enroll to restore - outbound telemetry. -
  • -
    -
-
-
- -
- - -
-
-
+
{renderNodeModal('pve')} diff --git a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts index 34005e8cdc..63f073d34b 100644 --- a/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts @@ -110,6 +110,9 @@ import unifiedAgentStatusPresentationSource from '@/utils/unifiedAgentStatusPres import unifiedAgentInventoryPresentationSource from '@/utils/unifiedAgentInventoryPresentation.ts?raw'; import relayPresentationSource from '@/utils/relayPresentation.ts?raw'; import relaySettingsPanelSource from '../RelaySettingsPanel.tsx?raw'; +import proxmoxDeleteNodeDialogSource from '../ProxmoxDeleteNodeDialog.tsx?raw'; +import proxmoxDirectConnectionsCardSource from '../ProxmoxDirectConnectionsCard.tsx?raw'; +import proxmoxDiscoveryResultsCardSource from '../ProxmoxDiscoveryResultsCard.tsx?raw'; import proxmoxSettingsPanelSource from '../ProxmoxSettingsPanel.tsx?raw'; import relayOnboardingCardSource from '@/components/Dashboard/RelayOnboardingCard.tsx?raw'; import generalSettingsPanelSource from '../GeneralSettingsPanel.tsx?raw'; @@ -298,9 +301,16 @@ describe('monitored-system model guardrails', () => { expect(relaySettingsPanelSource).not.toContain('const connectionStatusVariant ='); expect(relaySettingsPanelSource).not.toContain('const connectionStatusText ='); expect(relaySettingsPanelSource).not.toContain('Loading configuration...'); - expect(proxmoxSettingsPanelSource).toContain('getSettingsConfigurationLoadingState'); expect(proxmoxSettingsPanelSource).toContain('CalloutCard'); expect(proxmoxSettingsPanelSource).not.toContain('Loading configuration...'); + expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDirectConnectionsCard'); + expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDiscoveryResultsCard'); + expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDeleteNodeDialog'); + expect(proxmoxSettingsPanelSource).not.toContain('No discovery matches for this Proxmox type yet.'); + expect(proxmoxSettingsPanelSource).not.toContain('What happens next'); + expect(proxmoxDirectConnectionsCardSource).toContain('getSettingsConfigurationLoadingState'); + expect(proxmoxDiscoveryResultsCardSource).toContain('formatRelativeTime'); + expect(proxmoxDeleteNodeDialogSource).toContain('SectionHeader'); expect(relayOnboardingCardSource).toContain('@/utils/relayPresentation'); expect(relayOnboardingCardSource).toContain('RELAY_ONBOARDING_TITLE'); expect(relayOnboardingCardSource).toContain('RELAY_ONBOARDING_DESCRIPTION'); diff --git a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts index 5e1c576ab6..93c3f2979b 100644 --- a/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts +++ b/frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts @@ -43,6 +43,10 @@ import organizationOverviewPanelSource from '../OrganizationOverviewPanel.tsx?ra import organizationAccessPanelSource from '../OrganizationAccessPanel.tsx?raw'; import organizationSharingPanelSource from '../OrganizationSharingPanel.tsx?raw'; import organizationBillingPanelSource from '../OrganizationBillingPanel.tsx?raw'; +import proxmoxDeleteNodeDialogSource from '../ProxmoxDeleteNodeDialog.tsx?raw'; +import proxmoxDirectConnectionsCardSource from '../ProxmoxDirectConnectionsCard.tsx?raw'; +import proxmoxDiscoveryResultsCardSource from '../ProxmoxDiscoveryResultsCard.tsx?raw'; +import proxmoxSettingsPanelSource from '../ProxmoxSettingsPanel.tsx?raw'; import securityOverviewPanelSource from '../SecurityOverviewPanel.tsx?raw'; import securityAuthPanelSource from '../SecurityAuthPanel.tsx?raw'; import ssoProvidersPanelSource from '../SSOProvidersPanel.tsx?raw'; @@ -93,6 +97,9 @@ const extractedModules = [ '../useSSOProvidersState.ts', '../ssoProvidersModel.ts', '../ProxmoxSettingsPanel.tsx', + '../ProxmoxDeleteNodeDialog.tsx', + '../ProxmoxDirectConnectionsCard.tsx', + '../ProxmoxDiscoveryResultsCard.tsx', '../SettingsDialogs.tsx', '../SettingsPageShell.tsx', '../useDiscoverySettingsState.ts', @@ -305,6 +312,20 @@ describe('Settings architecture guardrails', () => { expect(settingsLineCount).toBeLessThanOrEqual(maxSettingsLines); }); + it('keeps the direct Proxmox settings workspace split into section owners', () => { + expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDirectConnectionsCard'); + expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDiscoveryResultsCard'); + expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDeleteNodeDialog'); + expect(proxmoxSettingsPanelSource).not.toContain('No discovery matches for this Proxmox type yet.'); + expect(proxmoxSettingsPanelSource).not.toContain('What happens next'); + expect(proxmoxDirectConnectionsCardSource).toContain('getSettingsConfigurationLoadingState'); + expect(proxmoxDiscoveryResultsCardSource).toContain('Discovery issues:'); + expect(proxmoxDiscoveryResultsCardSource).toContain( + 'No discovery matches for this Proxmox type yet. You can still add a direct', + ); + expect(proxmoxDeleteNodeDialogSource).toContain('What happens next'); + }); + it('uses lazy() imports for panel components in settingsPanelRegistry', async () => { const source = (await import('../settingsPanelRegistry.ts?raw')).default; diff --git a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts index dcc175ef57..c6dd834ea7 100644 --- a/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts +++ b/frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts @@ -273,6 +273,7 @@ import configuredNodeStatusPresentationSource from '@/utils/configuredNodeStatus import rbacPresentationSource from '@/utils/rbacPresentation.ts?raw'; import upgradePresentationSource from '@/utils/upgradePresentation.ts?raw'; import agentProfilesPanelSource from '@/components/Settings/AgentProfilesPanel.tsx?raw'; +import agentProfilesPanelStateSource from '@/components/Settings/useAgentProfilesPanelState.ts?raw'; import agentProfilesPresentationSource from '@/utils/agentProfilesPresentation.ts?raw'; import agentProfileSuggestionPresentationSource from '@/utils/agentProfileSuggestionPresentation.ts?raw'; import organizationAccessPanelSource from '@/components/Settings/OrganizationAccessPanel.tsx?raw'; @@ -799,7 +800,8 @@ describe('frontend resource type boundaries', () => { expect(reportingPanelSource).not.toContain('window.URL.createObjectURL'); expect(rolesPanelSource).toContain('@/utils/upgradePresentation'); expect(userAssignmentsPanelSource).toContain('@/utils/upgradePresentation'); - expect(agentProfilesPanelSource).toContain('@/utils/upgradePresentation'); + expect(agentProfilesPanelSource).toContain('./useAgentProfilesPanelState'); + expect(agentProfilesPanelStateSource).toContain('@/utils/upgradePresentation'); expect(agentProfilesPanelSource).toContain('@/utils/agentProfilesPresentation'); expect(agentProfilesPanelSource).toContain('getAgentProfilesEmptyState'); expect(agentProfilesPanelSource).toContain('getAgentProfileAssignmentsEmptyState'); diff --git a/scripts/release_control/canonical_completion_guard_test.py b/scripts/release_control/canonical_completion_guard_test.py index 7f21cb5200..4073332657 100644 --- a/scripts/release_control/canonical_completion_guard_test.py +++ b/scripts/release_control/canonical_completion_guard_test.py @@ -1838,6 +1838,48 @@ class CanonicalCompletionGuardTest(unittest.TestCase): ], ) + def _assert_direct_proxmox_workspace_change_requires_agent_lifecycle( + self, touched_path: str + ) -> None: + required = infer_impacted_subsystems([touched_path]) + self.assertEqual(set(required), {"agent-lifecycle"}) + + lifecycle = required["agent-lifecycle"] + self.assertEqual( + lifecycle["contract"], + "docs/release-control/v6/internal/subsystems/agent-lifecycle.md", + ) + self.assertEqual(lifecycle["touched_runtime_files"], [touched_path]) + self.assertEqual( + lifecycle["verification_requirements"], + [ + { + "id": "direct-proxmox-workspace-surface", + "label": "direct Proxmox workspace lifecycle proof", + "touched_runtime_files": [touched_path], + "allow_same_subsystem_tests": False, + "test_prefixes": [], + "exact_files": [ + "frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx", + "frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx", + "frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts", + "frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts", + "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts", + ], + } + ], + ) + + def test_proxmox_settings_panel_change_requires_agent_lifecycle(self): + self._assert_direct_proxmox_workspace_change_requires_agent_lifecycle( + "frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx" + ) + + def test_infrastructure_settings_state_change_requires_agent_lifecycle(self): + self._assert_direct_proxmox_workspace_change_requires_agent_lifecycle( + "frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts" + ) + def test_agent_install_backend_change_requires_lifecycle_and_api_contracts(self): required = infer_impacted_subsystems(["internal/api/agent_install_command_shared.go"]) self.assertEqual(set(required), {"agent-lifecycle", "api-contracts"}) diff --git a/scripts/release_control/subsystem_lookup_test.py b/scripts/release_control/subsystem_lookup_test.py index c91d5d2e24..4114766f1d 100644 --- a/scripts/release_control/subsystem_lookup_test.py +++ b/scripts/release_control/subsystem_lookup_test.py @@ -1219,6 +1219,40 @@ class SubsystemLookupTest(unittest.TestCase): ], ) + def test_lookup_paths_assigns_proxmox_settings_panel_to_agent_lifecycle(self) -> None: + result = lookup_paths(["frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx"]) + self.assertEqual(result["unowned_runtime_files"], []) + self.assertEqual( + {item["subsystem"] for item in result["impacted_subsystems"]}, + {"agent-lifecycle"}, + ) + file_entry = result["files"][0] + self.assertEqual(file_entry["classification"], "runtime") + self.assertEqual( + {match["subsystem"] for match in file_entry["matches"]}, + {"agent-lifecycle"}, + ) + match = file_entry["matches"][0] + self.assertEqual( + match["contract"], + "docs/release-control/v6/internal/subsystems/agent-lifecycle.md", + ) + self.assertEqual(match["lane_context"]["lane_id"], "L16") + self.assertEqual( + match["verification_requirement"]["id"], + "direct-proxmox-workspace-surface", + ) + self.assertEqual( + match["verification_requirement"]["exact_files"], + [ + "frontend-modern/src/components/Settings/__tests__/InfrastructureWorkspace.test.tsx", + "frontend-modern/src/components/Settings/__tests__/UnifiedAgents.test.tsx", + "frontend-modern/src/components/Settings/__tests__/monitoredSystemModelGuardrails.test.ts", + "frontend-modern/src/components/Settings/__tests__/settingsArchitecture.test.ts", + "frontend-modern/src/utils/__tests__/frontendResourceTypeBoundaries.test.ts", + ], + ) + def test_lookup_paths_reports_agent_install_backend_as_shared_boundary(self) -> None: result = lookup_paths(["internal/api/agent_install_command_shared.go"]) self.assertEqual(result["unowned_runtime_files"], [])