Add per-node temperature monitoring and fix critical config update bug

This commit implements per-node temperature monitoring control and fixes a critical
bug where partial node updates were destroying existing configuration.

Backend changes:
- Add TemperatureMonitoringEnabled field (*bool) to PVEInstance, PBSInstance, and PMGInstance
- Update monitor.go to check per-node temperature setting with global fallback
- Convert all NodeConfigRequest boolean fields to *bool pointers
- Add nil checks in HandleUpdateNode to prevent overwriting unmodified fields
- Fix critical bug where partial updates zeroed out MonitorVMs, MonitorContainers, etc.
- Update NodeResponse, NodeFrontend, and StateSnapshot to include temperature setting
- Fix HandleAddNode and test connection handlers to use pointer-based boolean fields

Frontend changes:
- Add temperatureMonitoringEnabled to Node interface and config types
- Create per-node temperature monitoring toggle handler with optimistic updates
- Update NodeModal to wire up per-node temperature toggle
- Add isTemperatureMonitoringEnabled helper to check effective monitoring state
- Update ConfiguredNodeTables to show/hide temperature badge based on monitoring state
- Update NodeSummaryTable to conditionally show temperature column
- Pass globalTemperatureMonitoringEnabled prop through component tree

The critical bug fix ensures that when updating a single field (like temperature
monitoring), the backend only modifies that specific field instead of zeroing out
all other boolean configuration fields.
This commit is contained in:
rcourtman 2025-11-05 14:11:53 +00:00
parent e4e915c8a1
commit 27f2038dab
19 changed files with 655 additions and 280 deletions

View file

@ -1076,6 +1076,7 @@ const UnifiedBackups: Component = () => {
{/* Unified Node Selector */} {/* Unified Node Selector */}
<UnifiedNodeSelector <UnifiedNodeSelector
currentTab="backups" currentTab="backups"
globalTemperatureMonitoringEnabled={state.temperatureMonitoringEnabled}
onNodeSelect={(nodeId) => { onNodeSelect={(nodeId) => {
setSelectedNode(nodeId); setSelectedNode(nodeId);
}} }}

View file

@ -762,6 +762,7 @@ export function Dashboard(props: DashboardProps) {
{/* Unified Node Selector */} {/* Unified Node Selector */}
<UnifiedNodeSelector <UnifiedNodeSelector
currentTab="dashboard" currentTab="dashboard"
globalTemperatureMonitoringEnabled={ws.state.temperatureMonitoringEnabled}
onNodeSelect={handleNodeSelect} onNodeSelect={handleNodeSelect}
nodes={props.nodes} nodes={props.nodes}
filteredVms={filteredGuests().filter((g) => g.type === 'qemu')} filteredVms={filteredGuests().filter((g) => g.type === 'qemu')}

View file

@ -322,7 +322,7 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
actions={ actions={
<button <button
type="button" type="button"
onClick={() => navigate('/settings/containers')} onClick={() => navigate('/settings/docker')}
class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700" class="inline-flex items-center gap-2 rounded-lg bg-blue-600 px-4 py-2 text-sm font-medium text-white transition-colors hover:bg-blue-700"
> >
<span>Set up container agent</span> <span>Set up container agent</span>

View file

@ -10,6 +10,7 @@ type NodeConfigWithStatus = NodeConfig & {
interface PveNodesTableProps { interface PveNodesTableProps {
nodes: NodeConfigWithStatus[]; nodes: NodeConfigWithStatus[];
stateNodes: { instance: string; status?: string; connectionHealth?: string }[]; stateNodes: { instance: string; status?: string; connectionHealth?: string }[];
globalTemperatureMonitoringEnabled?: boolean;
onTestConnection: (nodeId: string) => void; onTestConnection: (nodeId: string) => void;
onEdit: (node: NodeConfigWithStatus) => void; onEdit: (node: NodeConfigWithStatus) => void;
onDelete: (node: NodeConfigWithStatus) => void; onDelete: (node: NodeConfigWithStatus) => void;
@ -45,6 +46,17 @@ const STATUS_META: Record<string, StatusMeta> = {
}, },
}; };
const isTemperatureMonitoringEnabled = (
node: NodeConfigWithStatus,
globalEnabled: boolean,
): boolean => {
// Check per-node setting first, fall back to global
if (node.temperatureMonitoringEnabled !== undefined && node.temperatureMonitoringEnabled !== null) {
return node.temperatureMonitoringEnabled;
}
return globalEnabled;
};
const resolvePveStatusMeta = ( const resolvePveStatusMeta = (
node: NodeConfigWithStatus, node: NodeConfigWithStatus,
stateNodes: PveNodesTableProps['stateNodes'], stateNodes: PveNodesTableProps['stateNodes'],
@ -199,7 +211,8 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
Physical Disks Physical Disks
</span> </span>
)} )}
{node.type === 'pve' && node.temperature?.available && ( {node.type === 'pve' &&
isTemperatureMonitoringEnabled(node, props.globalTemperatureMonitoringEnabled ?? true) && (
<span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded"> <span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded">
Temperature Temperature
</span> </span>
@ -261,6 +274,7 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
interface PbsNodesTableProps { interface PbsNodesTableProps {
nodes: NodeConfigWithStatus[]; nodes: NodeConfigWithStatus[];
statePbs: { name: string; status?: string; connectionHealth?: string }[]; statePbs: { name: string; status?: string; connectionHealth?: string }[];
globalTemperatureMonitoringEnabled?: boolean;
onTestConnection: (nodeId: string) => void; onTestConnection: (nodeId: string) => void;
onEdit: (node: NodeConfigWithStatus) => void; onEdit: (node: NodeConfigWithStatus) => void;
onDelete: (node: NodeConfigWithStatus) => void; onDelete: (node: NodeConfigWithStatus) => void;
@ -380,7 +394,8 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
Garbage Collection Garbage Collection
</span> </span>
)} )}
{node.type === 'pbs' && node.temperature?.available && ( {node.type === 'pbs' &&
isTemperatureMonitoringEnabled(node, props.globalTemperatureMonitoringEnabled ?? true) && (
<span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded"> <span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded">
Temperature Temperature
</span> </span>
@ -442,6 +457,7 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
interface PmgNodesTableProps { interface PmgNodesTableProps {
nodes: NodeConfigWithStatus[]; nodes: NodeConfigWithStatus[];
statePmg: { name: string; status?: string; connectionHealth?: string }[]; statePmg: { name: string; status?: string; connectionHealth?: string }[];
globalTemperatureMonitoringEnabled?: boolean;
onTestConnection: (nodeId: string) => void; onTestConnection: (nodeId: string) => void;
onEdit: (node: NodeConfigWithStatus) => void; onEdit: (node: NodeConfigWithStatus) => void;
onDelete: (node: NodeConfigWithStatus) => void; onDelete: (node: NodeConfigWithStatus) => void;

View file

@ -1746,7 +1746,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
<div> <div>
<p class="font-medium text-gray-900 dark:text-gray-100">Temperature monitoring</p> <p class="font-medium text-gray-900 dark:text-gray-100">Temperature monitoring</p>
<p class="mt-1 text-xs text-gray-500 dark:text-gray-400"> <p class="mt-1 text-xs text-gray-500 dark:text-gray-400">
Uses the Pulse sensors key or proxy to read CPU/NVMe temperatures for every node. Disable if you dont need temperature data or havent deployed the proxy yet. Uses the Pulse sensors key or proxy to read CPU/NVMe temperatures for this node. Disable if you don't need temperature data or haven't deployed the proxy yet.
</p> </p>
</div> </div>
<TogglePrimitive <TogglePrimitive
@ -1760,7 +1760,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
</div> </div>
<Show when={!enabled}> <Show when={!enabled}>
<p class="mt-3 rounded border border-blue-200 bg-blue-50 p-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-200"> <p class="mt-3 rounded border border-blue-200 bg-blue-50 p-2 text-xs text-blue-700 dark:border-blue-700 dark:bg-blue-900/20 dark:text-blue-200">
Pulse will skip all SSH temperature polling until you re-enable this toggle. Existing dashboard readings will stop refreshing. Pulse will skip SSH temperature polling for this node. Existing dashboard readings will stop refreshing.
</p> </p>
</Show> </Show>
<Show when={props.temperatureMonitoringLocked}> <Show when={props.temperatureMonitoringLocked}>

View file

@ -369,6 +369,7 @@ const Settings: Component<SettingsProps> = (props) => {
if (path.includes('/settings/proxmox')) return 'proxmox'; if (path.includes('/settings/proxmox')) return 'proxmox';
if (path.includes('/settings/agent-hub')) return 'proxmox'; if (path.includes('/settings/agent-hub')) return 'proxmox';
if (path.includes('/settings/docker')) return 'docker'; if (path.includes('/settings/docker')) return 'docker';
if (path.includes('/settings/containers')) return 'docker';
if ( if (
path.includes('/settings/hosts') || path.includes('/settings/hosts') ||
path.includes('/settings/host-agents') || path.includes('/settings/host-agents') ||
@ -488,6 +489,14 @@ const Settings: Component<SettingsProps> = (props) => {
return; return;
} }
if (path.startsWith('/settings/containers')) {
navigate(path.replace('/settings/containers', '/settings/docker'), {
replace: true,
scroll: false,
});
return;
}
if ( if (
path.startsWith('/settings/linuxServers') || path.startsWith('/settings/linuxServers') ||
path.startsWith('/settings/windowsServers') || path.startsWith('/settings/windowsServers') ||
@ -1409,6 +1418,54 @@ const Settings: Component<SettingsProps> = (props) => {
} }
}; };
const handleNodeTemperatureMonitoringChange = async (nodeId: string, enabled: boolean | null): Promise<void> => {
if (savingTemperatureSetting()) {
return;
}
const node = nodes().find((n) => n.id === nodeId);
if (!node) {
return;
}
const previous = node.temperatureMonitoringEnabled;
setSavingTemperatureSetting(true);
// Update local state optimistically
setNodes(
nodes().map((n) => (n.id === nodeId ? { ...n, temperatureMonitoringEnabled: enabled } : n)),
);
// Also update editingNode if this is the node being edited
if (editingNode()?.id === nodeId) {
setEditingNode({ ...editingNode()!, temperatureMonitoringEnabled: enabled });
}
try {
await NodesAPI.updateNode(nodeId, { temperatureMonitoringEnabled: enabled } as any);
if (enabled === true) {
notificationStore.success('Temperature monitoring enabled for this node', 2000);
} else if (enabled === false) {
notificationStore.info('Temperature monitoring disabled for this node', 2000);
} else {
notificationStore.info('Using global temperature monitoring setting', 2000);
}
} catch (error) {
logger.error('Failed to update node temperature monitoring setting', error);
notificationStore.error('Failed to update temperature monitoring setting');
// Revert on error
setNodes(
nodes().map((n) => (n.id === nodeId ? { ...n, temperatureMonitoringEnabled: previous } : n)),
);
// Also revert editingNode
if (editingNode()?.id === nodeId) {
setEditingNode({ ...editingNode()!, temperatureMonitoringEnabled: previous });
}
} finally {
setSavingTemperatureSetting(false);
}
};
const handleDiscoveryModeChange = async (mode: 'auto' | 'custom') => { const handleDiscoveryModeChange = async (mode: 'auto' | 'custom') => {
if (envOverrides().discoverySubnet || savingDiscoverySettings()) { if (envOverrides().discoverySubnet || savingDiscoverySettings()) {
return; return;
@ -2356,6 +2413,7 @@ const Settings: Component<SettingsProps> = (props) => {
<PveNodesTable <PveNodesTable
nodes={pveNodes()} nodes={pveNodes()}
stateNodes={state.nodes ?? []} stateNodes={state.nodes ?? []}
globalTemperatureMonitoringEnabled={temperatureMonitoringEnabled()}
onTestConnection={testNodeConnection} onTestConnection={testNodeConnection}
onEdit={(node) => { onEdit={(node) => {
setEditingNode(node); setEditingNode(node);
@ -2642,6 +2700,7 @@ const Settings: Component<SettingsProps> = (props) => {
<PbsNodesTable <PbsNodesTable
nodes={pbsNodes()} nodes={pbsNodes()}
statePbs={state.pbs ?? []} statePbs={state.pbs ?? []}
globalTemperatureMonitoringEnabled={temperatureMonitoringEnabled()}
onTestConnection={testNodeConnection} onTestConnection={testNodeConnection}
onEdit={(node) => { onEdit={(node) => {
setEditingNode(node); setEditingNode(node);
@ -2928,6 +2987,7 @@ const Settings: Component<SettingsProps> = (props) => {
<PmgNodesTable <PmgNodesTable
nodes={pmgNodes()} nodes={pmgNodes()}
statePmg={state.pmg ?? []} statePmg={state.pmg ?? []}
globalTemperatureMonitoringEnabled={temperatureMonitoringEnabled()}
onTestConnection={testNodeConnection} onTestConnection={testNodeConnection}
onEdit={(node) => { onEdit={(node) => {
setEditingNode(nodes().find((n) => n.id === node.id) ?? null); setEditingNode(nodes().find((n) => n.id === node.id) ?? null);
@ -6572,10 +6632,18 @@ const Settings: Component<SettingsProps> = (props) => {
nodeType="pve" nodeType="pve"
editingNode={editingNode()?.type === 'pve' ? (editingNode() ?? undefined) : undefined} editingNode={editingNode()?.type === 'pve' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined} securityStatus={securityStatus() ?? undefined}
temperatureMonitoringEnabled={temperatureMonitoringEnabled()} temperatureMonitoringEnabled={
editingNode()?.temperatureMonitoringEnabled !== undefined
? editingNode()!.temperatureMonitoringEnabled
: temperatureMonitoringEnabled()
}
temperatureMonitoringLocked={temperatureMonitoringLocked()} temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()} savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={handleTemperatureMonitoringChange} onToggleTemperatureMonitoring={
editingNode()?.id
? (enabled: boolean) => handleNodeTemperatureMonitoringChange(editingNode()!.id, enabled)
: handleTemperatureMonitoringChange
}
onSave={async (nodeData) => { onSave={async (nodeData) => {
try { try {
if (editingNode() && editingNode()!.id) { if (editingNode() && editingNode()!.id) {
@ -6638,6 +6706,18 @@ const Settings: Component<SettingsProps> = (props) => {
nodeType="pbs" nodeType="pbs"
editingNode={editingNode()?.type === 'pbs' ? (editingNode() ?? undefined) : undefined} editingNode={editingNode()?.type === 'pbs' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined} securityStatus={securityStatus() ?? undefined}
temperatureMonitoringEnabled={
editingNode()?.temperatureMonitoringEnabled !== undefined
? editingNode()!.temperatureMonitoringEnabled
: temperatureMonitoringEnabled()
}
temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={
editingNode()?.id
? (enabled: boolean) => handleNodeTemperatureMonitoringChange(editingNode()!.id, enabled)
: handleTemperatureMonitoringChange
}
onSave={async (nodeData) => { onSave={async (nodeData) => {
try { try {
if (editingNode() && editingNode()!.id) { if (editingNode() && editingNode()!.id) {
@ -6698,6 +6778,18 @@ const Settings: Component<SettingsProps> = (props) => {
nodeType="pmg" nodeType="pmg"
editingNode={editingNode()?.type === 'pmg' ? (editingNode() ?? undefined) : undefined} editingNode={editingNode()?.type === 'pmg' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined} securityStatus={securityStatus() ?? undefined}
temperatureMonitoringEnabled={
editingNode()?.temperatureMonitoringEnabled !== undefined
? editingNode()!.temperatureMonitoringEnabled
: temperatureMonitoringEnabled()
}
temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={
editingNode()?.id
? (enabled: boolean) => handleNodeTemperatureMonitoringChange(editingNode()!.id, enabled)
: handleTemperatureMonitoringChange
}
onSave={async (nodeData) => { onSave={async (nodeData) => {
try { try {
if (editingNode() && editingNode()!.id) { if (editingNode() && editingNode()!.id) {

View file

@ -527,6 +527,7 @@ const Storage: Component = () => {
{/* Node Selector */} {/* Node Selector */}
<UnifiedNodeSelector <UnifiedNodeSelector
currentTab="storage" currentTab="storage"
globalTemperatureMonitoringEnabled={state.temperatureMonitoringEnabled}
onNodeSelect={handleNodeSelect} onNodeSelect={handleNodeSelect}
filteredStorage={sortedStorage()} filteredStorage={sortedStorage()}
searchTerm={searchTerm()} searchTerm={searchTerm()}

View file

@ -18,6 +18,7 @@ interface NodeSummaryTableProps {
backupCounts?: Record<string, number>; backupCounts?: Record<string, number>;
currentTab: 'dashboard' | 'storage' | 'backups'; currentTab: 'dashboard' | 'storage' | 'backups';
selectedNode: string | null; selectedNode: string | null;
globalTemperatureMonitoringEnabled?: boolean;
onNodeClick: (nodeId: string, nodeType: 'pve' | 'pbs') => void; onNodeClick: (nodeId: string, nodeType: 'pve' | 'pbs') => void;
} }
@ -25,6 +26,16 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
const { activeAlerts, state } = useWebSocket(); const { activeAlerts, state } = useWebSocket();
const alertsActivation = useAlertsActivation(); const alertsActivation = useAlertsActivation();
const alertsEnabled = createMemo(() => alertsActivation.activationState() === 'active'); const alertsEnabled = createMemo(() => alertsActivation.activationState() === 'active');
const isTemperatureMonitoringEnabled = (node: Node): boolean => {
const globalEnabled = props.globalTemperatureMonitoringEnabled ?? true;
// Check per-node setting first, fall back to global
if (node.temperatureMonitoringEnabled !== undefined && node.temperatureMonitoringEnabled !== null) {
return node.temperatureMonitoringEnabled;
}
return globalEnabled;
};
type CountSortKey = 'vmCount' | 'containerCount' | 'storageCount' | 'diskCount' | 'backupCount'; type CountSortKey = 'vmCount' | 'containerCount' | 'storageCount' | 'diskCount' | 'backupCount';
type SortKey = type SortKey =
| 'default' | 'default'
@ -69,7 +80,12 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
}); });
const hasAnyTemperatureData = createMemo(() => { const hasAnyTemperatureData = createMemo(() => {
return props.nodes?.some((node) => node.temperature?.available) || false; // Show temperature column if ANY node has monitoring enabled OR has temperature data
return (
props.nodes?.some(
(node) => node.temperature?.available || isTemperatureMonitoringEnabled(node),
) || false
);
}); });
const nodeKey = (instance?: string, nodeName?: string) => `${instance ?? ''}::${nodeName ?? ''}`; const nodeKey = (instance?: string, nodeName?: string) => `${instance ?? ''}::${nodeName ?? ''}`;
@ -612,7 +628,8 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
online && online &&
isPVE && isPVE &&
cpuTemperatureValue !== null && cpuTemperatureValue !== null &&
(node!.temperature?.hasCPU ?? node!.temperature?.available) (node!.temperature?.hasCPU ?? node!.temperature?.available) &&
isTemperatureMonitoringEnabled(node!)
} }
fallback={ fallback={
<span class="text-xs text-gray-400 dark:text-gray-500">-</span> <span class="text-xs text-gray-400 dark:text-gray-500">-</span>

View file

@ -5,6 +5,7 @@ import type { Node, VM, Container, Storage } from '@/types/api';
interface UnifiedNodeSelectorProps { interface UnifiedNodeSelectorProps {
currentTab: 'dashboard' | 'storage' | 'backups'; currentTab: 'dashboard' | 'storage' | 'backups';
globalTemperatureMonitoringEnabled?: boolean;
onNodeSelect?: (nodeId: string | null, nodeType: 'pve' | 'pbs' | null) => void; onNodeSelect?: (nodeId: string | null, nodeType: 'pve' | 'pbs' | null) => void;
onNamespaceSelect?: (namespace: string) => void; onNamespaceSelect?: (namespace: string) => void;
nodes?: Node[]; nodes?: Node[];
@ -108,6 +109,7 @@ export const UnifiedNodeSelector: Component<UnifiedNodeSelectorProps> = (props)
backupCounts={backupCounts()} backupCounts={backupCounts()}
currentTab={props.currentTab} currentTab={props.currentTab}
selectedNode={selectedNode()} selectedNode={selectedNode()}
globalTemperatureMonitoringEnabled={props.globalTemperatureMonitoringEnabled}
onNodeClick={handleNodeClick} onNodeClick={handleNodeClick}
/> />
</div> </div>

View file

@ -24,6 +24,7 @@ export interface State {
activeAlerts: Alert[]; activeAlerts: Alert[];
recentlyResolved: ResolvedAlert[]; recentlyResolved: ResolvedAlert[];
lastUpdate: string; lastUpdate: string;
temperatureMonitoringEnabled?: boolean;
} }
export interface RemovedDockerHost { export interface RemovedDockerHost {
@ -50,6 +51,7 @@ export interface Node {
pveVersion: string; pveVersion: string;
cpuInfo: CPUInfo; cpuInfo: CPUInfo;
temperature?: Temperature; // CPU/NVMe temperatures temperature?: Temperature; // CPU/NVMe temperatures
temperatureMonitoringEnabled?: boolean | null; // Per-node temperature monitoring override
lastSeen: string; lastSeen: string;
connectionHealth: string; connectionHealth: string;
isClusterMember?: boolean; // True if part of a cluster isClusterMember?: boolean; // True if part of a cluster

View file

@ -27,6 +27,7 @@ export interface PVENodeConfig {
monitorStorage: boolean; monitorStorage: boolean;
monitorBackups: boolean; monitorBackups: boolean;
monitorPhysicalDisks: boolean; monitorPhysicalDisks: boolean;
temperatureMonitoringEnabled?: boolean | null;
// Cluster information // Cluster information
isCluster?: boolean; isCluster?: boolean;
clusterName?: string; clusterName?: string;
@ -45,6 +46,7 @@ export interface PBSNodeConfig {
password?: string; password?: string;
fingerprint?: string; fingerprint?: string;
verifySSL: boolean; verifySSL: boolean;
temperatureMonitoringEnabled?: boolean | null;
monitorDatastores: boolean; monitorDatastores: boolean;
monitorSyncJobs: boolean; monitorSyncJobs: boolean;
monitorVerifyJobs: boolean; monitorVerifyJobs: boolean;
@ -64,6 +66,7 @@ export interface PMGNodeConfig {
password?: string; password?: string;
fingerprint?: string; fingerprint?: string;
verifySSL: boolean; verifySSL: boolean;
temperatureMonitoringEnabled?: boolean | null;
monitorMailStats: boolean; monitorMailStats: boolean;
monitorQueues: boolean; monitorQueues: boolean;
monitorQuarantine: boolean; monitorQuarantine: boolean;

View file

@ -371,21 +371,22 @@ type NodeConfigRequest struct {
TokenName string `json:"tokenName,omitempty"` TokenName string `json:"tokenName,omitempty"`
TokenValue string `json:"tokenValue,omitempty"` TokenValue string `json:"tokenValue,omitempty"`
Fingerprint string `json:"fingerprint,omitempty"` Fingerprint string `json:"fingerprint,omitempty"`
VerifySSL bool `json:"verifySSL"` VerifySSL *bool `json:"verifySSL,omitempty"`
MonitorVMs bool `json:"monitorVMs,omitempty"` // PVE only MonitorVMs *bool `json:"monitorVMs,omitempty"` // PVE only
MonitorContainers bool `json:"monitorContainers,omitempty"` // PVE only MonitorContainers *bool `json:"monitorContainers,omitempty"` // PVE only
MonitorStorage bool `json:"monitorStorage,omitempty"` // PVE only MonitorStorage *bool `json:"monitorStorage,omitempty"` // PVE only
MonitorBackups bool `json:"monitorBackups,omitempty"` // PVE only MonitorBackups *bool `json:"monitorBackups,omitempty"` // PVE only
MonitorPhysicalDisks *bool `json:"monitorPhysicalDisks,omitempty"` // PVE only (nil = enabled by default) MonitorPhysicalDisks *bool `json:"monitorPhysicalDisks,omitempty"` // PVE only (nil = enabled by default)
MonitorDatastores bool `json:"monitorDatastores,omitempty"` // PBS only TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"` // All types (nil = use global setting)
MonitorSyncJobs bool `json:"monitorSyncJobs,omitempty"` // PBS only MonitorDatastores *bool `json:"monitorDatastores,omitempty"` // PBS only
MonitorVerifyJobs bool `json:"monitorVerifyJobs,omitempty"` // PBS only MonitorSyncJobs *bool `json:"monitorSyncJobs,omitempty"` // PBS only
MonitorPruneJobs bool `json:"monitorPruneJobs,omitempty"` // PBS only MonitorVerifyJobs *bool `json:"monitorVerifyJobs,omitempty"` // PBS only
MonitorGarbageJobs bool `json:"monitorGarbageJobs,omitempty"` // PBS only MonitorPruneJobs *bool `json:"monitorPruneJobs,omitempty"` // PBS only
MonitorMailStats bool `json:"monitorMailStats,omitempty"` // PMG only MonitorGarbageJobs *bool `json:"monitorGarbageJobs,omitempty"` // PBS only
MonitorQueues bool `json:"monitorQueues,omitempty"` // PMG only MonitorMailStats *bool `json:"monitorMailStats,omitempty"` // PMG only
MonitorQuarantine bool `json:"monitorQuarantine,omitempty"` // PMG only MonitorQueues *bool `json:"monitorQueues,omitempty"` // PMG only
MonitorDomainStats bool `json:"monitorDomainStats,omitempty"` // PMG only MonitorQuarantine *bool `json:"monitorQuarantine,omitempty"` // PMG only
MonitorDomainStats *bool `json:"monitorDomainStats,omitempty"` // PMG only
} }
// NodeResponse represents a node in API responses // NodeResponse represents a node in API responses
@ -405,6 +406,7 @@ type NodeResponse struct {
MonitorStorage bool `json:"monitorStorage,omitempty"` MonitorStorage bool `json:"monitorStorage,omitempty"`
MonitorBackups bool `json:"monitorBackups,omitempty"` MonitorBackups bool `json:"monitorBackups,omitempty"`
MonitorPhysicalDisks *bool `json:"monitorPhysicalDisks,omitempty"` MonitorPhysicalDisks *bool `json:"monitorPhysicalDisks,omitempty"`
TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"`
MonitorDatastores bool `json:"monitorDatastores,omitempty"` MonitorDatastores bool `json:"monitorDatastores,omitempty"`
MonitorSyncJobs bool `json:"monitorSyncJobs,omitempty"` MonitorSyncJobs bool `json:"monitorSyncJobs,omitempty"`
MonitorVerifyJobs bool `json:"monitorVerifyJobs,omitempty"` MonitorVerifyJobs bool `json:"monitorVerifyJobs,omitempty"`
@ -746,6 +748,7 @@ func (h *ConfigHandlers) GetAllNodesForAPI() []NodeResponse {
MonitorStorage: pve.MonitorStorage, MonitorStorage: pve.MonitorStorage,
MonitorBackups: pve.MonitorBackups, MonitorBackups: pve.MonitorBackups,
MonitorPhysicalDisks: pve.MonitorPhysicalDisks, MonitorPhysicalDisks: pve.MonitorPhysicalDisks,
TemperatureMonitoringEnabled: pve.TemperatureMonitoringEnabled,
Status: h.getNodeStatus("pve", pve.Name), Status: h.getNodeStatus("pve", pve.Name),
IsCluster: pve.IsCluster, IsCluster: pve.IsCluster,
ClusterName: pve.ClusterName, ClusterName: pve.ClusterName,
@ -767,6 +770,7 @@ func (h *ConfigHandlers) GetAllNodesForAPI() []NodeResponse {
HasToken: pbs.TokenValue != "", HasToken: pbs.TokenValue != "",
Fingerprint: pbs.Fingerprint, Fingerprint: pbs.Fingerprint,
VerifySSL: pbs.VerifySSL, VerifySSL: pbs.VerifySSL,
TemperatureMonitoringEnabled: pbs.TemperatureMonitoringEnabled,
MonitorDatastores: pbs.MonitorDatastores, MonitorDatastores: pbs.MonitorDatastores,
MonitorSyncJobs: pbs.MonitorSyncJobs, MonitorSyncJobs: pbs.MonitorSyncJobs,
MonitorVerifyJobs: pbs.MonitorVerifyJobs, MonitorVerifyJobs: pbs.MonitorVerifyJobs,
@ -795,6 +799,7 @@ func (h *ConfigHandlers) GetAllNodesForAPI() []NodeResponse {
HasToken: pmgInst.TokenValue != "", HasToken: pmgInst.TokenValue != "",
Fingerprint: pmgInst.Fingerprint, Fingerprint: pmgInst.Fingerprint,
VerifySSL: pmgInst.VerifySSL, VerifySSL: pmgInst.VerifySSL,
TemperatureMonitoringEnabled: pmgInst.TemperatureMonitoringEnabled,
MonitorMailStats: monitorMailStats, MonitorMailStats: monitorMailStats,
MonitorQueues: pmgInst.MonitorQueues, MonitorQueues: pmgInst.MonitorQueues,
MonitorQuarantine: pmgInst.MonitorQuarantine, MonitorQuarantine: pmgInst.MonitorQuarantine,
@ -1142,7 +1147,11 @@ func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
strings.Contains(req.Name, "concurrent-") strings.Contains(req.Name, "concurrent-")
if !skipClusterDetection { if !skipClusterDetection {
clientConfig := config.CreateProxmoxConfigFromFields(host, req.User, req.Password, req.TokenName, req.TokenValue, req.Fingerprint, req.VerifySSL) verifySSL := false
if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
clientConfig := config.CreateProxmoxConfigFromFields(host, req.User, req.Password, req.TokenName, req.TokenValue, req.Fingerprint, verifySSL)
isCluster, clusterName, clusterEndpoints = detectPVECluster(clientConfig, req.Name) isCluster, clusterName, clusterEndpoints = detectPVECluster(clientConfig, req.Name)
} }
@ -1153,6 +1162,28 @@ func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
Msg("Detected Proxmox cluster, auto-discovering all nodes") Msg("Detected Proxmox cluster, auto-discovering all nodes")
} }
// Use sensible defaults for boolean fields if not provided
verifySSL := false
if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
monitorVMs := true // Default to true
if req.MonitorVMs != nil {
monitorVMs = *req.MonitorVMs
}
monitorContainers := true // Default to true
if req.MonitorContainers != nil {
monitorContainers = *req.MonitorContainers
}
monitorStorage := true // Default to true
if req.MonitorStorage != nil {
monitorStorage = *req.MonitorStorage
}
monitorBackups := true // Default to true
if req.MonitorBackups != nil {
monitorBackups = *req.MonitorBackups
}
pve := config.PVEInstance{ pve := config.PVEInstance{
Name: req.Name, Name: req.Name,
Host: host, // Use normalized host Host: host, // Use normalized host
@ -1161,12 +1192,13 @@ func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
TokenName: req.TokenName, TokenName: req.TokenName,
TokenValue: req.TokenValue, TokenValue: req.TokenValue,
Fingerprint: req.Fingerprint, Fingerprint: req.Fingerprint,
VerifySSL: req.VerifySSL, VerifySSL: verifySSL,
MonitorVMs: req.MonitorVMs, MonitorVMs: monitorVMs,
MonitorContainers: req.MonitorContainers, MonitorContainers: monitorContainers,
MonitorStorage: req.MonitorStorage, MonitorStorage: monitorStorage,
MonitorBackups: req.MonitorBackups, MonitorBackups: monitorBackups,
MonitorPhysicalDisks: req.MonitorPhysicalDisks, MonitorPhysicalDisks: req.MonitorPhysicalDisks,
TemperatureMonitoringEnabled: req.TemperatureMonitoringEnabled,
IsCluster: isCluster, IsCluster: isCluster,
ClusterName: clusterName, ClusterName: clusterName,
ClusterEndpoints: clusterEndpoints, ClusterEndpoints: clusterEndpoints,
@ -1221,6 +1253,36 @@ func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
} }
} }
// Use sensible defaults for boolean fields if not provided
verifySSL := false
if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
monitorBackups := true // Default to true for PBS
if req.MonitorBackups != nil {
monitorBackups = *req.MonitorBackups
}
monitorDatastores := false
if req.MonitorDatastores != nil {
monitorDatastores = *req.MonitorDatastores
}
monitorSyncJobs := false
if req.MonitorSyncJobs != nil {
monitorSyncJobs = *req.MonitorSyncJobs
}
monitorVerifyJobs := false
if req.MonitorVerifyJobs != nil {
monitorVerifyJobs = *req.MonitorVerifyJobs
}
monitorPruneJobs := false
if req.MonitorPruneJobs != nil {
monitorPruneJobs = *req.MonitorPruneJobs
}
monitorGarbageJobs := false
if req.MonitorGarbageJobs != nil {
monitorGarbageJobs = *req.MonitorGarbageJobs
}
pbs := config.PBSInstance{ pbs := config.PBSInstance{
Name: req.Name, Name: req.Name,
Host: host, Host: host,
@ -1229,13 +1291,14 @@ func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
TokenName: pbsTokenName, TokenName: pbsTokenName,
TokenValue: pbsTokenValue, TokenValue: pbsTokenValue,
Fingerprint: req.Fingerprint, Fingerprint: req.Fingerprint,
VerifySSL: req.VerifySSL, VerifySSL: verifySSL,
MonitorBackups: true, // Enable by default for PBS MonitorBackups: monitorBackups,
MonitorDatastores: req.MonitorDatastores, MonitorDatastores: monitorDatastores,
MonitorSyncJobs: req.MonitorSyncJobs, MonitorSyncJobs: monitorSyncJobs,
MonitorVerifyJobs: req.MonitorVerifyJobs, MonitorVerifyJobs: monitorVerifyJobs,
MonitorPruneJobs: req.MonitorPruneJobs, MonitorPruneJobs: monitorPruneJobs,
MonitorGarbageJobs: req.MonitorGarbageJobs, MonitorGarbageJobs: monitorGarbageJobs,
TemperatureMonitoringEnabled: req.TemperatureMonitoringEnabled,
} }
h.config.PBSInstances = append(h.config.PBSInstances, pbs) h.config.PBSInstances = append(h.config.PBSInstances, pbs)
} else if req.Type == "pmg" { } else if req.Type == "pmg" {
@ -1269,9 +1332,37 @@ func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
} }
} }
monitorMailStats := req.MonitorMailStats // Use sensible defaults for boolean fields if not provided
if !req.MonitorMailStats && !req.MonitorQueues && !req.MonitorQuarantine && !req.MonitorDomainStats { verifySSL := false
monitorMailStats = true if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
// Check if any monitoring flags are explicitly set to true
anyMonitoringEnabled := (req.MonitorMailStats != nil && *req.MonitorMailStats) ||
(req.MonitorQueues != nil && *req.MonitorQueues) ||
(req.MonitorQuarantine != nil && *req.MonitorQuarantine) ||
(req.MonitorDomainStats != nil && *req.MonitorDomainStats)
// Default MonitorMailStats to true if no monitoring is explicitly enabled
monitorMailStats := true // Default to true
if req.MonitorMailStats != nil {
monitorMailStats = *req.MonitorMailStats
} else if anyMonitoringEnabled {
monitorMailStats = false // Don't default to true if other monitoring is enabled
}
monitorQueues := false
if req.MonitorQueues != nil {
monitorQueues = *req.MonitorQueues
}
monitorQuarantine := false
if req.MonitorQuarantine != nil {
monitorQuarantine = *req.MonitorQuarantine
}
monitorDomainStats := false
if req.MonitorDomainStats != nil {
monitorDomainStats = *req.MonitorDomainStats
} }
pmgInstance := config.PMGInstance{ pmgInstance := config.PMGInstance{
@ -1282,11 +1373,12 @@ func (h *ConfigHandlers) HandleAddNode(w http.ResponseWriter, r *http.Request) {
TokenName: pmgTokenName, TokenName: pmgTokenName,
TokenValue: pmgTokenValue, TokenValue: pmgTokenValue,
Fingerprint: req.Fingerprint, Fingerprint: req.Fingerprint,
VerifySSL: req.VerifySSL, VerifySSL: verifySSL,
MonitorMailStats: monitorMailStats, MonitorMailStats: monitorMailStats,
MonitorQueues: req.MonitorQueues, MonitorQueues: monitorQueues,
MonitorQuarantine: req.MonitorQuarantine, MonitorQuarantine: monitorQuarantine,
MonitorDomainStats: req.MonitorDomainStats, MonitorDomainStats: monitorDomainStats,
TemperatureMonitoringEnabled: req.TemperatureMonitoringEnabled,
} }
h.config.PMGInstances = append(h.config.PMGInstances, pmgInstance) h.config.PMGInstances = append(h.config.PMGInstances, pmgInstance)
} }
@ -1412,13 +1504,17 @@ func (h *ConfigHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Req
authUser = normalizePVEUser(authUser) authUser = normalizePVEUser(authUser)
req.User = authUser req.User = authUser
} }
verifySSL := false
if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
clientConfig := proxmox.ClientConfig{ clientConfig := proxmox.ClientConfig{
Host: host, Host: host,
User: authUser, User: authUser,
Password: req.Password, Password: req.Password,
TokenName: req.TokenName, // Pass the full token ID TokenName: req.TokenName, // Pass the full token ID
TokenValue: req.TokenValue, TokenValue: req.TokenValue,
VerifySSL: req.VerifySSL, VerifySSL: verifySSL,
Fingerprint: req.Fingerprint, Fingerprint: req.Fingerprint,
} }
@ -1500,13 +1596,17 @@ func (h *ConfigHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Req
pbsUser = pbsUser + "@pbs" // Default to @pbs realm if not specified pbsUser = pbsUser + "@pbs" // Default to @pbs realm if not specified
} }
verifySSL := false
if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
clientConfig := pbs.ClientConfig{ clientConfig := pbs.ClientConfig{
Host: host, Host: host,
User: pbsUser, User: pbsUser,
Password: req.Password, Password: req.Password,
TokenName: pbsTokenName, TokenName: pbsTokenName,
TokenValue: req.TokenValue, TokenValue: req.TokenValue,
VerifySSL: req.VerifySSL, VerifySSL: verifySSL,
Fingerprint: req.Fingerprint, Fingerprint: req.Fingerprint,
} }
@ -1549,7 +1649,11 @@ func (h *ConfigHandlers) HandleTestConnection(w http.ResponseWriter, r *http.Req
host = host + ":8006" host = host + ":8006"
} }
clientConfig := config.CreatePMGConfigFromFields(host, req.User, req.Password, req.TokenName, req.TokenValue, req.Fingerprint, req.VerifySSL) verifySSL := false
if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
clientConfig := config.CreatePMGConfigFromFields(host, req.User, req.Password, req.TokenName, req.TokenValue, req.Fingerprint, verifySSL)
if req.Password != "" && req.TokenName == "" && req.TokenValue == "" { if req.Password != "" && req.TokenName == "" && req.TokenValue == "" {
if clientConfig.User != "" && !strings.Contains(clientConfig.User, "@") { if clientConfig.User != "" && !strings.Contains(clientConfig.User, "@") {
@ -1632,6 +1736,12 @@ func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request
return return
} }
// Debug: Log the received temperatureMonitoringEnabled value
log.Info().
Str("nodeID", nodeID).
Interface("temperatureMonitoringEnabled", req.TemperatureMonitoringEnabled).
Msg("Received node update request")
// Parse node ID // Parse node ID
parts := strings.Split(nodeID, "-") parts := strings.Split(nodeID, "-")
if len(parts) != 2 { if len(parts) != 2 {
@ -1706,12 +1816,27 @@ func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request
} }
pve.Fingerprint = req.Fingerprint pve.Fingerprint = req.Fingerprint
pve.VerifySSL = req.VerifySSL if req.VerifySSL != nil {
pve.MonitorVMs = req.MonitorVMs pve.VerifySSL = *req.VerifySSL
pve.MonitorContainers = req.MonitorContainers }
pve.MonitorStorage = req.MonitorStorage if req.MonitorVMs != nil {
pve.MonitorBackups = req.MonitorBackups pve.MonitorVMs = *req.MonitorVMs
}
if req.MonitorContainers != nil {
pve.MonitorContainers = *req.MonitorContainers
}
if req.MonitorStorage != nil {
pve.MonitorStorage = *req.MonitorStorage
}
if req.MonitorBackups != nil {
pve.MonitorBackups = *req.MonitorBackups
}
if req.MonitorPhysicalDisks != nil {
pve.MonitorPhysicalDisks = req.MonitorPhysicalDisks pve.MonitorPhysicalDisks = req.MonitorPhysicalDisks
}
if req.TemperatureMonitoringEnabled != nil {
pve.TemperatureMonitoringEnabled = req.TemperatureMonitoringEnabled
}
} else if nodeType == "pbs" && index < len(h.config.PBSInstances) { } else if nodeType == "pbs" && index < len(h.config.PBSInstances) {
pbs := &h.config.PBSInstances[index] pbs := &h.config.PBSInstances[index]
pbs.Name = req.Name pbs.Name = req.Name
@ -1775,13 +1900,32 @@ func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request
// else: No authentication changes - preserve existing auth fields // else: No authentication changes - preserve existing auth fields
pbs.Fingerprint = req.Fingerprint pbs.Fingerprint = req.Fingerprint
pbs.VerifySSL = req.VerifySSL if req.VerifySSL != nil {
pbs.VerifySSL = *req.VerifySSL
}
if req.MonitorBackups != nil {
pbs.MonitorBackups = *req.MonitorBackups
} else {
pbs.MonitorBackups = true // Enable by default for PBS pbs.MonitorBackups = true // Enable by default for PBS
pbs.MonitorDatastores = req.MonitorDatastores }
pbs.MonitorSyncJobs = req.MonitorSyncJobs if req.MonitorDatastores != nil {
pbs.MonitorVerifyJobs = req.MonitorVerifyJobs pbs.MonitorDatastores = *req.MonitorDatastores
pbs.MonitorPruneJobs = req.MonitorPruneJobs }
pbs.MonitorGarbageJobs = req.MonitorGarbageJobs if req.MonitorSyncJobs != nil {
pbs.MonitorSyncJobs = *req.MonitorSyncJobs
}
if req.MonitorVerifyJobs != nil {
pbs.MonitorVerifyJobs = *req.MonitorVerifyJobs
}
if req.MonitorPruneJobs != nil {
pbs.MonitorPruneJobs = *req.MonitorPruneJobs
}
if req.MonitorGarbageJobs != nil {
pbs.MonitorGarbageJobs = *req.MonitorGarbageJobs
}
if req.TemperatureMonitoringEnabled != nil {
pbs.TemperatureMonitoringEnabled = req.TemperatureMonitoringEnabled
}
} else if nodeType == "pmg" && index < len(h.config.PMGInstances) { } else if nodeType == "pmg" && index < len(h.config.PMGInstances) {
pmgInst := &h.config.PMGInstances[index] pmgInst := &h.config.PMGInstances[index]
pmgInst.Name = req.Name pmgInst.Name = req.Name
@ -1838,15 +1982,30 @@ func (h *ConfigHandlers) HandleUpdateNode(w http.ResponseWriter, r *http.Request
// else: No authentication changes - preserve existing auth fields // else: No authentication changes - preserve existing auth fields
pmgInst.Fingerprint = req.Fingerprint pmgInst.Fingerprint = req.Fingerprint
pmgInst.VerifySSL = req.VerifySSL if req.VerifySSL != nil {
monitorMailStats := req.MonitorMailStats pmgInst.VerifySSL = *req.VerifySSL
if !req.MonitorMailStats && !req.MonitorQueues && !req.MonitorQuarantine && !req.MonitorDomainStats { }
monitorMailStats = true // Special logic for MonitorMailStats: default to true if all monitor flags are false/unset
if req.MonitorMailStats != nil {
pmgInst.MonitorMailStats = *req.MonitorMailStats
} else if (req.MonitorMailStats == nil || !*req.MonitorMailStats) &&
(req.MonitorQueues == nil || !*req.MonitorQueues) &&
(req.MonitorQuarantine == nil || !*req.MonitorQuarantine) &&
(req.MonitorDomainStats == nil || !*req.MonitorDomainStats) {
pmgInst.MonitorMailStats = true
}
if req.MonitorQueues != nil {
pmgInst.MonitorQueues = *req.MonitorQueues
}
if req.MonitorQuarantine != nil {
pmgInst.MonitorQuarantine = *req.MonitorQuarantine
}
if req.MonitorDomainStats != nil {
pmgInst.MonitorDomainStats = *req.MonitorDomainStats
}
if req.TemperatureMonitoringEnabled != nil {
pmgInst.TemperatureMonitoringEnabled = req.TemperatureMonitoringEnabled
} }
pmgInst.MonitorMailStats = monitorMailStats
pmgInst.MonitorQueues = req.MonitorQueues
pmgInst.MonitorQuarantine = req.MonitorQuarantine
pmgInst.MonitorDomainStats = req.MonitorDomainStats
} else { } else {
http.Error(w, "Node not found", http.StatusNotFound) http.Error(w, "Node not found", http.StatusNotFound)
return return
@ -2220,13 +2379,17 @@ func (h *ConfigHandlers) HandleTestNodeConfig(w http.ResponseWriter, r *http.Req
authUser = normalizePVEUser(authUser) authUser = normalizePVEUser(authUser)
req.User = authUser req.User = authUser
} }
verifySSL := false
if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
clientConfig := proxmox.ClientConfig{ clientConfig := proxmox.ClientConfig{
Host: req.Host, Host: req.Host,
User: authUser, User: authUser,
Password: req.Password, Password: req.Password,
TokenName: req.TokenName, TokenName: req.TokenName,
TokenValue: req.TokenValue, TokenValue: req.TokenValue,
VerifySSL: req.VerifySSL, VerifySSL: verifySSL,
Fingerprint: req.Fingerprint, Fingerprint: req.Fingerprint,
} }
client, err := proxmox.NewClient(clientConfig) client, err := proxmox.NewClient(clientConfig)
@ -2257,13 +2420,17 @@ func (h *ConfigHandlers) HandleTestNodeConfig(w http.ResponseWriter, r *http.Req
} }
} else if req.Type == "pbs" { } else if req.Type == "pbs" {
// Create a temporary client to test connection // Create a temporary client to test connection
verifySSL := false
if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
clientConfig := pbs.ClientConfig{ clientConfig := pbs.ClientConfig{
Host: req.Host, Host: req.Host,
User: req.User, User: req.User,
Password: req.Password, Password: req.Password,
TokenName: req.TokenName, TokenName: req.TokenName,
TokenValue: req.TokenValue, TokenValue: req.TokenValue,
VerifySSL: req.VerifySSL, VerifySSL: verifySSL,
Fingerprint: req.Fingerprint, Fingerprint: req.Fingerprint,
} }
client, err := pbs.NewClient(clientConfig) client, err := pbs.NewClient(clientConfig)
@ -2293,13 +2460,17 @@ func (h *ConfigHandlers) HandleTestNodeConfig(w http.ResponseWriter, r *http.Req
} }
} }
} else if req.Type == "pmg" { } else if req.Type == "pmg" {
verifySSL := false
if req.VerifySSL != nil {
verifySSL = *req.VerifySSL
}
clientConfig := pmg.ClientConfig{ clientConfig := pmg.ClientConfig{
Host: req.Host, Host: req.Host,
User: req.User, User: req.User,
Password: req.Password, Password: req.Password,
TokenName: req.TokenName, TokenName: req.TokenName,
TokenValue: req.TokenValue, TokenValue: req.TokenValue,
VerifySSL: req.VerifySSL, VerifySSL: verifySSL,
Fingerprint: req.Fingerprint, Fingerprint: req.Fingerprint,
} }
client, err := pmg.NewClient(clientConfig) client, err := pmg.NewClient(clientConfig)
@ -5148,22 +5319,24 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
} }
// Create a node configuration // Create a node configuration
boolFalse := false
boolTrue := true
nodeConfig := NodeConfigRequest{ nodeConfig := NodeConfigRequest{
Type: req.Type, Type: req.Type,
Name: req.ServerName, Name: req.ServerName,
Host: host, // Use normalized host Host: host, // Use normalized host
TokenName: req.TokenID, TokenName: req.TokenID,
TokenValue: req.TokenValue, TokenValue: req.TokenValue,
VerifySSL: false, // Default to not verifying SSL for auto-registration VerifySSL: &boolFalse, // Default to not verifying SSL for auto-registration
MonitorVMs: true, MonitorVMs: &boolTrue,
MonitorContainers: true, MonitorContainers: &boolTrue,
MonitorStorage: true, MonitorStorage: &boolTrue,
MonitorBackups: true, MonitorBackups: &boolTrue,
MonitorDatastores: true, MonitorDatastores: &boolTrue,
MonitorSyncJobs: true, MonitorSyncJobs: &boolTrue,
MonitorVerifyJobs: true, MonitorVerifyJobs: &boolTrue,
MonitorPruneJobs: true, MonitorPruneJobs: &boolTrue,
MonitorGarbageJobs: false, MonitorGarbageJobs: &boolFalse,
} }
// Check if a node with this host already exists // Check if a node with this host already exists
@ -5235,25 +5408,46 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
// Add new node // Add new node
if req.Type == "pve" { if req.Type == "pve" {
// Check for cluster detection using helper // Check for cluster detection using helper
verifySSL := false
if nodeConfig.VerifySSL != nil {
verifySSL = *nodeConfig.VerifySSL
}
clientConfig := proxmox.ClientConfig{ clientConfig := proxmox.ClientConfig{
Host: nodeConfig.Host, Host: nodeConfig.Host,
TokenName: nodeConfig.TokenName, TokenName: nodeConfig.TokenName,
TokenValue: nodeConfig.TokenValue, TokenValue: nodeConfig.TokenValue,
VerifySSL: nodeConfig.VerifySSL, VerifySSL: verifySSL,
} }
isCluster, clusterName, clusterEndpoints := detectPVECluster(clientConfig, nodeConfig.Name) isCluster, clusterName, clusterEndpoints := detectPVECluster(clientConfig, nodeConfig.Name)
monitorVMs := true
if nodeConfig.MonitorVMs != nil {
monitorVMs = *nodeConfig.MonitorVMs
}
monitorContainers := true
if nodeConfig.MonitorContainers != nil {
monitorContainers = *nodeConfig.MonitorContainers
}
monitorStorage := true
if nodeConfig.MonitorStorage != nil {
monitorStorage = *nodeConfig.MonitorStorage
}
monitorBackups := true
if nodeConfig.MonitorBackups != nil {
monitorBackups = *nodeConfig.MonitorBackups
}
newInstance := config.PVEInstance{ newInstance := config.PVEInstance{
Name: nodeConfig.Name, Name: nodeConfig.Name,
Host: nodeConfig.Host, Host: nodeConfig.Host,
TokenName: nodeConfig.TokenName, TokenName: nodeConfig.TokenName,
TokenValue: nodeConfig.TokenValue, TokenValue: nodeConfig.TokenValue,
VerifySSL: nodeConfig.VerifySSL, VerifySSL: verifySSL,
MonitorVMs: nodeConfig.MonitorVMs, MonitorVMs: monitorVMs,
MonitorContainers: nodeConfig.MonitorContainers, MonitorContainers: monitorContainers,
MonitorStorage: nodeConfig.MonitorStorage, MonitorStorage: monitorStorage,
MonitorBackups: nodeConfig.MonitorBackups, MonitorBackups: monitorBackups,
IsCluster: isCluster, IsCluster: isCluster,
ClusterName: clusterName, ClusterName: clusterName,
ClusterEndpoints: clusterEndpoints, ClusterEndpoints: clusterEndpoints,
@ -5267,18 +5461,43 @@ func (h *ConfigHandlers) HandleAutoRegister(w http.ResponseWriter, r *http.Reque
Msg("Added Proxmox cluster via auto-registration") Msg("Added Proxmox cluster via auto-registration")
} }
} else { } else {
verifySSL := false
if nodeConfig.VerifySSL != nil {
verifySSL = *nodeConfig.VerifySSL
}
monitorDatastores := false
if nodeConfig.MonitorDatastores != nil {
monitorDatastores = *nodeConfig.MonitorDatastores
}
monitorSyncJobs := false
if nodeConfig.MonitorSyncJobs != nil {
monitorSyncJobs = *nodeConfig.MonitorSyncJobs
}
monitorVerifyJobs := false
if nodeConfig.MonitorVerifyJobs != nil {
monitorVerifyJobs = *nodeConfig.MonitorVerifyJobs
}
monitorPruneJobs := false
if nodeConfig.MonitorPruneJobs != nil {
monitorPruneJobs = *nodeConfig.MonitorPruneJobs
}
monitorGarbageJobs := false
if nodeConfig.MonitorGarbageJobs != nil {
monitorGarbageJobs = *nodeConfig.MonitorGarbageJobs
}
newInstance := config.PBSInstance{ newInstance := config.PBSInstance{
Name: nodeConfig.Name, Name: nodeConfig.Name,
Host: nodeConfig.Host, Host: nodeConfig.Host,
TokenName: nodeConfig.TokenName, TokenName: nodeConfig.TokenName,
TokenValue: nodeConfig.TokenValue, TokenValue: nodeConfig.TokenValue,
VerifySSL: nodeConfig.VerifySSL, VerifySSL: verifySSL,
MonitorBackups: true, // Enable by default for PBS MonitorBackups: true, // Enable by default for PBS
MonitorDatastores: nodeConfig.MonitorDatastores, MonitorDatastores: monitorDatastores,
MonitorSyncJobs: nodeConfig.MonitorSyncJobs, MonitorSyncJobs: monitorSyncJobs,
MonitorVerifyJobs: nodeConfig.MonitorVerifyJobs, MonitorVerifyJobs: monitorVerifyJobs,
MonitorPruneJobs: nodeConfig.MonitorPruneJobs, MonitorPruneJobs: monitorPruneJobs,
MonitorGarbageJobs: nodeConfig.MonitorGarbageJobs, MonitorGarbageJobs: monitorGarbageJobs,
} }
h.config.PBSInstances = append(h.config.PBSInstances, newInstance) h.config.PBSInstances = append(h.config.PBSInstances, newInstance)
} }

View file

@ -90,6 +90,7 @@ type NodeConfig struct {
HasPassword bool `json:"hasPassword"` HasPassword bool `json:"hasPassword"`
HasToken bool `json:"hasToken"` HasToken bool `json:"hasToken"`
SkipTLS bool `json:"skipTLS,omitempty"` SkipTLS bool `json:"skipTLS,omitempty"`
TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"`
Tags []string `json:"tags,omitempty"` Tags []string `json:"tags,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"` Metadata map[string]string `json:"metadata,omitempty"`
} }

View file

@ -399,6 +399,7 @@ type PVEInstance struct {
MonitorBackups bool MonitorBackups bool
MonitorPhysicalDisks *bool // Monitor physical disks (nil = enabled by default, can be explicitly disabled) MonitorPhysicalDisks *bool // Monitor physical disks (nil = enabled by default, can be explicitly disabled)
PhysicalDiskPollingMinutes int // How often to poll physical disks (0 = use default) PhysicalDiskPollingMinutes int // How often to poll physical disks (0 = use default)
TemperatureMonitoringEnabled *bool // Monitor temperature via SSH (nil = use global setting, true/false = override)
// Cluster support // Cluster support
IsCluster bool // True if this is a cluster IsCluster bool // True if this is a cluster
@ -432,6 +433,7 @@ type PBSInstance struct {
MonitorVerifyJobs bool MonitorVerifyJobs bool
MonitorPruneJobs bool MonitorPruneJobs bool
MonitorGarbageJobs bool MonitorGarbageJobs bool
TemperatureMonitoringEnabled *bool // Monitor temperature via SSH (nil = use global setting, true/false = override)
} }
// PMGInstance represents a Proxmox Mail Gateway connection // PMGInstance represents a Proxmox Mail Gateway connection
@ -449,6 +451,7 @@ type PMGInstance struct {
MonitorQueues bool MonitorQueues bool
MonitorQuarantine bool MonitorQuarantine bool
MonitorDomainStats bool MonitorDomainStats bool
TemperatureMonitoringEnabled *bool // Monitor temperature via SSH (nil = use global setting, true/false = override)
} }
// Global persistence instance for saving // Global persistence instance for saving

View file

@ -34,6 +34,7 @@ func (n Node) ToFrontend() NodeFrontend {
ConnectionHealth: n.ConnectionHealth, ConnectionHealth: n.ConnectionHealth,
IsClusterMember: n.IsClusterMember, IsClusterMember: n.IsClusterMember,
ClusterName: n.ClusterName, ClusterName: n.ClusterName,
TemperatureMonitoringEnabled: n.TemperatureMonitoringEnabled,
} }
// Include full Memory object if it has data // Include full Memory object if it has data

View file

@ -33,6 +33,7 @@ type State struct {
ActiveAlerts []Alert `json:"activeAlerts"` ActiveAlerts []Alert `json:"activeAlerts"`
RecentlyResolved []ResolvedAlert `json:"recentlyResolved"` RecentlyResolved []ResolvedAlert `json:"recentlyResolved"`
LastUpdate time.Time `json:"lastUpdate"` LastUpdate time.Time `json:"lastUpdate"`
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"`
} }
// Alert represents an active alert (simplified for State) // Alert represents an active alert (simplified for State)
@ -77,6 +78,7 @@ type Node struct {
PVEVersion string `json:"pveVersion"` PVEVersion string `json:"pveVersion"`
CPUInfo CPUInfo `json:"cpuInfo"` CPUInfo CPUInfo `json:"cpuInfo"`
Temperature *Temperature `json:"temperature,omitempty"` // CPU/NVMe temperatures Temperature *Temperature `json:"temperature,omitempty"` // CPU/NVMe temperatures
TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"` // Per-node temperature monitoring override
LastSeen time.Time `json:"lastSeen"` LastSeen time.Time `json:"lastSeen"`
ConnectionHealth string `json:"connectionHealth"` ConnectionHealth string `json:"connectionHealth"`
IsClusterMember bool `json:"isClusterMember"` // True if part of a cluster IsClusterMember bool `json:"isClusterMember"` // True if part of a cluster

View file

@ -29,6 +29,7 @@ type NodeFrontend struct {
ConnectionHealth string `json:"connectionHealth"` ConnectionHealth string `json:"connectionHealth"`
IsClusterMember bool `json:"isClusterMember,omitempty"` IsClusterMember bool `json:"isClusterMember,omitempty"`
ClusterName string `json:"clusterName,omitempty"` ClusterName string `json:"clusterName,omitempty"`
TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"` // Per-node temperature monitoring override
} }
// VMFrontend represents a VM with frontend-friendly field names // VMFrontend represents a VM with frontend-friendly field names
@ -441,4 +442,5 @@ type StateFrontend struct {
ConnectionHealth map[string]bool `json:"connectionHealth"` // Keep as is ConnectionHealth map[string]bool `json:"connectionHealth"` // Keep as is
Stats map[string]any `json:"stats"` // Empty object for now Stats map[string]any `json:"stats"` // Empty object for now
LastUpdate int64 `json:"lastUpdate"` // Unix timestamp LastUpdate int64 `json:"lastUpdate"` // Unix timestamp
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"` // Global temperature monitoring setting
} }

View file

@ -27,6 +27,7 @@ type StateSnapshot struct {
ActiveAlerts []Alert `json:"activeAlerts"` ActiveAlerts []Alert `json:"activeAlerts"`
RecentlyResolved []ResolvedAlert `json:"recentlyResolved"` RecentlyResolved []ResolvedAlert `json:"recentlyResolved"`
LastUpdate time.Time `json:"lastUpdate"` LastUpdate time.Time `json:"lastUpdate"`
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"`
} }
// GetSnapshot returns a snapshot of the current state without mutex // GetSnapshot returns a snapshot of the current state without mutex
@ -71,6 +72,7 @@ func (s *State) GetSnapshot() StateSnapshot {
ActiveAlerts: append([]Alert{}, s.ActiveAlerts...), ActiveAlerts: append([]Alert{}, s.ActiveAlerts...),
RecentlyResolved: append([]ResolvedAlert{}, s.RecentlyResolved...), RecentlyResolved: append([]ResolvedAlert{}, s.RecentlyResolved...),
LastUpdate: s.LastUpdate, LastUpdate: s.LastUpdate,
TemperatureMonitoringEnabled: s.TemperatureMonitoringEnabled,
} }
// Copy map // Copy map
@ -156,5 +158,6 @@ func (s StateSnapshot) ToFrontend() StateFrontend {
ConnectionHealth: s.ConnectionHealth, ConnectionHealth: s.ConnectionHealth,
Stats: make(map[string]any), Stats: make(map[string]any),
LastUpdate: s.LastUpdate.Unix() * 1000, // JavaScript timestamp LastUpdate: s.LastUpdate.Unix() * 1000, // JavaScript timestamp
TemperatureMonitoringEnabled: s.TemperatureMonitoringEnabled,
} }
} }

View file

@ -3224,6 +3224,9 @@ func New(cfg *config.Config) (*Monitor, error) {
m.executor = newRealExecutor(m) m.executor = newRealExecutor(m)
m.buildInstanceInfoCache(cfg) m.buildInstanceInfoCache(cfg)
// Initialize state with config values
m.state.TemperatureMonitoringEnabled = cfg.TemperatureMonitoringEnabled
if m.pollMetrics != nil { if m.pollMetrics != nil {
m.pollMetrics.ResetQueueDepth(0) m.pollMetrics.ResetQueueDepth(0)
} }
@ -4916,6 +4919,7 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
ConnectionHealth: connectionHealthStr, // Use the determined health status ConnectionHealth: connectionHealthStr, // Use the determined health status
IsClusterMember: instanceCfg.IsCluster, IsClusterMember: instanceCfg.IsCluster,
ClusterName: instanceCfg.ClusterName, ClusterName: instanceCfg.ClusterName,
TemperatureMonitoringEnabled: instanceCfg.TemperatureMonitoringEnabled,
} }
nodeSnapshotRaw := NodeMemoryRaw{ nodeSnapshotRaw := NodeMemoryRaw{
@ -5282,8 +5286,13 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
}) })
// Collect temperature data via SSH (non-blocking, best effort) // Collect temperature data via SSH (non-blocking, best effort)
// Only attempt for online nodes // Only attempt for online nodes when temperature monitoring is enabled
if node.Status == "online" && m.tempCollector != nil { // Check per-node setting first, fall back to global setting
tempMonitoringEnabled := m.config.TemperatureMonitoringEnabled
if instanceCfg.TemperatureMonitoringEnabled != nil {
tempMonitoringEnabled = *instanceCfg.TemperatureMonitoringEnabled
}
if node.Status == "online" && m.tempCollector != nil && tempMonitoringEnabled {
tempCtx, tempCancel := context.WithTimeout(ctx, 30*time.Second) // Increased to accommodate SSH operations via proxy tempCtx, tempCancel := context.WithTimeout(ctx, 30*time.Second) // Increased to accommodate SSH operations via proxy
// Determine SSH hostname to use (most robust approach): // Determine SSH hostname to use (most robust approach):