diff --git a/.gitignore b/.gitignore index 7c4bdc1fb..865aafe8e 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ deno.lock .DS_Store .idea .dev +teste-temp/ diff --git a/bin/core/single-arch.Dockerfile b/bin/core/single-arch.Dockerfile index ce2b780f6..64fe2f893 100644 --- a/bin/core/single-arch.Dockerfile +++ b/bin/core/single-arch.Dockerfile @@ -8,6 +8,7 @@ FROM ${BINARIES_IMAGE} AS binaries # Build UI FROM node:22.12-alpine AS ui-builder +ARG CACHE_BUST=1 WORKDIR /builder COPY ./ui ./ui COPY ./client/core/ts ./client diff --git a/bin/core/src/monitor/record.rs b/bin/core/src/monitor/record.rs index 3d3eb626c..7d218043e 100644 --- a/bin/core/src/monitor/record.rs +++ b/bin/core/src/monitor/record.rs @@ -20,6 +20,7 @@ pub async fn record_server_stats(ts: i64) { ts, sid: status.id.clone(), cpu_perc: stats.cpu_perc, + cpu_temp: stats.cpu_temp, load_average: stats.load_average.clone(), mem_total_gb: stats.mem_total_gb, mem_used_gb: stats.mem_used_gb, diff --git a/bin/periphery/src/stats.rs b/bin/periphery/src/stats.rs index 2a82a3791..b5708ef89 100644 --- a/bin/periphery/src/stats.rs +++ b/bin/periphery/src/stats.rs @@ -39,6 +39,7 @@ pub struct StatsClient { system: sysinfo::System, disks: sysinfo::Disks, networks: sysinfo::Networks, + components: sysinfo::Components, } const BYTES_PER_GB: f64 = 1073741824.0; @@ -50,6 +51,7 @@ impl Default for StatsClient { let system = sysinfo::System::new_all(); let disks = sysinfo::Disks::new_with_refreshed_list(); let networks = sysinfo::Networks::new_with_refreshed_list(); + let components = sysinfo::Components::new_with_refreshed_list(); let stats = SystemStats { polling_rate: periphery_config().stats_polling_rate, ..Default::default() @@ -59,6 +61,7 @@ impl Default for StatsClient { system, disks, networks, + components, stats, } } @@ -75,6 +78,7 @@ impl StatsClient { ); self.disks.refresh(true); self.networks.refresh(true); + self.components.refresh(true); } pub fn get_system_stats(&self) -> SystemStats { @@ -91,8 +95,21 @@ impl StatsClient { let load_avg = System::load_average(); + let cpu_temp = self + .components + .iter() + .filter(|c| { + let label = c.label().to_lowercase(); + label.contains("core") + || label.contains("package") + || label.contains("cpu") + }) + .filter_map(|c| c.temperature()) + .max_by(|a, b| a.partial_cmp(b).unwrap_or(Ordering::Equal)); + SystemStats { cpu_perc: self.system.global_cpu_usage(), + cpu_temp, load_average: SystemLoadAverage { one: load_avg.one, five: load_avg.five, diff --git a/client/core/rs/src/entities/stats.rs b/client/core/rs/src/entities/stats.rs index da22cb9c1..71a6cf074 100644 --- a/client/core/rs/src/entities/stats.rs +++ b/client/core/rs/src/entities/stats.rs @@ -43,6 +43,9 @@ pub struct SystemStatsRecord { // basic stats /// Cpu usage percentage pub cpu_perc: f32, + /// CPU temperature in Celsius + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cpu_temp: Option, /// Load average (1m, 5m, 15m) #[serde(default)] pub load_average: SystemLoadAverage, @@ -74,6 +77,9 @@ pub struct SystemStatsRecord { pub struct SystemStats { /// Cpu usage percentage pub cpu_perc: f32, + /// CPU temperature in Celsius + #[serde(default, skip_serializing_if = "Option::is_none")] + pub cpu_temp: Option, /// Load average (1m, 5m, 15m) #[serde(default)] pub load_average: SystemLoadAverage, diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index cb7f0c110..01e4528dd 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -2825,6 +2825,8 @@ export interface SingleDiskUsage { export interface SystemStats { /** Cpu usage percentage */ cpu_perc: number; + /** CPU temperature in Celsius */ + cpu_temp?: number; /** Load average (1m, 5m, 15m) */ load_average?: SystemLoadAverage; /** @@ -7916,6 +7918,8 @@ export interface SystemStatsRecord { sid: string; /** Cpu usage percentage */ cpu_perc: number; + /** CPU temperature in Celsius */ + cpu_temp?: number; /** Load average (1m, 5m, 15m) */ load_average?: SystemLoadAverage; /** Memory used in GB */ diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 244a4c6fc..5078a38ec 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -2951,6 +2951,8 @@ export interface SingleDiskUsage { export interface SystemStats { /** Cpu usage percentage */ cpu_perc: number; + /** CPU temperature in Celsius */ + cpu_temp?: number; /** Load average (1m, 5m, 15m) */ load_average?: SystemLoadAverage; /** @@ -7572,6 +7574,8 @@ export interface SystemStatsRecord { sid: string; /** Cpu usage percentage */ cpu_perc: number; + /** CPU temperature in Celsius */ + cpu_temp?: number; /** Load average (1m, 5m, 15m) */ load_average?: SystemLoadAverage; /** Memory used in GB */ diff --git a/ui/src/index.scss b/ui/src/index.scss index 343ea225e..6cb67c1a9 100644 --- a/ui/src/index.scss +++ b/ui/src/index.scss @@ -16,3 +16,63 @@ body[data-mantine-color-scheme="dark"] { width: 5px; border-radius: var(--mantine-radius-sm); } + +.mantine-Table-table { + th:first-child:has(.mantine-Checkbox-root), + td:first-child:has(.mantine-Checkbox-root) { + width: 45px !important; + min-width: 45px !important; + max-width: 45px !important; + padding: 0 !important; + text-align: center; + > * { + display: flex; + justify-content: center; + } + } +} + +.monitoring-stats-table { + table { + table-layout: fixed; + width: 100%; + } + + th:nth-child(2), + td:nth-child(2) { + width: 240px !important; + min-width: 240px !important; + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; + } + + th:nth-child(3), td:nth-child(3), + th:nth-child(4), td:nth-child(4), + th:nth-child(5), td:nth-child(5), + th:nth-child(6), td:nth-child(6) { + width: auto !important; + } + + th:nth-child(7), + td:nth-child(7) { + width: 210px !important; + min-width: 210px !important; + } + + th:nth-child(8), + td:nth-child(8) { + width: 120px !important; + min-width: 120px !important; + } + + th:nth-child(9), + td:nth-child(9) { + width: 121px !important; + min-width: 121px !important; + } + + .mantine-UnstyledButton-root > .mantine-Group-root > .mantine-Group-root { + min-width: 0 !important; + } +} diff --git a/ui/src/lib/icons.ts b/ui/src/lib/icons.ts index 9779c11de..1423f1dbf 100644 --- a/ui/src/lib/icons.ts +++ b/ui/src/lib/icons.ts @@ -69,6 +69,7 @@ import { Tag, Tags, Terminal, + Thermometer, Trash, Trash2, TriangleAlert, @@ -121,6 +122,7 @@ export const ICONS = { // Device IP: Globe, Cpu, + Temperature: Thermometer, LoadAvg: ChartLine, Memory: MemoryStick, Disk: Database, diff --git a/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index 45743b1d3..630d6c899 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -175,7 +175,7 @@ export const ServerComponents: RequiredResourceComponents< }} style={{ cursor: "pointer" }} > - + {publicIp ?? "Unknown IP"} @@ -198,7 +198,7 @@ export const ServerComponents: RequiredResourceComponents< - + {coreCount ? `${coreCount} Core${coreCount === 1 ? "" : "s"}` : "N/A"} @@ -210,6 +210,34 @@ export const ServerComponents: RequiredResourceComponents< ); }, + Temperature: ({ id }) => { + const isServerAvailable = useIsServerAvailable(id); + const stats = useRead( + "GetSystemStats", + { server: id }, + { + enabled: isServerAvailable, + refetchInterval: 5000, + }, + ).data; + + const temp = stats?.cpu_temp; + + return ( + + + + + {temp !== undefined ? `${temp.toFixed(1)}°C` : "N/A"} + + + + CPU Temperature:{" "} + {temp !== undefined ? `${temp.toFixed(1)}°C` : "N/A"} + + + ); + }, LoadAvg: ({ id }) => { const isServerAvailable = useIsServerAvailable(id); const stats = useRead( @@ -227,7 +255,7 @@ export const ServerComponents: RequiredResourceComponents< - + {one?.toFixed(2) ?? "N/A"} @@ -251,7 +279,7 @@ export const ServerComponents: RequiredResourceComponents< - + {stats?.mem_total_gb.toFixed(2).concat(" GB") ?? "N/A"} @@ -279,7 +307,7 @@ export const ServerComponents: RequiredResourceComponents< - + {diskTotalGb?.toFixed(2).concat(" GB") ?? "N/A"} diff --git a/ui/src/resources/server/stats-card.tsx b/ui/src/resources/server/stats-card.tsx index 23c118050..bea72cf3d 100644 --- a/ui/src/resources/server/stats-card.tsx +++ b/ui/src/resources/server/stats-card.tsx @@ -136,6 +136,7 @@ export default function ServerStatsCard({ id }: ServerStatsCardProps) { key={item.label} isUnreachable={isUnreachable || isDisabled} intention={intention} + cpuTemp={item.label === "CPU" ? stats?.cpu_temp : undefined} {...item} /> ))} @@ -166,6 +167,7 @@ function StatItem({ type, isUnreachable, intention, + cpuTemp, }: { icon: LucideIcon; label: string; @@ -176,6 +178,7 @@ function StatItem({ percentage: number, type: "cpu" | "memory" | "disk", ) => ColorIntention; + cpuTemp?: number; }) { return ( @@ -191,7 +194,11 @@ function StatItem({ : hexColorByIntention(intention(percentage, type)) } > - {isUnreachable ? "N/A" : `${percentage.toFixed(1)}%`} + {isUnreachable + ? "N/A" + : type === "cpu" && cpuTemp !== undefined + ? `${cpuTemp.toFixed(0)}°C | ${percentage.toFixed(1)}%` + : `${percentage.toFixed(1)}%`} diff --git a/ui/src/resources/server/stats/current/index.tsx b/ui/src/resources/server/stats/current/index.tsx index dfe4d1a50..55f93d887 100644 --- a/ui/src/resources/server/stats/current/index.tsx +++ b/ui/src/resources/server/stats/current/index.tsx @@ -30,6 +30,13 @@ export default function ServerCurrentStats({ } + description={ + stats?.cpu_temp !== undefined && ( + <> + Temperature: {stats.cpu_temp.toFixed(1)}°C + + ) + } percentage={stats?.cpu_perc} warning={server?.config?.cpu_warning} critical={server?.config?.cpu_critical} diff --git a/ui/src/resources/server/table/stats.tsx b/ui/src/resources/server/table/stats.tsx index 221cd091d..ed2b9bafd 100644 --- a/ui/src/resources/server/table/stats.tsx +++ b/ui/src/resources/server/table/stats.tsx @@ -1,23 +1,28 @@ import { useSelectedResources } from "@/lib/hooks"; import ResourceLink from "@/resources/link"; import { DataTable, fmtRateBytes, SortableHeader } from "mogh_ui"; -import { BoxProps, Group, Text } from "@mantine/core"; +import { BoxProps, Text, Group } from "@mantine/core"; import { Types } from "komodo_client"; import { useServerStats, useServerThresholds } from "@/resources/server/hooks"; -import { StatCell } from "mogh_ui"; +import StatCell from "@/ui/stat-cell"; import ServerVersion from "@/resources/server/version"; import ServerDiskUsage from "../diskUsage"; +import { ICONS } from "@/lib/icons"; export default function StatsServerTable({ resources, + noBorder, ...boxProps }: { resources: Types.ServerListItem[]; + noBorder?: boolean; } & BoxProps) { const [_, setSelectedResources] = useSelectedResources("Server"); return ( ( ), - cell: ({ row }) => ( - - ), + cell: ({ row }) => , }, { header: "CPU", - size: 200, + size: 170, cell: ({ row }) => , }, + { + header: "Temp", + size: 170, + cell: ({ row }) => , + }, { header: "Memory", - size: 200, + size: 170, cell: ({ row }) => , }, { header: "Disk", - size: 200, + size: 170, cell: ({ row }) => , }, { header: "Load Avg", - size: 160, + size: 210, cell: ({ row }) => , }, { header: "Net", - size: 100, + size: 120, cell: ({ row }) => , }, { header: "Version", - size: 160, + size: 121, cell: ({ row }) => , }, ]} @@ -72,39 +80,74 @@ export default function StatsServerTable({ function CpuCell({ id }: { id: string }) { const stats = useServerStats(id); - const cpu = stats?.cpu_perc ?? 0; - const { cpuWarning: warning, cpuCritical: critical } = - useServerThresholds(id); + const thresholds = useServerThresholds(id); + const value = stats?.cpu_perc ?? 0; + + const intent: "Good" | "Warning" | "Critical" = + value < thresholds.cpuWarning + ? "Good" + : value < thresholds.cpuCritical + ? "Warning" + : "Critical"; + + return ; +} + +function TempCell({ id }: { id: string }) { + const stats = useServerStats(id); + const value = stats?.cpu_temp; + const intent: "Good" | "Warning" | "Critical" = - cpu < warning ? "Good" : cpu < critical ? "Warning" : "Critical"; - return ; + value === undefined + ? "Good" + : value < 65 + ? "Good" + : value < 80 + ? "Warning" + : "Critical"; + + return ( + + ); } function MemCell({ id }: { id: string }) { const stats = useServerStats(id); + const thresholds = useServerThresholds(id); + const used = stats?.mem_used_gb ?? 0; const total = stats?.mem_total_gb ?? 0; - const perc = total > 0 ? (used / total) * 100 : 0; - const { memWarning: warning, memCritical: critical } = - useServerThresholds(id); + const value = total > 0 ? (used / total) * 100 : 0; + const intent: "Good" | "Warning" | "Critical" = - perc < warning ? "Good" : perc < critical ? "Warning" : "Critical"; - return ; + value < thresholds.memWarning + ? "Good" + : value < thresholds.memCritical + ? "Warning" + : "Critical"; + + return ; } function DiskCell({ id }: { id: string }) { const stats = useServerStats(id); + const thresholds = useServerThresholds(id); + const used = stats?.disks?.reduce((acc, d) => acc + (d.used_gb || 0), 0) ?? 0; const total = stats?.disks?.reduce((acc, d) => acc + (d.total_gb || 0), 0) ?? 0; - const perc = total > 0 ? (used / total) * 100 : 0; - const { diskWarning: warning, diskCritical: critical } = - useServerThresholds(id); + const value = total > 0 ? (used / total) * 100 : 0; + const intent: "Good" | "Warning" | "Critical" = - perc < warning ? "Good" : perc < critical ? "Warning" : "Critical"; + value < thresholds.diskWarning + ? "Good" + : value < thresholds.diskCritical + ? "Warning" + : "Critical"; + return ( } @@ -118,30 +161,24 @@ function LoadAvgCell({ id }: { id: string }) { const five = stats?.load_average?.five; const fifteen = stats?.load_average?.fifteen; return ( - - - + + + 1m - - {one !== undefined ? one.toFixed(2) : "N/A"} - + {one !== undefined ? one.toFixed(2) : "N/A"} - - + + 5m - - {five !== undefined ? five.toFixed(2) : "N/A"} - + {five !== undefined ? five.toFixed(2) : "N/A"} - - + + 15m - - {fifteen !== undefined ? fifteen.toFixed(2) : "N/A"} - + {fifteen !== undefined ? fifteen.toFixed(2) : "N/A"} ); @@ -152,7 +189,16 @@ function NetCell({ id }: { id: string }) { const ingress = stats?.network_ingress_bytes ?? 0; const egress = stats?.network_egress_bytes ?? 0; if (!stats) { - return N/A; + return ( + + N/A + + ); } - return {fmtRateBytes(ingress + egress)}; + return ( + + + {fmtRateBytes(ingress + egress)} + + ); } diff --git a/ui/src/ui/stat-cell.tsx b/ui/src/ui/stat-cell.tsx new file mode 100644 index 000000000..0c5116389 --- /dev/null +++ b/ui/src/ui/stat-cell.tsx @@ -0,0 +1,74 @@ +import { ColorIntention, hexColorByIntention } from "mogh_ui"; +import { ICONS } from "@/lib/icons"; +import { + ActionIcon, + FloatingPosition, + Group, + GroupProps, + HoverCard, + Progress, + ProgressProps, + Text, + TextProps, +} from "@mantine/core"; +import { ReactNode } from "react"; + +export interface StatCellProps extends GroupProps { + value: number | undefined; + intent: ColorIntention; + textProps?: TextProps; + barProps?: ProgressProps; + info?: ReactNode; + infoPosition?: FloatingPosition; + infoDisabled?: boolean; + suffix?: string; +} + +export default function StatCell({ + value, + intent, + textProps, + barProps, + info, + infoPosition = "left-start", + infoDisabled, + suffix = "%", + ...groupProps +}: StatCellProps) { + const ProgressComponent = ( + + ); + return ( + + + {value === undefined ? "N/A" : value.toFixed(0) + suffix} + + {!info && ProgressComponent} + {info && ( + + + + {ProgressComponent} + + + + + + {info} + + )} + + ); +}