From d79b8e8883f76b62ee9d64457493caf84fe20a0b Mon Sep 17 00:00:00 2001 From: Richard Courtman Date: Sat, 18 Oct 2025 08:57:10 +0000 Subject: [PATCH] feat: display temperature min/max range in tooltip and remove unused NodeCard MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add instant-display tooltip on temperature column showing min-max range - Color-code min/max temperatures individually (green/yellow/red) - Remove unused NodeCard.tsx component from codebase - Keep table row height consistent by using tooltip instead of inline display 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../src/components/Dashboard/NodeCard.tsx | 276 ------------------ .../components/shared/NodeSummaryTable.tsx | 26 ++ 2 files changed, 26 insertions(+), 276 deletions(-) delete mode 100644 frontend-modern/src/components/Dashboard/NodeCard.tsx diff --git a/frontend-modern/src/components/Dashboard/NodeCard.tsx b/frontend-modern/src/components/Dashboard/NodeCard.tsx deleted file mode 100644 index 0410c98..0000000 --- a/frontend-modern/src/components/Dashboard/NodeCard.tsx +++ /dev/null @@ -1,276 +0,0 @@ -import { Component, Show, createMemo, createEffect } from 'solid-js'; -import type { Node } from '@/types/api'; -import { formatUptime } from '@/utils/format'; -import { getAlertStyles, getResourceAlerts } from '@/utils/alerts'; -import { AlertIndicator, AlertCountBadge } from '@/components/shared/AlertIndicators'; -import { useWebSocket } from '@/App'; -import { Card } from '@/components/shared/Card'; -import { getNodeDisplayName, hasAlternateDisplayName } from '@/utils/nodes'; -import { getCpuTemperature } from '@/utils/temperature'; - -interface NodeCardProps { - node: Node; - isSelected?: boolean; -} - -const NodeCard: Component = (props) => { - const { activeAlerts } = useWebSocket(); - // Early return if node data is incomplete - if (!props.node || !props.node.memory || !props.node.disk) { - return ( - -
Loading node data...
-
- ); - } - - const isOnline = () => - props.node.status === 'online' && - props.node.uptime > 0 && - props.node.connectionHealth !== 'error'; - - // Memoize CPU percent to avoid multiple calculations - const cpuPercent = createMemo(() => { - const percent = Math.round(props.node.cpu * 100); - return percent; - }); - - // Track CPU updates (logging removed for cleaner output) - createEffect(() => { - cpuPercent(); // Just track the value changes - }); - - const memPercent = createMemo(() => { - if (!props.node.memory) return 0; - // Use the pre-calculated usage percentage from the backend - return Math.round(props.node.memory.usage || 0); - }); - - const diskPercent = createMemo(() => { - if (!props.node.disk || props.node.disk.total === 0) return 0; - return Math.round((props.node.disk.used / props.node.disk.total) * 100); - }); - - const displayName = () => getNodeDisplayName(props.node); - const showActualName = () => hasAlternateDisplayName(props.node); - - // Calculate normalized load (load average / cpu count) - const normalizedLoad = () => { - if (props.node.loadAverage && props.node.loadAverage.length > 0) { - const load1m = props.node.loadAverage[0]; - if (typeof load1m === 'number' && !isNaN(load1m)) { - // Use CPU cores from cpuInfo if available, otherwise assume 4 - const cpuCount = props.node.cpuInfo?.cores || 4; - return (load1m / cpuCount).toFixed(2); - } - } - return 'N/A'; - }; - - // Helper function to create compact progress bar - const createProgressBar = (percentage: number, label: string, colorClass: string) => { - const bgColorClass = 'bg-gray-200 dark:bg-gray-600'; - const progressColorClass = - { - red: 'bg-red-500/70 dark:bg-red-500/60', - yellow: 'bg-yellow-500/70 dark:bg-yellow-500/60', - green: 'bg-green-500/70 dark:bg-green-500/60', - }[colorClass] || 'bg-gray-500/70 dark:bg-gray-500/60'; - - return ( -
-
- {label} - - {percentage}% - -
-
-
-
-
- ); - }; - - // Get color based on percentage and metric type - const getColor = (percentage: number, metric: 'cpu' | 'memory' | 'disk') => { - if (metric === 'cpu') { - if (percentage >= 90) return 'red'; - if (percentage >= 80) return 'yellow'; - return 'green'; - } else if (metric === 'memory') { - if (percentage >= 85) return 'red'; - if (percentage >= 75) return 'yellow'; - return 'green'; - } else if (metric === 'disk') { - if (percentage >= 90) return 'red'; - if (percentage >= 80) return 'yellow'; - return 'green'; - } - return 'green'; - }; - - const alertStyles = getAlertStyles(props.node.id || props.node.name, activeAlerts); - const nodeAlerts = createMemo(() => - getResourceAlerts(props.node.id || props.node.name, activeAlerts), - ); - const unacknowledgedNodeAlerts = createMemo(() => nodeAlerts().filter((alert) => !alert.acknowledged)); - - const cpuTemperature = createMemo(() => getCpuTemperature(props.node.temperature)); - const cpuTemperatureValue = createMemo(() => { - const value = cpuTemperature(); - return value !== null ? Math.round(value) : null; - }); - const temperatureTooltip = createMemo(() => { - const hasCPU = props.node.temperature?.hasCPU ?? props.node.temperature?.available; - if (!hasCPU) { - return ''; - } - const value = cpuTemperatureValue(); - 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`; - }); - - // Determine border/ring style based on status and alerts - const getBorderClass = () => { - // Selected nodes get blue ring - if (props.isSelected) { - return 'ring-2 ring-blue-500 border-blue-200 dark:border-blue-500'; - } - // Offline nodes get red ring - if (!isOnline()) { - return 'ring-2 ring-red-500 border-red-200 dark:border-red-600'; - } - // Alert nodes get colored ring based on severity - if (alertStyles.hasUnacknowledgedAlert) { - return alertStyles.severity === 'critical' - ? 'ring-2 ring-red-500 border-red-200 dark:border-red-600' - : 'ring-2 ring-orange-500 border-orange-200 dark:border-orange-500'; - } - if (alertStyles.hasAcknowledgedOnlyAlert) { - return 'ring-2 ring-gray-400 border-gray-200 dark:border-gray-600 dark:ring-gray-500'; - } - // Normal nodes get standard border - return ''; - }; - - // Get background class from alert styles but remove the border-l-4 part - const getBackgroundClass = () => { - if (!alertStyles.rowClass) return ''; - // Remove border classes from rowClass to avoid conflicts - return alertStyles.rowClass.replace(/border-[^\s]+/g, '').trim(); - }; - - return ( - - {/* Header */} -
-

- - {displayName()} - - - ({props.node.name}) - - {/* Cluster/Standalone indicator - more compact */} - - - {props.node.isClusterMember ? 'C' : 'S'} - - - -
- - 1}> - - -
-
-

- -
- - {/* Metrics - Compact */} -
- {createProgressBar(cpuPercent(), 'CPU', getColor(cpuPercent(), 'cpu'))} - {createProgressBar(memPercent(), 'Memory', getColor(memPercent(), 'memory'))} - {createProgressBar(diskPercent(), 'Disk', getColor(diskPercent(), 'disk'))} -
- - {/* Footer Info - More compact */} -
- - ↑{formatUptime(props.node.uptime)} - - ⚡{normalizedLoad()} - } - > - 80 - ? 'text-red-500' - : (cpuTemperatureValue() ?? 0) > 60 - ? 'text-yellow-500' - : 'text-green-500' - }`} - title={temperatureTooltip() || undefined} - > - 🌡{cpuTemperatureValue()}°C - - -
-
- ); -}; - -export default NodeCard; diff --git a/frontend-modern/src/components/shared/NodeSummaryTable.tsx b/frontend-modern/src/components/shared/NodeSummaryTable.tsx index 5f08d23..63af253 100644 --- a/frontend-modern/src/components/shared/NodeSummaryTable.tsx +++ b/frontend-modern/src/components/shared/NodeSummaryTable.tsx @@ -619,6 +619,32 @@ export const NodeSummaryTable: Component = (props) => { : value >= 70 ? 'text-yellow-600 dark:text-yellow-400' : 'text-green-600 dark:text-green-400'; + + const temp = node!.temperature; + const hasMinMax = temp && temp.cpuMin && temp.cpuMin > 0 && temp.cpuMaxRecord && temp.cpuMaxRecord > 0; + + if (hasMinMax) { + const min = Math.round(temp.cpuMin); + const max = Math.round(temp.cpuMaxRecord); + + const getTooltipColor = (temp: number) => { + if (temp >= 80) return 'text-red-400'; + if (temp >= 70) return 'text-yellow-400'; + return 'text-green-400'; + }; + + return ( + + + {value}°C + + + + ); + } + return {value}°C; })()}