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,11 +211,12 @@ export const PveNodesTable: Component<PveNodesTableProps> = (props) => {
Physical Disks
</span>
)}
{node.type === 'pve' && node.temperature?.available && (
<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>
)}
{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>
)}
</div>
</td>
<td class="align-top px-3 py-3 whitespace-nowrap">
@ -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,11 +394,12 @@ export const PbsNodesTable: Component<PbsNodesTableProps> = (props) => {
Garbage Collection
</span>
)}
{node.type === 'pbs' && node.temperature?.available && (
<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>
)}
{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>
)}
</div>
</td>
<td class="align-top px-3 py-3 whitespace-nowrap">
@ -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

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

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

View file

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

@ -13,27 +13,28 @@ func (s *State) ToFrontend() StateFrontend {
// ToFrontend converts a Node to NodeFrontend
func (n Node) ToFrontend() NodeFrontend {
nf := NodeFrontend{
ID: n.ID,
Node: n.Name,
Name: n.Name,
DisplayName: n.DisplayName,
Instance: n.Instance,
Host: n.Host,
Status: n.Status,
Type: n.Type,
CPU: n.CPU,
Mem: n.Memory.Used,
MaxMem: n.Memory.Total,
MaxDisk: n.Disk.Total,
Uptime: n.Uptime,
LoadAverage: n.LoadAverage,
KernelVersion: n.KernelVersion,
PVEVersion: n.PVEVersion,
CPUInfo: n.CPUInfo,
LastSeen: n.LastSeen.Unix() * 1000,
ConnectionHealth: n.ConnectionHealth,
IsClusterMember: n.IsClusterMember,
ClusterName: n.ClusterName,
ID: n.ID,
Node: n.Name,
Name: n.Name,
DisplayName: n.DisplayName,
Instance: n.Instance,
Host: n.Host,
Status: n.Status,
Type: n.Type,
CPU: n.CPU,
Mem: n.Memory.Used,
MaxMem: n.Memory.Total,
MaxDisk: n.Disk.Total,
Uptime: n.Uptime,
LoadAverage: n.LoadAverage,
KernelVersion: n.KernelVersion,
PVEVersion: n.PVEVersion,
CPUInfo: n.CPUInfo,
LastSeen: n.LastSeen.Unix() * 1000,
ConnectionHealth: n.ConnectionHealth,
IsClusterMember: n.IsClusterMember,
ClusterName: n.ClusterName,
TemperatureMonitoringEnabled: n.TemperatureMonitoringEnabled,
}
// Include full Memory object if it has data

View file

@ -29,10 +29,11 @@ type State struct {
PVEBackups PVEBackups `json:"pveBackups"`
Performance Performance `json:"performance"`
ConnectionHealth map[string]bool `json:"connectionHealth"`
Stats Stats `json:"stats"`
ActiveAlerts []Alert `json:"activeAlerts"`
RecentlyResolved []ResolvedAlert `json:"recentlyResolved"`
LastUpdate time.Time `json:"lastUpdate"`
Stats Stats `json:"stats"`
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)
@ -76,9 +77,10 @@ type Node struct {
KernelVersion string `json:"kernelVersion"`
PVEVersion string `json:"pveVersion"`
CPUInfo CPUInfo `json:"cpuInfo"`
Temperature *Temperature `json:"temperature,omitempty"` // CPU/NVMe temperatures
LastSeen time.Time `json:"lastSeen"`
ConnectionHealth string `json:"connectionHealth"`
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
ClusterName string `json:"clusterName"` // Name of cluster (empty if standalone)
}

View file

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

@ -23,10 +23,11 @@ type StateSnapshot struct {
PVEBackups PVEBackups `json:"pveBackups"`
Performance Performance `json:"performance"`
ConnectionHealth map[string]bool `json:"connectionHealth"`
Stats Stats `json:"stats"`
ActiveAlerts []Alert `json:"activeAlerts"`
RecentlyResolved []ResolvedAlert `json:"recentlyResolved"`
LastUpdate time.Time `json:"lastUpdate"`
Stats Stats `json:"stats"`
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
@ -67,10 +68,11 @@ func (s *State) GetSnapshot() StateSnapshot {
PVEBackups: pveBackups,
Performance: s.Performance,
ConnectionHealth: make(map[string]bool),
Stats: s.Stats,
ActiveAlerts: append([]Alert{}, s.ActiveAlerts...),
RecentlyResolved: append([]ResolvedAlert{}, s.RecentlyResolved...),
LastUpdate: s.LastUpdate,
Stats: s.Stats,
ActiveAlerts: append([]Alert{}, s.ActiveAlerts...),
RecentlyResolved: append([]ResolvedAlert{}, s.RecentlyResolved...),
LastUpdate: s.LastUpdate,
TemperatureMonitoringEnabled: s.TemperatureMonitoringEnabled,
}
// Copy map
@ -153,8 +155,9 @@ func (s StateSnapshot) ToFrontend() StateFrontend {
Metrics: make(map[string]any),
PVEBackups: s.PVEBackups,
Performance: make(map[string]any),
ConnectionHealth: s.ConnectionHealth,
Stats: make(map[string]any),
LastUpdate: s.LastUpdate.Unix() * 1000, // JavaScript timestamp
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)
}
@ -4914,8 +4917,9 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
LoadAverage: []float64{},
LastSeen: time.Now(),
ConnectionHealth: connectionHealthStr, // Use the determined health status
IsClusterMember: instanceCfg.IsCluster,
ClusterName: instanceCfg.ClusterName,
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):