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:
parent
e4e915c8a1
commit
27f2038dab
19 changed files with 655 additions and 280 deletions
|
|
@ -1076,6 +1076,7 @@ const UnifiedBackups: Component = () => {
|
|||
{/* Unified Node Selector */}
|
||||
<UnifiedNodeSelector
|
||||
currentTab="backups"
|
||||
globalTemperatureMonitoringEnabled={state.temperatureMonitoringEnabled}
|
||||
onNodeSelect={(nodeId) => {
|
||||
setSelectedNode(nodeId);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -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')}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 don’t need temperature data or haven’t 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}>
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -527,6 +527,7 @@ const Storage: Component = () => {
|
|||
{/* Node Selector */}
|
||||
<UnifiedNodeSelector
|
||||
currentTab="storage"
|
||||
globalTemperatureMonitoringEnabled={state.temperatureMonitoringEnabled}
|
||||
onNodeSelect={handleNodeSelect}
|
||||
filteredStorage={sortedStorage()}
|
||||
searchTerm={searchTerm()}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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):
|
||||
|
|
|
|||
Loading…
Reference in a new issue