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,11 +211,12 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
Physical Disks Physical Disks
</span> </span>
)} )}
{node.type === 'pve' && node.temperature?.available && ( {node.type === 'pve' &&
<span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded"> isTemperatureMonitoringEnabled(node, props.globalTemperatureMonitoringEnabled ?? true) && (
Temperature <span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded">
</span> Temperature
)} </span>
)}
</div> </div>
</td> </td>
<td class="align-top px-3 py-3 whitespace-nowrap"> <td class="align-top px-3 py-3 whitespace-nowrap">
@ -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,11 +394,12 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
Garbage Collection Garbage Collection
</span> </span>
)} )}
{node.type === 'pbs' && node.temperature?.available && ( {node.type === 'pbs' &&
<span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded"> isTemperatureMonitoringEnabled(node, props.globalTemperatureMonitoringEnabled ?? true) && (
Temperature <span class="text-xs px-2 py-1 bg-green-100 dark:bg-green-900 text-green-700 dark:text-green-300 rounded">
</span> Temperature
)} </span>
)}
</div> </div>
</td> </td>
<td class="align-top px-3 py-3 whitespace-nowrap"> <td class="align-top px-3 py-3 whitespace-nowrap">
@ -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

@ -363,29 +363,30 @@ func (h *ConfigHandlers) maybeRefreshClusterInfo(instance *config.PVEInstance) {
// NodeConfigRequest represents a request to add/update a node // NodeConfigRequest represents a request to add/update a node
type NodeConfigRequest struct { type NodeConfigRequest struct {
Type string `json:"type"` // "pve", "pbs", or "pmg" Type string `json:"type"` // "pve", "pbs", or "pmg"
Name string `json:"name"` Name string `json:"name"`
Host string `json:"host"` Host string `json:"host"`
User string `json:"user,omitempty"` User string `json:"user,omitempty"`
Password string `json:"password,omitempty"` Password string `json:"password,omitempty"`
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
@ -403,9 +404,10 @@ type NodeResponse struct {
MonitorVMs bool `json:"monitorVMs,omitempty"` MonitorVMs bool `json:"monitorVMs,omitempty"`
MonitorContainers bool `json:"monitorContainers,omitempty"` MonitorContainers bool `json:"monitorContainers,omitempty"`
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"`
MonitorDatastores bool `json:"monitorDatastores,omitempty"` TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,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"`
MonitorPruneJobs bool `json:"monitorPruneJobs,omitempty"` MonitorPruneJobs bool `json:"monitorPruneJobs,omitempty"`
@ -731,25 +733,26 @@ func (h *ConfigHandlers) GetAllNodesForAPI() []NodeResponse {
h.maybeRefreshClusterInfo(&h.config.PVEInstances[i]) h.maybeRefreshClusterInfo(&h.config.PVEInstances[i])
pve = h.config.PVEInstances[i] pve = h.config.PVEInstances[i]
node := NodeResponse{ node := NodeResponse{
ID: generateNodeID("pve", i), ID: generateNodeID("pve", i),
Type: "pve", Type: "pve",
Name: pve.Name, Name: pve.Name,
Host: pve.Host, Host: pve.Host,
User: pve.User, User: pve.User,
HasPassword: pve.Password != "", HasPassword: pve.Password != "",
TokenName: pve.TokenName, TokenName: pve.TokenName,
HasToken: pve.TokenValue != "", HasToken: pve.TokenValue != "",
Fingerprint: pve.Fingerprint, Fingerprint: pve.Fingerprint,
VerifySSL: pve.VerifySSL, VerifySSL: pve.VerifySSL,
MonitorVMs: pve.MonitorVMs, MonitorVMs: pve.MonitorVMs,
MonitorContainers: pve.MonitorContainers, MonitorContainers: pve.MonitorContainers,
MonitorStorage: pve.MonitorStorage, MonitorStorage: pve.MonitorStorage,
MonitorBackups: pve.MonitorBackups, MonitorBackups: pve.MonitorBackups,
MonitorPhysicalDisks: pve.MonitorPhysicalDisks, MonitorPhysicalDisks: pve.MonitorPhysicalDisks,
Status: h.getNodeStatus("pve", pve.Name), TemperatureMonitoringEnabled: pve.TemperatureMonitoringEnabled,
IsCluster: pve.IsCluster, Status: h.getNodeStatus("pve", pve.Name),
ClusterName: pve.ClusterName, IsCluster: pve.IsCluster,
ClusterEndpoints: pve.ClusterEndpoints, ClusterName: pve.ClusterName,
ClusterEndpoints: pve.ClusterEndpoints,
} }
nodes = append(nodes, node) nodes = append(nodes, node)
} }
@ -757,22 +760,23 @@ func (h *ConfigHandlers) GetAllNodesForAPI() []NodeResponse {
// Add PBS nodes // Add PBS nodes
for i, pbs := range h.config.PBSInstances { for i, pbs := range h.config.PBSInstances {
node := NodeResponse{ node := NodeResponse{
ID: generateNodeID("pbs", i), ID: generateNodeID("pbs", i),
Type: "pbs", Type: "pbs",
Name: pbs.Name, Name: pbs.Name,
Host: pbs.Host, Host: pbs.Host,
User: pbs.User, User: pbs.User,
HasPassword: pbs.Password != "", HasPassword: pbs.Password != "",
TokenName: pbs.TokenName, TokenName: pbs.TokenName,
HasToken: pbs.TokenValue != "", HasToken: pbs.TokenValue != "",
Fingerprint: pbs.Fingerprint, Fingerprint: pbs.Fingerprint,
VerifySSL: pbs.VerifySSL, VerifySSL: pbs.VerifySSL,
MonitorDatastores: pbs.MonitorDatastores, TemperatureMonitoringEnabled: pbs.TemperatureMonitoringEnabled,
MonitorSyncJobs: pbs.MonitorSyncJobs, MonitorDatastores: pbs.MonitorDatastores,
MonitorVerifyJobs: pbs.MonitorVerifyJobs, MonitorSyncJobs: pbs.MonitorSyncJobs,
MonitorPruneJobs: pbs.MonitorPruneJobs, MonitorVerifyJobs: pbs.MonitorVerifyJobs,
MonitorGarbageJobs: pbs.MonitorGarbageJobs, MonitorPruneJobs: pbs.MonitorPruneJobs,
Status: h.getNodeStatus("pbs", pbs.Name), MonitorGarbageJobs: pbs.MonitorGarbageJobs,
Status: h.getNodeStatus("pbs", pbs.Name),
} }
nodes = append(nodes, node) nodes = append(nodes, node)
} }
@ -785,21 +789,22 @@ func (h *ConfigHandlers) GetAllNodesForAPI() []NodeResponse {
} }
node := NodeResponse{ node := NodeResponse{
ID: generateNodeID("pmg", i), ID: generateNodeID("pmg", i),
Type: "pmg", Type: "pmg",
Name: pmgInst.Name, Name: pmgInst.Name,
Host: pmgInst.Host, Host: pmgInst.Host,
User: pmgInst.User, User: pmgInst.User,
HasPassword: pmgInst.Password != "", HasPassword: pmgInst.Password != "",
TokenName: pmgInst.TokenName, TokenName: pmgInst.TokenName,
HasToken: pmgInst.TokenValue != "", HasToken: pmgInst.TokenValue != "",
Fingerprint: pmgInst.Fingerprint, Fingerprint: pmgInst.Fingerprint,
VerifySSL: pmgInst.VerifySSL, VerifySSL: pmgInst.VerifySSL,
MonitorMailStats: monitorMailStats, TemperatureMonitoringEnabled: pmgInst.TemperatureMonitoringEnabled,
MonitorQueues: pmgInst.MonitorQueues, MonitorMailStats: monitorMailStats,
MonitorQuarantine: pmgInst.MonitorQuarantine, MonitorQueues: pmgInst.MonitorQueues,
MonitorDomainStats: pmgInst.MonitorDomainStats, MonitorQuarantine: pmgInst.MonitorQuarantine,
Status: h.getNodeStatus("pmg", pmgInst.Name), MonitorDomainStats: pmgInst.MonitorDomainStats,
Status: h.getNodeStatus("pmg", pmgInst.Name),
} }
nodes = append(nodes, node) nodes = append(nodes, node)
} }
@ -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,23 +1162,46 @@ 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
User: req.User, User: req.User,
Password: req.Password, Password: req.Password,
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,
IsCluster: isCluster, TemperatureMonitoringEnabled: req.TemperatureMonitoringEnabled,
ClusterName: clusterName, IsCluster: isCluster,
ClusterEndpoints: clusterEndpoints, ClusterName: clusterName,
ClusterEndpoints: clusterEndpoints,
} }
h.config.PVEInstances = append(h.config.PVEInstances, pve) h.config.PVEInstances = append(h.config.PVEInstances, pve)
@ -1221,21 +1253,52 @@ 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,
User: pbsUser, User: pbsUser,
Password: pbsPassword, Password: pbsPassword,
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,24 +1332,53 @@ 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{
Name: req.Name, Name: req.Name,
Host: host, Host: host,
User: pmgUser, User: pmgUser,
Password: pmgPassword, Password: pmgPassword,
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
pve.MonitorPhysicalDisks = req.MonitorPhysicalDisks }
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
}
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.MonitorBackups = true // Enable by default for PBS pbs.VerifySSL = *req.VerifySSL
pbs.MonitorDatastores = req.MonitorDatastores }
pbs.MonitorSyncJobs = req.MonitorSyncJobs if req.MonitorBackups != nil {
pbs.MonitorVerifyJobs = req.MonitorVerifyJobs pbs.MonitorBackups = *req.MonitorBackups
pbs.MonitorPruneJobs = req.MonitorPruneJobs } else {
pbs.MonitorGarbageJobs = req.MonitorGarbageJobs pbs.MonitorBackups = true // Enable by default for PBS
}
if req.MonitorDatastores != nil {
pbs.MonitorDatastores = *req.MonitorDatastores
}
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

@ -81,17 +81,18 @@ type ConfigResponse struct {
// NodeConfig represents a node configuration // NodeConfig represents a node configuration
type NodeConfig struct { type NodeConfig struct {
ID string `json:"id"` ID string `json:"id"`
Name string `json:"name"` Name string `json:"name"`
Type string `json:"type"` Type string `json:"type"`
Address string `json:"address"` Address string `json:"address"`
Port int `json:"port,omitempty"` Port int `json:"port,omitempty"`
Username string `json:"username,omitempty"` Username string `json:"username,omitempty"`
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"`
Tags []string `json:"tags,omitempty"` TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"` Tags []string `json:"tags,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
} }
// SettingsConfig represents application settings // SettingsConfig represents application settings

View file

@ -385,20 +385,21 @@ func splitAndTrim(value string) []string {
// PVEInstance represents a Proxmox VE connection // PVEInstance represents a Proxmox VE connection
type PVEInstance struct { type PVEInstance struct {
Name string Name string
Host string // Primary endpoint (user-provided) Host string // Primary endpoint (user-provided)
User string User string
Password string Password string
TokenName string TokenName string
TokenValue string TokenValue string
Fingerprint string Fingerprint string
VerifySSL bool VerifySSL bool
MonitorVMs bool MonitorVMs bool
MonitorContainers bool MonitorContainers bool
MonitorStorage bool MonitorStorage bool
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
@ -418,20 +419,21 @@ type ClusterEndpoint struct {
// PBSInstance represents a Proxmox Backup Server connection // PBSInstance represents a Proxmox Backup Server connection
type PBSInstance struct { type PBSInstance struct {
Name string Name string
Host string Host string
User string User string
Password string Password string
TokenName string TokenName string
TokenValue string TokenValue string
Fingerprint string Fingerprint string
VerifySSL bool VerifySSL bool
MonitorBackups bool MonitorBackups bool
MonitorDatastores bool MonitorDatastores bool
MonitorSyncJobs bool MonitorSyncJobs bool
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
@ -445,10 +447,11 @@ type PMGInstance struct {
Fingerprint string Fingerprint string
VerifySSL bool VerifySSL bool
MonitorMailStats bool MonitorMailStats bool
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

@ -13,27 +13,28 @@ func (s *State) ToFrontend() StateFrontend {
// ToFrontend converts a Node to NodeFrontend // ToFrontend converts a Node to NodeFrontend
func (n Node) ToFrontend() NodeFrontend { func (n Node) ToFrontend() NodeFrontend {
nf := NodeFrontend{ nf := NodeFrontend{
ID: n.ID, ID: n.ID,
Node: n.Name, Node: n.Name,
Name: n.Name, Name: n.Name,
DisplayName: n.DisplayName, DisplayName: n.DisplayName,
Instance: n.Instance, Instance: n.Instance,
Host: n.Host, Host: n.Host,
Status: n.Status, Status: n.Status,
Type: n.Type, Type: n.Type,
CPU: n.CPU, CPU: n.CPU,
Mem: n.Memory.Used, Mem: n.Memory.Used,
MaxMem: n.Memory.Total, MaxMem: n.Memory.Total,
MaxDisk: n.Disk.Total, MaxDisk: n.Disk.Total,
Uptime: n.Uptime, Uptime: n.Uptime,
LoadAverage: n.LoadAverage, LoadAverage: n.LoadAverage,
KernelVersion: n.KernelVersion, KernelVersion: n.KernelVersion,
PVEVersion: n.PVEVersion, PVEVersion: n.PVEVersion,
CPUInfo: n.CPUInfo, CPUInfo: n.CPUInfo,
LastSeen: n.LastSeen.Unix() * 1000, LastSeen: n.LastSeen.Unix() * 1000,
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

@ -29,10 +29,11 @@ type State struct {
PVEBackups PVEBackups `json:"pveBackups"` PVEBackups PVEBackups `json:"pveBackups"`
Performance Performance `json:"performance"` Performance Performance `json:"performance"`
ConnectionHealth map[string]bool `json:"connectionHealth"` ConnectionHealth map[string]bool `json:"connectionHealth"`
Stats Stats `json:"stats"` Stats Stats `json:"stats"`
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)
@ -76,9 +77,10 @@ type Node struct {
KernelVersion string `json:"kernelVersion"` KernelVersion string `json:"kernelVersion"`
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
LastSeen time.Time `json:"lastSeen"` TemperatureMonitoringEnabled *bool `json:"temperatureMonitoringEnabled,omitempty"` // Per-node temperature monitoring override
ConnectionHealth string `json:"connectionHealth"` LastSeen time.Time `json:"lastSeen"`
ConnectionHealth string `json:"connectionHealth"`
IsClusterMember bool `json:"isClusterMember"` // True if part of a cluster IsClusterMember bool `json:"isClusterMember"` // True if part of a cluster
ClusterName string `json:"clusterName"` // Name of cluster (empty if standalone) ClusterName string `json:"clusterName"` // Name of cluster (empty if standalone)
} }

View file

@ -26,9 +26,10 @@ type NodeFrontend struct {
CPUInfo CPUInfo `json:"cpuInfo"` CPUInfo CPUInfo `json:"cpuInfo"`
Temperature *Temperature `json:"temperature,omitempty"` // CPU/NVMe temperatures Temperature *Temperature `json:"temperature,omitempty"` // CPU/NVMe temperatures
LastSeen int64 `json:"lastSeen"` // Unix timestamp LastSeen int64 `json:"lastSeen"` // Unix timestamp
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
@ -438,7 +439,8 @@ type StateFrontend struct {
Metrics map[string]any `json:"metrics"` // Empty object for now Metrics map[string]any `json:"metrics"` // Empty object for now
PVEBackups PVEBackups `json:"pveBackups"` // Keep as is PVEBackups PVEBackups `json:"pveBackups"` // Keep as is
Performance map[string]any `json:"performance"` // Empty object for now Performance map[string]any `json:"performance"` // Empty object for now
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

@ -23,10 +23,11 @@ type StateSnapshot struct {
PVEBackups PVEBackups `json:"pveBackups"` PVEBackups PVEBackups `json:"pveBackups"`
Performance Performance `json:"performance"` Performance Performance `json:"performance"`
ConnectionHealth map[string]bool `json:"connectionHealth"` ConnectionHealth map[string]bool `json:"connectionHealth"`
Stats Stats `json:"stats"` Stats Stats `json:"stats"`
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
@ -67,10 +68,11 @@ func (s *State) GetSnapshot() StateSnapshot {
PVEBackups: pveBackups, PVEBackups: pveBackups,
Performance: s.Performance, Performance: s.Performance,
ConnectionHealth: make(map[string]bool), ConnectionHealth: make(map[string]bool),
Stats: s.Stats, Stats: s.Stats,
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
@ -153,8 +155,9 @@ func (s StateSnapshot) ToFrontend() StateFrontend {
Metrics: make(map[string]any), Metrics: make(map[string]any),
PVEBackups: s.PVEBackups, PVEBackups: s.PVEBackups,
Performance: make(map[string]any), Performance: make(map[string]any),
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)
} }
@ -4914,8 +4917,9 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
LoadAverage: []float64{}, LoadAverage: []float64{},
LastSeen: time.Now(), LastSeen: time.Now(),
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):