feat: add min/max temperature tracking for nodes

Track minimum and maximum CPU temperatures since monitoring started.
This provides better insight into temperature trends and cooling
adequacy over time.

Changes:
- Backend: Add CPUMin, CPUMaxRecord, MinRecorded, MaxRecorded fields
  to Temperature model
- Backend: Implement min/max tracking logic in monitoring cycle that
  preserves values across polling cycles
- Backend: Initialize min/max on first reading, update on extremes
- Frontend: Update Temperature TypeScript interface with new fields
- Frontend: Display min/max range in NodeCard tooltip (e.g., "52°C
  (48-67°C since monitoring started)")
- Frontend: Rebuild dist assets

Temperature display now shows:
- Current temperature with color coding (green/yellow/red)
- Tooltip with full min-max range and context
- Min/max tracked in-memory (resets on Pulse restart)

Example tooltip: "CPU: 52°C (48-67°C since monitoring started)"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Richard Courtman 2025-10-18 08:15:10 +00:00
parent 27db397f62
commit 97b9c6739c
7 changed files with 71 additions and 245 deletions

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View file

@ -6,7 +6,7 @@
<meta name="theme-color" content="#000000" />
<link rel="icon" type="image/svg+xml" href="/logo.svg" />
<title>Pulse</title>
<script type="module" crossorigin src="/assets/index-cdElc049.js"></script>
<script type="module" crossorigin src="/assets/index-BFQdFd5E.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-VFB0BYLW.css">
</head>
<body>

View file

@ -133,6 +133,16 @@ const NodeCard: Component<NodeCardProps> = (props) => {
if (value === null) {
return 'CPU sensor unavailable';
}
const temp = props.node.temperature;
const hasMinMax = temp && temp.cpuMin > 0 && temp.cpuMaxRecord > 0;
if (hasMinMax) {
const min = Math.round(temp.cpuMin);
const max = Math.round(temp.cpuMaxRecord);
return `CPU: ${value}°C (${min}-${max}°C since monitoring started)`;
}
return `CPU: ${value}°C`;
});

View file

@ -512,6 +512,10 @@ export interface CPUInfo {
export interface Temperature {
cpuPackage?: number; // CPU package temperature (primary metric)
cpuMax?: number; // Highest core temperature
cpuMin?: number; // Minimum recorded CPU temperature (since monitoring started)
cpuMaxRecord?: number; // Maximum recorded CPU temperature (since monitoring started)
minRecorded?: string; // When minimum temperature was recorded
maxRecorded?: string; // When maximum temperature was recorded
cores?: CoreTemp[]; // Individual core temperatures
nvme?: NVMeTemp[]; // NVMe drive temperatures
available: boolean; // Whether any temperature data is available

View file

@ -569,14 +569,18 @@ type CPUInfo struct {
// Temperature represents temperature sensors data
type Temperature struct {
CPUPackage float64 `json:"cpuPackage,omitempty"` // CPU package temperature (primary metric)
CPUMax float64 `json:"cpuMax,omitempty"` // Highest core temperature
Cores []CoreTemp `json:"cores,omitempty"` // Individual core temperatures
NVMe []NVMeTemp `json:"nvme,omitempty"` // NVMe drive temperatures
Available bool `json:"available"` // Whether any temperature data is available
HasCPU bool `json:"hasCPU"` // Whether CPU temperature data is available
HasNVMe bool `json:"hasNVMe"` // Whether NVMe temperature data is available
LastUpdate time.Time `json:"lastUpdate"` // When this data was collected
CPUPackage float64 `json:"cpuPackage,omitempty"` // CPU package temperature (primary metric)
CPUMax float64 `json:"cpuMax,omitempty"` // Highest core temperature
CPUMin float64 `json:"cpuMin,omitempty"` // Minimum recorded CPU temperature (since monitoring started)
CPUMaxRecord float64 `json:"cpuMaxRecord,omitempty"` // Maximum recorded CPU temperature (since monitoring started)
MinRecorded time.Time `json:"minRecorded,omitempty"` // When minimum temperature was recorded
MaxRecorded time.Time `json:"maxRecorded,omitempty"` // When maximum temperature was recorded
Cores []CoreTemp `json:"cores,omitempty"` // Individual core temperatures
NVMe []NVMeTemp `json:"nvme,omitempty"` // NVMe drive temperatures
Available bool `json:"available"` // Whether any temperature data is available
HasCPU bool `json:"hasCPU"` // Whether CPU temperature data is available
HasNVMe bool `json:"hasNVMe"` // Whether NVMe temperature data is available
LastUpdate time.Time `json:"lastUpdate"` // When this data was collected
}
// CoreTemp represents a CPU core temperature

View file

@ -2590,12 +2590,56 @@ func (m *Monitor) pollPVEInstance(ctx context.Context, instanceName string, clie
tempCancel()
if err == nil && temp != nil && temp.Available {
// Get the current CPU temperature (prefer package, fall back to max)
currentTemp := temp.CPUPackage
if currentTemp == 0 && temp.CPUMax > 0 {
currentTemp = temp.CPUMax
}
// Find previous temperature data for this node to preserve min/max
var prevTemp *models.Temperature
for _, prevNode := range prevInstanceNodes {
if prevNode.ID == modelNode.ID && prevNode.Temperature != nil {
prevTemp = prevNode.Temperature
break
}
}
// Initialize or update min/max tracking
if prevTemp != nil && prevTemp.CPUMin > 0 {
// Preserve existing min/max and update if necessary
temp.CPUMin = prevTemp.CPUMin
temp.CPUMaxRecord = prevTemp.CPUMaxRecord
temp.MinRecorded = prevTemp.MinRecorded
temp.MaxRecorded = prevTemp.MaxRecorded
// Update min if current is lower
if currentTemp > 0 && currentTemp < temp.CPUMin {
temp.CPUMin = currentTemp
temp.MinRecorded = time.Now()
}
// Update max if current is higher
if currentTemp > temp.CPUMaxRecord {
temp.CPUMaxRecord = currentTemp
temp.MaxRecorded = time.Now()
}
} else if currentTemp > 0 {
// First reading - initialize min/max to current value
temp.CPUMin = currentTemp
temp.CPUMaxRecord = currentTemp
temp.MinRecorded = time.Now()
temp.MaxRecorded = time.Now()
}
modelNode.Temperature = temp
log.Debug().
Str("node", node.Node).
Str("sshHost", sshHost).
Float64("cpuPackage", temp.CPUPackage).
Float64("cpuMax", temp.CPUMax).
Float64("cpuMin", temp.CPUMin).
Float64("cpuMaxRecord", temp.CPUMaxRecord).
Int("nvmeCount", len(temp.NVMe)).
Msg("Collected temperature data")
} else if err != nil {