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 */}
<UnifiedNodeSelector
currentTab="backups"
globalTemperatureMonitoringEnabled={state.temperatureMonitoringEnabled}
onNodeSelect={(nodeId) => {
setSelectedNode(nodeId);
}}

View file

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

View file

@ -322,7 +322,7 @@ export const DockerHosts: Component<DockerHostsProps> = (props) => {
actions={
<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"
>
<span>Set up container agent</span>

View file

@ -10,6 +10,7 @@ type NodeConfigWithStatus = NodeConfig & {
interface PveNodesTableProps {
nodes: NodeConfigWithStatus[];
stateNodes: { instance: string; status?: string; connectionHealth?: string }[];
globalTemperatureMonitoringEnabled?: boolean;
onTestConnection: (nodeId: string) => void;
onEdit: (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 = (
node: NodeConfigWithStatus,
stateNodes: PveNodesTableProps['stateNodes'],
@ -199,7 +211,8 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
Physical Disks
</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">
Temperature
</span>
@ -261,6 +274,7 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
interface PbsNodesTableProps {
nodes: NodeConfigWithStatus[];
statePbs: { name: string; status?: string; connectionHealth?: string }[];
globalTemperatureMonitoringEnabled?: boolean;
onTestConnection: (nodeId: string) => void;
onEdit: (node: NodeConfigWithStatus) => void;
onDelete: (node: NodeConfigWithStatus) => void;
@ -380,7 +394,8 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
Garbage Collection
</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">
Temperature
</span>
@ -442,6 +457,7 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
interface PmgNodesTableProps {
nodes: NodeConfigWithStatus[];
statePmg: { name: string; status?: string; connectionHealth?: string }[];
globalTemperatureMonitoringEnabled?: boolean;
onTestConnection: (nodeId: string) => void;
onEdit: (node: NodeConfigWithStatus) => void;
onDelete: (node: NodeConfigWithStatus) => void;

View file

@ -1746,7 +1746,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
<div>
<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">
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>
</div>
<TogglePrimitive
@ -1760,7 +1760,7 @@ export const NodeModal: Component<NodeModalProps> = (props) => {
</div>
<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">
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>
</Show>
<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/agent-hub')) return 'proxmox';
if (path.includes('/settings/docker')) return 'docker';
if (path.includes('/settings/containers')) return 'docker';
if (
path.includes('/settings/hosts') ||
path.includes('/settings/host-agents') ||
@ -488,6 +489,14 @@ const Settings: Component<SettingsProps> = (props) => {
return;
}
if (path.startsWith('/settings/containers')) {
navigate(path.replace('/settings/containers', '/settings/docker'), {
replace: true,
scroll: false,
});
return;
}
if (
path.startsWith('/settings/linuxServers') ||
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') => {
if (envOverrides().discoverySubnet || savingDiscoverySettings()) {
return;
@ -2356,6 +2413,7 @@ const Settings: Component<SettingsProps> = (props) => {
<PveNodesTable
nodes={pveNodes()}
stateNodes={state.nodes ?? []}
globalTemperatureMonitoringEnabled={temperatureMonitoringEnabled()}
onTestConnection={testNodeConnection}
onEdit={(node) => {
setEditingNode(node);
@ -2642,6 +2700,7 @@ const Settings: Component<SettingsProps> = (props) => {
<PbsNodesTable
nodes={pbsNodes()}
statePbs={state.pbs ?? []}
globalTemperatureMonitoringEnabled={temperatureMonitoringEnabled()}
onTestConnection={testNodeConnection}
onEdit={(node) => {
setEditingNode(node);
@ -2928,6 +2987,7 @@ const Settings: Component<SettingsProps> = (props) => {
<PmgNodesTable
nodes={pmgNodes()}
statePmg={state.pmg ?? []}
globalTemperatureMonitoringEnabled={temperatureMonitoringEnabled()}
onTestConnection={testNodeConnection}
onEdit={(node) => {
setEditingNode(nodes().find((n) => n.id === node.id) ?? null);
@ -6572,10 +6632,18 @@ const Settings: Component<SettingsProps> = (props) => {
nodeType="pve"
editingNode={editingNode()?.type === 'pve' ? (editingNode() ?? undefined) : undefined}
securityStatus={securityStatus() ?? undefined}
temperatureMonitoringEnabled={temperatureMonitoringEnabled()}
temperatureMonitoringEnabled={
editingNode()?.temperatureMonitoringEnabled !== undefined
? editingNode()!.temperatureMonitoringEnabled
: temperatureMonitoringEnabled()
}
temperatureMonitoringLocked={temperatureMonitoringLocked()}
savingTemperatureSetting={savingTemperatureSetting()}
onToggleTemperatureMonitoring={handleTemperatureMonitoringChange}
onToggleTemperatureMonitoring={
editingNode()?.id
? (enabled: boolean) => handleNodeTemperatureMonitoringChange(editingNode()!.id, enabled)
: handleTemperatureMonitoringChange
}
onSave={async (nodeData) => {
try {
if (editingNode() && editingNode()!.id) {
@ -6638,6 +6706,18 @@ const Settings: Component<SettingsProps> = (props) => {
nodeType="pbs"
editingNode={editingNode()?.type === 'pbs' ? (editingNode() ?? undefined) : 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) => {
try {
if (editingNode() && editingNode()!.id) {
@ -6698,6 +6778,18 @@ const Settings: Component<SettingsProps> = (props) => {
nodeType="pmg"
editingNode={editingNode()?.type === 'pmg' ? (editingNode() ?? undefined) : 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) => {
try {
if (editingNode() && editingNode()!.id) {

View file

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

View file

@ -18,6 +18,7 @@ interface NodeSummaryTableProps {
backupCounts?: Record<string, number>;
currentTab: 'dashboard' | 'storage' | 'backups';
selectedNode: string | null;
globalTemperatureMonitoringEnabled?: boolean;
onNodeClick: (nodeId: string, nodeType: 'pve' | 'pbs') => void;
}
@ -25,6 +26,16 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
const { activeAlerts, state } = useWebSocket();
const alertsActivation = useAlertsActivation();
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 SortKey =
| 'default'
@ -69,7 +80,12 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
});
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 ?? ''}`;
@ -612,7 +628,8 @@ export const NodeSummaryTable: Component<NodeSummaryTableProps> = (props) => {
online &&
isPVE &&
cpuTemperatureValue !== null &&
(node!.temperature?.hasCPU ?? node!.temperature?.available)
(node!.temperature?.hasCPU ?? node!.temperature?.available) &&
isTemperatureMonitoringEnabled(node!)
}
fallback={
<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 {
currentTab: 'dashboard' | 'storage' | 'backups';
globalTemperatureMonitoringEnabled?: boolean;
onNodeSelect?: (nodeId: string | null, nodeType: 'pve' | 'pbs' | null) => void;
onNamespaceSelect?: (namespace: string) => void;
nodes?: Node[];
@ -108,6 +109,7 @@ export const UnifiedNodeSelector: Component<UnifiedNodeSelectorProps> = (props)
backupCounts={backupCounts()}
currentTab={props.currentTab}
selectedNode={selectedNode()}
globalTemperatureMonitoringEnabled={props.globalTemperatureMonitoringEnabled}
onNodeClick={handleNodeClick}
/>
</div>

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -29,6 +29,7 @@ type NodeFrontend struct {
ConnectionHealth string `json:"connectionHealth"`
IsClusterMember bool `json:"isClusterMember,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
@ -441,4 +442,5 @@ type StateFrontend struct {
ConnectionHealth map[string]bool `json:"connectionHealth"` // Keep as is
Stats map[string]any `json:"stats"` // Empty object for now
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"`
RecentlyResolved []ResolvedAlert `json:"recentlyResolved"`
LastUpdate time.Time `json:"lastUpdate"`
TemperatureMonitoringEnabled bool `json:"temperatureMonitoringEnabled"`
}
// GetSnapshot returns a snapshot of the current state without mutex
@ -71,6 +72,7 @@ func (s *State) GetSnapshot() StateSnapshot {
ActiveAlerts: append([]Alert{}, s.ActiveAlerts...),
RecentlyResolved: append([]ResolvedAlert{}, s.RecentlyResolved...),
LastUpdate: s.LastUpdate,
TemperatureMonitoringEnabled: s.TemperatureMonitoringEnabled,
}
// Copy map
@ -156,5 +158,6 @@ func (s StateSnapshot) ToFrontend() StateFrontend {
ConnectionHealth: s.ConnectionHealth,
Stats: make(map[string]any),
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.buildInstanceInfoCache(cfg)
// Initialize state with config values
m.state.TemperatureMonitoringEnabled = cfg.TemperatureMonitoringEnabled
if m.pollMetrics != nil {
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
IsClusterMember: instanceCfg.IsCluster,
ClusterName: instanceCfg.ClusterName,
TemperatureMonitoringEnabled: instanceCfg.TemperatureMonitoringEnabled,
}
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)
// Only attempt for online nodes
if node.Status == "online" && m.tempCollector != nil {
// Only attempt for online nodes when temperature monitoring is enabled
// 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
// Determine SSH hostname to use (most robust approach):