Extract Proxmox settings panel owners

This commit is contained in:
rcourtman
2026-03-21 00:40:18 +00:00
parent 446677f636
commit 0986f46fab
11 changed files with 383 additions and 179 deletions
@@ -40,22 +40,25 @@ management, and fleet control surfaces.
16. `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`
17. `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`
18. `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx`
19. `frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx`
20. `frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx`
21. `frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx`
22. `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`
23. `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`
24. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`
25. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`
26. `frontend-modern/src/components/Settings/UnifiedAgents.tsx`
27. `frontend-modern/src/components/Settings/NodeModal.tsx`
28. `frontend-modern/src/components/Settings/nodeModalModel.ts`
29. `frontend-modern/src/components/Settings/useNodeModalState.ts`
30. `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`
31. `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`
32. `frontend-modern/src/utils/agentProfilesPresentation.ts`
33. `frontend-modern/src/utils/agentInstallCommand.ts`
34. `frontend-modern/src/api/nodes.ts`
19. `frontend-modern/src/components/Settings/ProxmoxConfiguredNodesTable.tsx`
20. `frontend-modern/src/components/Settings/ProxmoxDirectConnectionsCard.tsx`
21. `frontend-modern/src/components/Settings/ProxmoxDiscoveryResultsCard.tsx`
22. `frontend-modern/src/components/Settings/ProxmoxDeleteNodeDialog.tsx`
23. `frontend-modern/src/components/Settings/ProxmoxNodeModalStack.tsx`
24. `frontend-modern/src/components/Settings/ConfiguredNodeTables.tsx`
25. `frontend-modern/src/components/Settings/SettingsSectionNav.tsx`
26. `frontend-modern/src/components/Settings/useInfrastructureOperationsState.tsx`
27. `frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts`
28. `frontend-modern/src/components/Settings/useProxmoxSettingsPanelState.ts`
29. `frontend-modern/src/components/Settings/UnifiedAgents.tsx`
30. `frontend-modern/src/components/Settings/NodeModal.tsx`
31. `frontend-modern/src/components/Settings/nodeModalModel.ts`
32. `frontend-modern/src/components/Settings/useNodeModalState.ts`
33. `frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx`
34. `frontend-modern/src/components/Infrastructure/deploy/ResultsStep.tsx`
35. `frontend-modern/src/utils/agentProfilesPresentation.ts`
36. `frontend-modern/src/utils/agentInstallCommand.ts`
37. `frontend-modern/src/api/nodes.ts`
## Shared Boundaries
@@ -82,7 +85,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, the direct Proxmox workspace shell, route model, reporting summary owner, 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/InfrastructureDirectConnectionsSummaryCard.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`, `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`.
6. Add or change profile management, the extracted agent profiles runtime owner, the pure unified-agent inventory/install model, the direct Proxmox workspace shell, route model, reporting summary owner, local panel-state owner, 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/InfrastructureDirectConnectionsSummaryCard.tsx`, `frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx`, `frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts`, `frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx`, `frontend-modern/src/components/Settings/ProxmoxConfiguredNodesTable.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/ProxmoxNodeModalStack.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/useProxmoxSettingsPanelState.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
@@ -154,10 +157,12 @@ lifecycle boundary: `InfrastructureWorkspace.tsx`,
`infrastructureWorkspaceModel.ts`,
`InfrastructureInstallPanel.tsx`, `InfrastructureReportingPanel.tsx`,
`InfrastructureDirectConnectionsSummaryCard.tsx`,
`ProxmoxSettingsPanel.tsx`, `ProxmoxDirectConnectionsCard.tsx`,
`ProxmoxDiscoveryResultsCard.tsx`, `ProxmoxDeleteNodeDialog.tsx`,
`ConfiguredNodeTables.tsx`, `SettingsSectionNav.tsx`, and
`useInfrastructureSettingsState.ts` own the fallback install/direct/reporting
`ProxmoxSettingsPanel.tsx`, `ProxmoxConfiguredNodesTable.tsx`,
`ProxmoxDirectConnectionsCard.tsx`, `ProxmoxDiscoveryResultsCard.tsx`,
`ProxmoxDeleteNodeDialog.tsx`, `ProxmoxNodeModalStack.tsx`,
`ConfiguredNodeTables.tsx`, `SettingsSectionNav.tsx`,
`useInfrastructureSettingsState.ts`, and
`useProxmoxSettingsPanelState.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:
@@ -378,9 +378,11 @@
"frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts",
"frontend-modern/src/components/Settings/NodeModal.tsx",
"frontend-modern/src/components/Settings/nodeModalModel.ts",
"frontend-modern/src/components/Settings/ProxmoxConfiguredNodesTable.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/ProxmoxNodeModalStack.tsx",
"frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx",
"frontend-modern/src/components/Settings/SettingsSectionNav.tsx",
"frontend-modern/src/components/Settings/UnifiedAgents.tsx",
@@ -388,6 +390,7 @@
"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/Settings/useProxmoxSettingsPanelState.ts",
"frontend-modern/src/components/SetupWizard/SetupCompletionPanel.tsx",
"frontend-modern/src/utils/agentInstallCommand.ts",
"frontend-modern/src/utils/agentProfilesPresentation.ts",
@@ -611,12 +614,15 @@
"frontend-modern/src/components/Settings/InfrastructureReportingPanel.tsx",
"frontend-modern/src/components/Settings/InfrastructureWorkspace.tsx",
"frontend-modern/src/components/Settings/infrastructureWorkspaceModel.ts",
"frontend-modern/src/components/Settings/ProxmoxConfiguredNodesTable.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/ProxmoxNodeModalStack.tsx",
"frontend-modern/src/components/Settings/ProxmoxSettingsPanel.tsx",
"frontend-modern/src/components/Settings/SettingsSectionNav.tsx",
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts"
"frontend-modern/src/components/Settings/useInfrastructureSettingsState.ts",
"frontend-modern/src/components/Settings/useProxmoxSettingsPanelState.ts"
],
"allow_same_subsystem_tests": false,
"test_prefixes": [],
@@ -0,0 +1,64 @@
import type { Component } from 'solid-js';
import type { Resource } from '@/types/resource';
import type { PBSInstance, PMGInstance } from '@/types/api';
import type { NodeConfigWithStatus } from '@/types/nodes';
import { PveNodesTable, PbsNodesTable, PmgNodesTable } from './ConfiguredNodeTables';
import type { NodeType } from './useInfrastructureSettingsState';
interface ProxmoxConfiguredNodesTableProps {
activeAgent: NodeType;
pveNodes: NodeConfigWithStatus[];
pbsNodes: NodeConfigWithStatus[];
pmgNodes: NodeConfigWithStatus[];
agentStateResources: Resource[];
pbsInstances: PBSInstance[];
pmgInstances: PMGInstance[];
temperatureMonitoringEnabled: boolean;
onTestConnection: (nodeId: string) => void;
onEditNode: (type: NodeType, node: NodeConfigWithStatus) => void;
onDeleteNode: (node: NodeConfigWithStatus) => void;
onRefreshClusterNodes: (nodeId: string) => Promise<void>;
}
export const ProxmoxConfiguredNodesTable: Component<ProxmoxConfiguredNodesTableProps> = (
props,
) => {
switch (props.activeAgent) {
case 'pve':
return (
<PveNodesTable
nodes={props.pveNodes}
stateNodes={props.agentStateResources}
globalTemperatureMonitoringEnabled={props.temperatureMonitoringEnabled}
onTestConnection={props.onTestConnection}
onEdit={(node) => props.onEditNode('pve', node)}
onDelete={props.onDeleteNode}
onRefreshCluster={props.onRefreshClusterNodes}
/>
);
case 'pbs':
return (
<PbsNodesTable
nodes={props.pbsNodes}
statePbs={props.pbsInstances}
globalTemperatureMonitoringEnabled={props.temperatureMonitoringEnabled}
onTestConnection={props.onTestConnection}
onEdit={(node) => props.onEditNode('pbs', node)}
onDelete={props.onDeleteNode}
/>
);
case 'pmg':
return (
<PmgNodesTable
nodes={props.pmgNodes}
statePmg={props.pmgInstances}
globalTemperatureMonitoringEnabled={props.temperatureMonitoringEnabled}
onTestConnection={props.onTestConnection}
onEdit={(node) => props.onEditNode('pmg', node)}
onDelete={props.onDeleteNode}
/>
);
}
};
export default ProxmoxConfiguredNodesTable;
@@ -0,0 +1,57 @@
import { Show, type Component } from 'solid-js';
import type { NodeConfig, NodeConfigWithStatus } from '@/types/nodes';
import type { SecurityStatus as SecurityStatusInfo } from '@/types/config';
import { NodeModal } from './NodeModal';
import type { NodeType } from './useInfrastructureSettingsState';
interface ProxmoxNodeModalStackProps {
modalResetKey: number;
prefillNode: Partial<NodeConfig> | null;
editingNodeType: NodeType | null;
editingNode: () => NodeConfigWithStatus | null;
isNodeModalVisible: (type: NodeType) => boolean;
securityStatus: SecurityStatusInfo | null;
resolveTemperatureMonitoringEnabled: (node?: NodeConfigWithStatus | null) => boolean;
temperatureMonitoringLocked: boolean;
savingTemperatureSetting: boolean;
handleNodeTemperatureMonitoringChange: (nodeId: string, enabled: boolean | null) => Promise<void>;
handleTemperatureMonitoringChange: (enabled: boolean) => Promise<void>;
saveNode: (nodeData: Partial<NodeConfig>) => Promise<void>;
onClose: () => void;
}
const PROXMOX_NODE_TYPES: readonly NodeType[] = ['pve', 'pbs', 'pmg'];
export const ProxmoxNodeModalStack: Component<ProxmoxNodeModalStackProps> = (props) => {
return (
<>
{PROXMOX_NODE_TYPES.map((type) => (
<Show when={props.isNodeModalVisible(type)}>
<NodeModal
isOpen={true}
resetKey={props.modalResetKey}
onClose={props.onClose}
nodeType={type}
editingNode={props.editingNodeType === type ? props.editingNode() ?? undefined : undefined}
prefillNode={props.prefillNode?.type === type ? props.prefillNode ?? undefined : undefined}
securityStatus={props.securityStatus ?? undefined}
temperatureMonitoringEnabled={props.resolveTemperatureMonitoringEnabled(
props.editingNodeType === type ? props.editingNode() : null,
)}
temperatureMonitoringLocked={props.temperatureMonitoringLocked}
savingTemperatureSetting={props.savingTemperatureSetting}
onToggleTemperatureMonitoring={
props.editingNode()?.id
? (enabled: boolean) =>
props.handleNodeTemperatureMonitoringChange(props.editingNode()!.id, enabled)
: props.handleTemperatureMonitoringChange
}
onSave={props.saveNode}
/>
</Show>
))}
</>
);
};
export default ProxmoxNodeModalStack;
@@ -1,22 +1,18 @@
import { Component, Accessor, Setter, Show, createMemo, createSignal } from 'solid-js';
import { Component, Accessor, Setter, Show } from 'solid-js';
import { useNavigate } from '@solidjs/router';
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 { CalloutCard } from '@/components/shared/CalloutCard';
import type { ToggleChangeEvent } from '@/components/shared/Toggle';
import { NodeModal } from './NodeModal';
import { PveNodesTable, PbsNodesTable, PmgNodesTable } from './ConfiguredNodeTables';
import { ProxmoxConfiguredNodesTable } from './ProxmoxConfiguredNodesTable';
import { ProxmoxDeleteNodeDialog } from './ProxmoxDeleteNodeDialog';
import { ProxmoxDirectConnectionsCard } from './ProxmoxDirectConnectionsCard';
import { ProxmoxDiscoveryResultsCard } from './ProxmoxDiscoveryResultsCard';
import { ProxmoxNodeModalStack } from './ProxmoxNodeModalStack';
import { SettingsSectionNav } from './SettingsSectionNav';
import {
buildProxmoxDiscoveryPrefillNode,
getProxmoxVariantPresentation,
} from '@/utils/proxmoxSettingsPresentation';
import { useProxmoxSettingsPanelState } from './useProxmoxSettingsPanelState';
import type {
DiscoveredServer,
DiscoveryScanStatus,
@@ -75,145 +71,16 @@ export interface ProxmoxSettingsPanelProps {
export const ProxmoxSettingsPanel: Component<ProxmoxSettingsPanelProps> = (props) => {
const navigate = useNavigate();
const [prefillNode, setPrefillNode] = createSignal<Partial<NodeConfig> | null>(null);
const activeAgent = () => props.selectedAgent();
const activeConfig = createMemo(() => getProxmoxVariantPresentation(activeAgent()));
const activeDiscoveredNodes = createMemo(() =>
props.discoveredNodes().filter((node) => node.type === activeAgent()),
);
const activeConfiguredNodes = createMemo(() => {
switch (activeAgent()) {
case 'pve':
return props.pveNodes();
case 'pbs':
return props.pbsNodes();
case 'pmg':
return props.pmgNodes();
}
});
const hasDiscoveryTimeouts = () =>
props.discoveryMode() === 'auto' &&
(props.discoveryScanStatus().errors || []).some((error) => /timed out|timeout/i.test(error));
const openCreateNode = (type: NodeType) => {
setPrefillNode(null);
props.setEditingNode(null);
props.setCurrentNodeType(type);
props.setModalResetKey((previous) => previous + 1);
props.setShowNodeModal(true);
};
const openEditNode = (type: NodeType, node: NodeConfigWithStatus) => {
setPrefillNode(null);
props.setEditingNode(node);
props.setCurrentNodeType(type);
props.setShowNodeModal(true);
};
const openDiscoveredNode = (server: DiscoveredServer) => {
setPrefillNode(buildProxmoxDiscoveryPrefillNode(server));
props.setEditingNode(null);
props.setCurrentNodeType(server.type);
props.setModalResetKey((previous) => previous + 1);
props.setShowNodeModal(true);
};
const closeNodeModal = () => {
setPrefillNode(null);
props.setShowNodeModal(false);
props.setEditingNode(null);
props.setModalResetKey((previous) => previous + 1);
};
const handleRefreshDiscovery = async () => {
notificationStore.info('Refreshing discovery...', 2000);
try {
await props.triggerDiscoveryScan({ quiet: true });
} finally {
await props.loadDiscoveredNodes();
}
};
const state = useProxmoxSettingsPanelState(props);
const handleDiscoveryToggle = async (event: ToggleChangeEvent) => {
if (props.envOverrides().discoveryEnabled || props.savingDiscoverySettings()) {
const nextValue = await state.handleDiscoveryToggle(event.currentTarget.checked);
if (nextValue !== event.currentTarget.checked) {
event.preventDefault();
return;
}
const success = await props.handleDiscoveryEnabledChange(event.currentTarget.checked);
if (!success) {
event.currentTarget.checked = props.discoveryEnabled();
event.currentTarget.checked = nextValue;
}
};
const renderConfiguredTable = () => {
switch (activeAgent()) {
case 'pve':
return (
<PveNodesTable
nodes={props.pveNodes()}
stateNodes={props.agentStateResources()}
globalTemperatureMonitoringEnabled={props.temperatureMonitoringEnabled()}
onTestConnection={props.testNodeConnection}
onEdit={(node) => openEditNode('pve', node)}
onDelete={props.requestDeleteNode}
onRefreshCluster={props.refreshClusterNodes}
/>
);
case 'pbs':
return (
<PbsNodesTable
nodes={props.pbsNodes()}
statePbs={props.pbsInstances()}
globalTemperatureMonitoringEnabled={props.temperatureMonitoringEnabled()}
onTestConnection={props.testNodeConnection}
onEdit={(node) => openEditNode('pbs', node)}
onDelete={props.requestDeleteNode}
/>
);
case 'pmg':
return (
<PmgNodesTable
nodes={props.pmgNodes()}
statePmg={props.pmgInstances()}
globalTemperatureMonitoringEnabled={props.temperatureMonitoringEnabled()}
onTestConnection={props.testNodeConnection}
onEdit={(node) => openEditNode('pmg', node)}
onDelete={props.requestDeleteNode}
/>
);
}
};
const renderNodeModal = (type: NodeType) => (
<Show when={props.isNodeModalVisible(type)}>
<NodeModal
isOpen={true}
resetKey={props.modalResetKey()}
onClose={closeNodeModal}
nodeType={type}
editingNode={
props.editingNode()?.type === type ? (props.editingNode() ?? undefined) : undefined
}
prefillNode={prefillNode()?.type === type ? (prefillNode() ?? undefined) : undefined}
securityStatus={props.securityStatus() ?? undefined}
temperatureMonitoringEnabled={props.resolveTemperatureMonitoringEnabled(
props.editingNode()?.type === type ? props.editingNode() : null,
)}
temperatureMonitoringLocked={props.temperatureMonitoringLocked()}
savingTemperatureSetting={props.savingTemperatureSetting()}
onToggleTemperatureMonitoring={
props.editingNode()?.id
? (enabled: boolean) =>
props.handleNodeTemperatureMonitoringChange(props.editingNode()!.id, enabled)
: props.handleTemperatureMonitoringChange
}
onSave={props.saveNode}
/>
</Show>
);
return (
<>
<SettingsSectionNav
@@ -261,27 +128,42 @@ export const ProxmoxSettingsPanel: Component<ProxmoxSettingsPanelProps> = (props
<div class="space-y-6 mt-6">
<div class="space-y-4">
<ProxmoxDirectConnectionsCard
activeAgent={activeAgent()}
activeConfig={activeConfig()}
activeConfiguredNodes={activeConfiguredNodes()}
activeDiscoveredNodes={activeDiscoveredNodes()}
configuredTable={renderConfiguredTable()}
activeAgent={state.activeAgent()}
activeConfig={state.activeConfig()}
activeConfiguredNodes={state.activeConfiguredNodes()}
activeDiscoveredNodes={state.activeDiscoveredNodes()}
configuredTable={
<ProxmoxConfiguredNodesTable
activeAgent={state.activeAgent()}
pveNodes={props.pveNodes()}
pbsNodes={props.pbsNodes()}
pmgNodes={props.pmgNodes()}
agentStateResources={props.agentStateResources()}
pbsInstances={props.pbsInstances()}
pmgInstances={props.pmgInstances()}
temperatureMonitoringEnabled={props.temperatureMonitoringEnabled()}
onTestConnection={props.testNodeConnection}
onEditNode={state.openEditNode}
onDeleteNode={props.requestDeleteNode}
onRefreshClusterNodes={props.refreshClusterNodes}
/>
}
discoveryEnabled={props.discoveryEnabled()}
envOverrides={props.envOverrides()}
initialLoadComplete={props.initialLoadComplete()}
onDiscoveryToggle={handleDiscoveryToggle}
onOpenCreateNode={openCreateNode}
onRefreshDiscovery={handleRefreshDiscovery}
onOpenCreateNode={state.openCreateNode}
onRefreshDiscovery={state.handleRefreshDiscovery}
savingDiscoverySettings={props.savingDiscoverySettings()}
/>
<Show when={props.discoveryEnabled()}>
<ProxmoxDiscoveryResultsCard
activeConfig={activeConfig()}
activeDiscoveredNodes={activeDiscoveredNodes()}
activeConfig={state.activeConfig()}
activeDiscoveredNodes={state.activeDiscoveredNodes()}
discoveryScanStatus={props.discoveryScanStatus()}
hasDiscoveryTimeouts={hasDiscoveryTimeouts()}
onOpenDiscoveredNode={openDiscoveredNode}
hasDiscoveryTimeouts={state.hasDiscoveryTimeouts()}
onOpenDiscoveredNode={state.openDiscoveredNode}
/>
</Show>
</div>
@@ -299,9 +181,21 @@ export const ProxmoxSettingsPanel: Component<ProxmoxSettingsPanelProps> = (props
/>
</Show>
{renderNodeModal('pve')}
{renderNodeModal('pbs')}
{renderNodeModal('pmg')}
<ProxmoxNodeModalStack
modalResetKey={props.modalResetKey()}
prefillNode={state.prefillNode()}
editingNodeType={props.editingNode()?.type ?? null}
editingNode={props.editingNode}
isNodeModalVisible={props.isNodeModalVisible}
securityStatus={props.securityStatus()}
resolveTemperatureMonitoringEnabled={props.resolveTemperatureMonitoringEnabled}
temperatureMonitoringLocked={props.temperatureMonitoringLocked()}
savingTemperatureSetting={props.savingTemperatureSetting()}
handleNodeTemperatureMonitoringChange={props.handleNodeTemperatureMonitoringChange}
handleTemperatureMonitoringChange={props.handleTemperatureMonitoringChange}
saveNode={props.saveNode}
onClose={state.closeNodeModal}
/>
</>
);
};
@@ -113,9 +113,12 @@ import unifiedAgentInventoryPresentationSource from '@/utils/unifiedAgentInvento
import relayPresentationSource from '@/utils/relayPresentation.ts?raw';
import relaySettingsPanelSource from '../RelaySettingsPanel.tsx?raw';
import proxmoxDeleteNodeDialogSource from '../ProxmoxDeleteNodeDialog.tsx?raw';
import proxmoxConfiguredNodesTableSource from '../ProxmoxConfiguredNodesTable.tsx?raw';
import proxmoxDirectConnectionsCardSource from '../ProxmoxDirectConnectionsCard.tsx?raw';
import proxmoxDiscoveryResultsCardSource from '../ProxmoxDiscoveryResultsCard.tsx?raw';
import proxmoxNodeModalStackSource from '../ProxmoxNodeModalStack.tsx?raw';
import proxmoxSettingsPanelSource from '../ProxmoxSettingsPanel.tsx?raw';
import proxmoxSettingsPanelStateSource from '../useProxmoxSettingsPanelState.ts?raw';
import relayOnboardingCardSource from '@/components/Dashboard/RelayOnboardingCard.tsx?raw';
import generalSettingsPanelSource from '../GeneralSettingsPanel.tsx?raw';
import networkSettingsPanelSource from '../NetworkSettingsPanel.tsx?raw';
@@ -305,14 +308,23 @@ describe('monitored-system model guardrails', () => {
expect(relaySettingsPanelSource).not.toContain('Loading configuration...');
expect(proxmoxSettingsPanelSource).toContain('CalloutCard');
expect(proxmoxSettingsPanelSource).not.toContain('Loading configuration...');
expect(proxmoxSettingsPanelSource).toContain('./useProxmoxSettingsPanelState');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxConfiguredNodesTable');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDirectConnectionsCard');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDiscoveryResultsCard');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDeleteNodeDialog');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxNodeModalStack');
expect(proxmoxSettingsPanelSource).not.toContain('setPrefillNode(');
expect(proxmoxSettingsPanelSource).not.toContain('const renderConfiguredTable = () =>');
expect(proxmoxSettingsPanelSource).not.toContain('const renderNodeModal = (type: NodeType)');
expect(proxmoxSettingsPanelSource).not.toContain('No discovery matches for this Proxmox type yet.');
expect(proxmoxSettingsPanelSource).not.toContain('What happens next');
expect(proxmoxConfiguredNodesTableSource).toContain('PveNodesTable');
expect(proxmoxDirectConnectionsCardSource).toContain('getSettingsConfigurationLoadingState');
expect(proxmoxDiscoveryResultsCardSource).toContain('formatRelativeTime');
expect(proxmoxDeleteNodeDialogSource).toContain('SectionHeader');
expect(proxmoxNodeModalStackSource).toContain('<NodeModal');
expect(proxmoxSettingsPanelStateSource).toContain('buildProxmoxDiscoveryPrefillNode');
expect(relayOnboardingCardSource).toContain('@/utils/relayPresentation');
expect(relayOnboardingCardSource).toContain('RELAY_ONBOARDING_TITLE');
expect(relayOnboardingCardSource).toContain('RELAY_ONBOARDING_DESCRIPTION');
@@ -46,9 +46,12 @@ 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 proxmoxConfiguredNodesTableSource from '../ProxmoxConfiguredNodesTable.tsx?raw';
import proxmoxDirectConnectionsCardSource from '../ProxmoxDirectConnectionsCard.tsx?raw';
import proxmoxDiscoveryResultsCardSource from '../ProxmoxDiscoveryResultsCard.tsx?raw';
import proxmoxNodeModalStackSource from '../ProxmoxNodeModalStack.tsx?raw';
import proxmoxSettingsPanelSource from '../ProxmoxSettingsPanel.tsx?raw';
import proxmoxSettingsPanelStateSource from '../useProxmoxSettingsPanelState.ts?raw';
import securityOverviewPanelSource from '../SecurityOverviewPanel.tsx?raw';
import securityAuthPanelSource from '../SecurityAuthPanel.tsx?raw';
import ssoProvidersPanelSource from '../SSOProvidersPanel.tsx?raw';
@@ -101,9 +104,12 @@ const extractedModules = [
'../useSSOProvidersState.ts',
'../ssoProvidersModel.ts',
'../ProxmoxSettingsPanel.tsx',
'../ProxmoxConfiguredNodesTable.tsx',
'../ProxmoxDeleteNodeDialog.tsx',
'../ProxmoxDirectConnectionsCard.tsx',
'../ProxmoxDiscoveryResultsCard.tsx',
'../ProxmoxNodeModalStack.tsx',
'../useProxmoxSettingsPanelState.ts',
'../SettingsDialogs.tsx',
'../SettingsPageShell.tsx',
'../useDiscoverySettingsState.ts',
@@ -317,17 +323,29 @@ describe('Settings architecture guardrails', () => {
});
it('keeps the direct Proxmox settings workspace split into section owners', () => {
expect(proxmoxSettingsPanelSource).toContain('./useProxmoxSettingsPanelState');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxConfiguredNodesTable');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDirectConnectionsCard');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDiscoveryResultsCard');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxDeleteNodeDialog');
expect(proxmoxSettingsPanelSource).toContain('./ProxmoxNodeModalStack');
expect(proxmoxSettingsPanelSource).not.toContain('const renderConfiguredTable = () =>');
expect(proxmoxSettingsPanelSource).not.toContain('const renderNodeModal = (type: NodeType)');
expect(proxmoxSettingsPanelSource).not.toContain('No discovery matches for this Proxmox type yet.');
expect(proxmoxSettingsPanelSource).not.toContain('What happens next');
expect(proxmoxConfiguredNodesTableSource).toContain('PveNodesTable');
expect(proxmoxConfiguredNodesTableSource).toContain('PbsNodesTable');
expect(proxmoxConfiguredNodesTableSource).toContain('PmgNodesTable');
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');
expect(proxmoxNodeModalStackSource).toContain('PROXMOX_NODE_TYPES');
expect(proxmoxNodeModalStackSource).toContain('<NodeModal');
expect(proxmoxSettingsPanelStateSource).toContain('export function useProxmoxSettingsPanelState');
expect(proxmoxSettingsPanelStateSource).toContain("notificationStore.info('Refreshing discovery...'");
});
it('uses lazy() imports for panel components in settingsPanelRegistry', async () => {
@@ -0,0 +1,95 @@
import { createMemo, createSignal } from 'solid-js';
import { notificationStore } from '@/stores/notifications';
import {
buildProxmoxDiscoveryPrefillNode,
getProxmoxVariantPresentation,
} from '@/utils/proxmoxSettingsPresentation';
import type { NodeConfig, NodeConfigWithStatus } from '@/types/nodes';
import type { DiscoveredServer, NodeType } from './useInfrastructureSettingsState';
import type { ProxmoxSettingsPanelProps } from './ProxmoxSettingsPanel';
export function useProxmoxSettingsPanelState(props: ProxmoxSettingsPanelProps) {
const [prefillNode, setPrefillNode] = createSignal<Partial<NodeConfig> | null>(null);
const activeAgent = () => props.selectedAgent();
const activeConfig = createMemo(() => getProxmoxVariantPresentation(activeAgent()));
const activeDiscoveredNodes = createMemo(() =>
props.discoveredNodes().filter((node) => node.type === activeAgent()),
);
const activeConfiguredNodes = createMemo(() => {
switch (activeAgent()) {
case 'pve':
return props.pveNodes();
case 'pbs':
return props.pbsNodes();
case 'pmg':
return props.pmgNodes();
}
});
const hasDiscoveryTimeouts = () =>
props.discoveryMode() === 'auto' &&
(props.discoveryScanStatus().errors || []).some((error) => /timed out|timeout/i.test(error));
const openCreateNode = (type: NodeType) => {
setPrefillNode(null);
props.setEditingNode(null);
props.setCurrentNodeType(type);
props.setModalResetKey((previous) => previous + 1);
props.setShowNodeModal(true);
};
const openEditNode = (type: NodeType, node: NodeConfigWithStatus) => {
setPrefillNode(null);
props.setEditingNode(node);
props.setCurrentNodeType(type);
props.setShowNodeModal(true);
};
const openDiscoveredNode = (server: DiscoveredServer) => {
setPrefillNode(buildProxmoxDiscoveryPrefillNode(server));
props.setEditingNode(null);
props.setCurrentNodeType(server.type);
props.setModalResetKey((previous) => previous + 1);
props.setShowNodeModal(true);
};
const closeNodeModal = () => {
setPrefillNode(null);
props.setShowNodeModal(false);
props.setEditingNode(null);
props.setModalResetKey((previous) => previous + 1);
};
const handleRefreshDiscovery = async () => {
notificationStore.info('Refreshing discovery...', 2000);
try {
await props.triggerDiscoveryScan({ quiet: true });
} finally {
await props.loadDiscoveredNodes();
}
};
const handleDiscoveryToggle = async (enabled: boolean) => {
if (props.envOverrides().discoveryEnabled || props.savingDiscoverySettings()) {
return props.discoveryEnabled();
}
const success = await props.handleDiscoveryEnabledChange(enabled);
return success ? enabled : props.discoveryEnabled();
};
return {
activeAgent,
activeConfig,
activeConfiguredNodes,
activeDiscoveredNodes,
closeNodeModal,
handleDiscoveryToggle,
handleRefreshDiscovery,
hasDiscoveryTimeouts,
openCreateNode,
openDiscoveredNode,
openEditNode,
prefillNode,
};
}
@@ -198,6 +198,9 @@ import pmgThreatPresentationSource from '@/utils/pmgThreatPresentation.ts?raw';
import pmgQueuePresentationSource from '@/utils/pmgQueuePresentation.ts?raw';
import pmgServiceHealthBadgeSource from '@/components/PMG/ServiceHealthBadge.tsx?raw';
import proxmoxSettingsPanelSource from '@/components/Settings/ProxmoxSettingsPanel.tsx?raw';
import proxmoxConfiguredNodesTableSource from '@/components/Settings/ProxmoxConfiguredNodesTable.tsx?raw';
import proxmoxNodeModalStackSource from '@/components/Settings/ProxmoxNodeModalStack.tsx?raw';
import proxmoxSettingsPanelStateSource from '@/components/Settings/useProxmoxSettingsPanelState.ts?raw';
import infrastructureWorkspaceSource from '@/components/Settings/InfrastructureWorkspace.tsx?raw';
import infrastructureWorkspaceModelSource from '@/components/Settings/infrastructureWorkspaceModel.ts?raw';
import proxmoxSettingsPresentationSource from '@/utils/proxmoxSettingsPresentation.ts?raw';
@@ -2143,10 +2146,19 @@ describe('frontend resource type boundaries', () => {
expect(pmgThreatPresentationSource).toContain('export function getPMGThreatPresentation');
expect(pmgQueuePresentationSource).toContain('export function getPMGQueueTextClass');
expect(pmgServiceHealthBadgeSource).toContain('getServiceHealthPresentation');
expect(proxmoxSettingsPanelSource).toContain('getProxmoxVariantPresentation');
expect(proxmoxSettingsPanelSource).toContain('buildProxmoxDiscoveryPrefillNode');
expect(proxmoxSettingsPanelSource).toContain('./useProxmoxSettingsPanelState');
expect(proxmoxSettingsPanelSource).not.toContain('const VARIANT_CONFIG: Record<NodeType');
expect(proxmoxSettingsPanelSource).not.toContain('const buildDiscoveryPrefillNode =');
expect(proxmoxSettingsPanelSource).toContain('./useProxmoxSettingsPanelState');
expect(proxmoxSettingsPanelSource).not.toContain('const openCreateNode = (type: NodeType) =>');
expect(proxmoxSettingsPanelSource).not.toContain('const openDiscoveredNode = (server: DiscoveredServer) =>');
expect(proxmoxConfiguredNodesTableSource).toContain('PveNodesTable');
expect(proxmoxConfiguredNodesTableSource).toContain('PbsNodesTable');
expect(proxmoxConfiguredNodesTableSource).toContain('PmgNodesTable');
expect(proxmoxNodeModalStackSource).toContain('PROXMOX_NODE_TYPES');
expect(proxmoxSettingsPanelStateSource).toContain('export function useProxmoxSettingsPanelState');
expect(proxmoxSettingsPanelStateSource).toContain('getProxmoxVariantPresentation');
expect(proxmoxSettingsPanelStateSource).toContain('buildProxmoxDiscoveryPrefillNode');
expect(proxmoxSettingsPresentationSource).toContain(
'export const PROXMOX_VARIANT_PRESENTATION',
);
@@ -1890,6 +1890,11 @@ class CanonicalCompletionGuardTest(unittest.TestCase):
"frontend-modern/src/components/Settings/InfrastructureDirectConnectionsSummaryCard.tsx"
)
def test_proxmox_settings_panel_state_change_requires_agent_lifecycle(self):
self._assert_direct_proxmox_workspace_change_requires_agent_lifecycle(
"frontend-modern/src/components/Settings/useProxmoxSettingsPanelState.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"})
@@ -1329,6 +1329,42 @@ class SubsystemLookupTest(unittest.TestCase):
],
)
def test_lookup_paths_assigns_proxmox_settings_panel_state_to_agent_lifecycle(self) -> None:
result = lookup_paths(
["frontend-modern/src/components/Settings/useProxmoxSettingsPanelState.ts"]
)
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"], [])