From ee64ae43953f9faadfc8823c3eb1d26d27a2d794 Mon Sep 17 00:00:00 2001 From: Thiago Alves Cavalcante Date: Mon, 13 Apr 2026 20:56:33 -0300 Subject: [PATCH 1/4] feat: add CPU temperature monitoring for host and UI improvements --- bin/core/src/monitor/record.rs | 1 + bin/periphery/src/stats.rs | 17 ++++++++++ client/core/rs/src/entities/stats.rs | 6 ++++ client/core/ts/src/types.ts | 4 +++ ui/public/client/types.d.ts | 4 +++ ui/src/resources/server/stats-card.tsx | 6 +++- .../resources/server/stats/current/index.tsx | 7 +++++ ui/src/resources/server/table/stats.tsx | 31 ++++++++++++++++--- ui/src/ui/stat-cell.tsx | 4 +-- 9 files changed, 73 insertions(+), 7 deletions(-) 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 dba43aa9c..79a562c4e 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -2816,6 +2816,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; /** @@ -7890,6 +7892,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 a25b5291e..1cb5f8b01 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -2942,6 +2942,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; /** @@ -7546,6 +7548,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/resources/server/stats-card.tsx b/ui/src/resources/server/stats-card.tsx index 2fabbb9d9..c49a4717d 100644 --- a/ui/src/resources/server/stats-card.tsx +++ b/ui/src/resources/server/stats-card.tsx @@ -191,7 +191,11 @@ function StatItem({ : hexColorByIntention(intention(percentage, type)) } > - {isUnreachable ? "N/A" : `${percentage.toFixed(1)}%`} + {isUnreachable + ? "N/A" + : type === "cpu" && stats?.cpu_temp !== undefined + ? `${stats.cpu_temp.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 1905fa335..6f767a8ed 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 cb82bc837..4c89a127b 100644 --- a/ui/src/resources/server/table/stats.tsx +++ b/ui/src/resources/server/table/stats.tsx @@ -27,7 +27,7 @@ export default function StatsServerTable({ }} columns={[ { - size: 250, + size: 200, accessorKey: "name", header: ({ column }) => ( @@ -38,17 +38,22 @@ export default function StatsServerTable({ }, { header: "CPU", - size: 200, + size: 180, cell: ({ row }) => , }, + { + header: "Temp", + size: 80, + cell: ({ row }) => , + }, { header: "Memory", - size: 200, + size: 180, cell: ({ row }) => , }, { header: "Disk", - size: 200, + size: 180, cell: ({ row }) => , }, { @@ -81,6 +86,24 @@ function CpuCell({ id }: { id: string }) { return ; } +function TempCell({ id }: { id: string }) { + const stats = useServerStats(id); + const temp = stats?.cpu_temp; + const intent: "Good" | "Warning" | "Critical" = + temp === undefined + ? "Good" + : temp < 65 + ? "Good" + : temp < 80 + ? "Warning" + : "Critical"; + return ( + + {temp !== undefined ? `${temp.toFixed(0)}°C` : "N/A"} + + ); +} + function MemCell({ id }: { id: string }) { const stats = useServerStats(id); const used = stats?.mem_used_gb ?? 0; diff --git a/ui/src/ui/stat-cell.tsx b/ui/src/ui/stat-cell.tsx index 98662ac21..058a0ff8c 100644 --- a/ui/src/ui/stat-cell.tsx +++ b/ui/src/ui/stat-cell.tsx @@ -37,7 +37,7 @@ export default function StatCell({ @@ -45,7 +45,7 @@ export default function StatCell({ return ( From 591525abc5f551c0abfab83f98cb85cd53284105 Mon Sep 17 00:00:00 2001 From: Thiago Alves Cavalcante Date: Mon, 13 Apr 2026 22:19:55 -0300 Subject: [PATCH 2/4] chore: final cleanup and documentation for temperature feature --- .gitignore | 1 + ui/src/resources/server/index.tsx | 27 +++++++++++++++++++++++++ ui/src/resources/server/stats-card.tsx | 7 +++++-- ui/src/resources/server/table/stats.tsx | 14 ++++++++++--- ui/src/theme/icons.ts | 2 ++ ui/src/ui/stat-cell.tsx | 4 +++- 6 files changed, 49 insertions(+), 6 deletions(-) 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/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index fc3c676f7..0dd9147dd 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -210,6 +210,33 @@ 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( diff --git a/ui/src/resources/server/stats-card.tsx b/ui/src/resources/server/stats-card.tsx index c49a4717d..73ba95c71 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 ( @@ -193,8 +196,8 @@ function StatItem({ > {isUnreachable ? "N/A" - : type === "cpu" && stats?.cpu_temp !== undefined - ? `${stats.cpu_temp.toFixed(0)}°C | ${percentage.toFixed(1)}%` + : type === "cpu" && cpuTemp !== undefined + ? `${cpuTemp.toFixed(0)}°C | ${percentage.toFixed(1)}%` : `${percentage.toFixed(1)}%`} diff --git a/ui/src/resources/server/table/stats.tsx b/ui/src/resources/server/table/stats.tsx index 4c89a127b..0802f6896 100644 --- a/ui/src/resources/server/table/stats.tsx +++ b/ui/src/resources/server/table/stats.tsx @@ -98,9 +98,17 @@ function TempCell({ id }: { id: string }) { ? "Warning" : "Critical"; return ( - - {temp !== undefined ? `${temp.toFixed(0)}°C` : "N/A"} - + + CPU Temperature: {temp?.toFixed(1)}°C + + } + /> ); } diff --git a/ui/src/theme/icons.ts b/ui/src/theme/icons.ts index 45dd2cc1d..5e90c894e 100644 --- a/ui/src/theme/icons.ts +++ b/ui/src/theme/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/ui/stat-cell.tsx b/ui/src/ui/stat-cell.tsx index 058a0ff8c..2e1ca21c2 100644 --- a/ui/src/ui/stat-cell.tsx +++ b/ui/src/ui/stat-cell.tsx @@ -21,6 +21,7 @@ export interface StatCellProps extends GroupProps { info?: ReactNode; infoPosition?: FloatingPosition; infoDisabled?: boolean; + suffix?: string; } export default function StatCell({ @@ -31,6 +32,7 @@ export default function StatCell({ info, infoPosition = "left-start", infoDisabled, + suffix = "%", ...groupProps }: StatCellProps) { const ProgressComponent = ( @@ -49,7 +51,7 @@ export default function StatCell({ c={value === undefined ? "dimmed" : undefined} {...textProps} > - {value === undefined ? "N/A" : value.toFixed(1) + "%"} + {value === undefined ? "N/A" : value.toFixed(1) + suffix} {!info && ProgressComponent} {info && ( From f3b1b308c9136c8720dd22041a6055ee8c01e260 Mon Sep 17 00:00:00 2001 From: Thiago Alves Cavalcante Date: Fri, 15 May 2026 18:05:10 -0300 Subject: [PATCH 3/4] feat: re-implement CPU temperature monitoring for v2.2.0 UI --- client/core/ts/src/types.ts | 42 +++++-------------------- ui/public/client/types.d.ts | 42 +++++-------------------- ui/src/resources/server/index.tsx | 7 +++-- ui/src/resources/server/table/stats.tsx | 16 +++------- 4 files changed, 25 insertions(+), 82 deletions(-) diff --git a/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 01e4528dd..79a562c4e 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -612,7 +612,6 @@ export interface ImageRegistryConfig { export interface SystemCommand { path?: string; command?: string; - shell_mode?: boolean; } /** The build configuration. */ @@ -1428,8 +1427,6 @@ export enum DeploymentState { Created = "created", /** Server mode only. Container is in restart loop */ Restarting = "restarting", - /** Server mode only. Container is in the process of stopping */ - Stopping = "stopping", /** Server mode only. Container is being removed */ Removing = "removing", /** Server mode only. Container is paused */ @@ -2473,12 +2470,6 @@ export interface StackConfig { * Komodo will redeploy the whole Stack (all services). */ auto_update_all_services?: boolean; - /** - * Ignore certain services during Global Auto Update polling. - * Services listed here are skipped only in the global auto-update flow. - * Manual checks still include all services. - */ - auto_update_skip_services?: string[]; /** Whether to run `docker compose down` before `compose up`. */ destroy_before_deploy?: boolean; /** Whether to skip secret interpolation into the stack environment variables. */ @@ -2967,7 +2958,6 @@ export enum ContainerStateStatusEnum { Paused = "paused", Restarting = "restarting", Exited = "exited", - Stopping = "stopping", Removing = "removing", Dead = "dead", Empty = "", @@ -3099,7 +3089,8 @@ export interface RestartPolicy { MaximumRetryCount?: I64; } -export enum MountType { +export enum MountTypeEnum { + Empty = "", Bind = "bind", Volume = "volume", Image = "image", @@ -3171,7 +3162,7 @@ export interface Mount { * - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container. * - `cluster` a Swarm cluster volume */ - Type?: MountType; + Type?: MountTypeEnum; /** Whether the mount should be read-only. */ ReadOnly?: boolean; /** The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`. */ @@ -3344,7 +3335,7 @@ export interface GraphDriverData { /** MountPoint represents a mount point configuration inside the container. This is used for reporting the mountpoints in use by a container. */ export interface MountPoint { /** The mount type: - `bind` a mount of a file or directory from the host into the container. - `volume` a docker volume with the given `Name`. - `tmpfs` a `tmpfs`. - `npipe` a named pipe from the host into the container. - `cluster` a Swarm cluster volume */ - Type?: string; + Type?: MountTypeEnum; /** Name is the name reference to the underlying data defined by `Source` e.g., the volume name. */ Name?: string; /** Source location of the mount. For volumes, this contains the storage location of the volume (within `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains the source (host) part of the bind-mount. For `tmpfs` mount points, this field is empty. */ @@ -4304,10 +4295,7 @@ export interface ClusterVolumeSpecAccessModeSecrets { Secret?: string; } -/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ -export interface Topology { - Segments?: Record; -} +export type Topology = Record; /** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { @@ -6830,8 +6818,7 @@ export interface CreateOnboardingKey { expires?: I64; /** * Optionally specify an existing private key, otherwise - * generate fresh key. This key is not stored directly, - * only the public key. + * generate fresh key. */ private_key?: string; /** Default tags to apply to Servers created using this key. */ @@ -7453,31 +7440,18 @@ export interface UserGroupToml { /** Specifies resources to sync on Komodo */ export interface ResourcesToml { - /** Declare a swarm */ swarms?: ResourceToml<_PartialSwarmConfig>[]; - /** Declare a server */ servers?: ResourceToml<_PartialServerConfig>[]; - /** Declare a stack */ - stacks?: ResourceToml<_PartialStackConfig>[]; - /** Declare a deployment */ deployments?: ResourceToml<_PartialDeploymentConfig>[]; - /** Declare a build */ + stacks?: ResourceToml<_PartialStackConfig>[]; builds?: ResourceToml<_PartialBuildConfig>[]; - /** Declare a repo */ repos?: ResourceToml<_PartialRepoConfig>[]; - /** Declare a procedure */ procedures?: ResourceToml<_PartialProcedureConfig>[]; - /** Declare an action */ actions?: ResourceToml<_PartialActionConfig>[]; - /** Declare an alerter */ alerters?: ResourceToml<_PartialAlerterConfig>[]; - /** Declare a builder */ builders?: ResourceToml<_PartialBuilderConfig>[]; - /** Declare a resource sync */ resource_syncs?: ResourceToml<_PartialResourceSyncConfig>[]; - /** Declare a user group */ user_groups?: UserGroupToml[]; - /** Declare a variable */ variables?: Variable[]; } @@ -7766,7 +7740,7 @@ export interface GetCoreInfoResponse { enable_fancy_toml: boolean; /** TZ identifier Core is using, if manually set. */ timezone: string; - /** Public key for Core / Periphery authentication. */ + /** Default public key allowing this Core to authenticate to Periphery agents. */ public_key: string; } diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 5078a38ec..1cb5f8b01 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -607,7 +607,6 @@ export interface ImageRegistryConfig { export interface SystemCommand { path?: string; command?: string; - shell_mode?: boolean; } /** The build configuration. */ export interface BuildConfig { @@ -1569,8 +1568,6 @@ export declare enum DeploymentState { Created = "created", /** Server mode only. Container is in restart loop */ Restarting = "restarting", - /** Server mode only. Container is in the process of stopping */ - Stopping = "stopping", /** Server mode only. Container is being removed */ Removing = "removing", /** Server mode only. Container is paused */ @@ -2617,12 +2614,6 @@ export interface StackConfig { * Komodo will redeploy the whole Stack (all services). */ auto_update_all_services?: boolean; - /** - * Ignore certain services during Global Auto Update polling. - * Services listed here are skipped only in the global auto-update flow. - * Manual checks still include all services. - */ - auto_update_skip_services?: string[]; /** Whether to run `docker compose down` before `compose up`. */ destroy_before_deploy?: boolean; /** Whether to skip secret interpolation into the stack environment variables. */ @@ -3084,7 +3075,6 @@ export declare enum ContainerStateStatusEnum { Paused = "paused", Restarting = "restarting", Exited = "exited", - Stopping = "stopping", Removing = "removing", Dead = "dead", Empty = "" @@ -3201,7 +3191,8 @@ export interface RestartPolicy { /** If `on-failure` is used, the number of times to retry before giving up. */ MaximumRetryCount?: I64; } -export declare enum MountType { +export declare enum MountTypeEnum { + Empty = "", Bind = "bind", Volume = "volume", Image = "image", @@ -3267,7 +3258,7 @@ export interface Mount { * - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container. * - `cluster` a Swarm cluster volume */ - Type?: MountType; + Type?: MountTypeEnum; /** Whether the mount should be read-only. */ ReadOnly?: boolean; /** The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`. */ @@ -3435,7 +3426,7 @@ export interface GraphDriverData { /** MountPoint represents a mount point configuration inside the container. This is used for reporting the mountpoints in use by a container. */ export interface MountPoint { /** The mount type: - `bind` a mount of a file or directory from the host into the container. - `volume` a docker volume with the given `Name`. - `tmpfs` a `tmpfs`. - `npipe` a named pipe from the host into the container. - `cluster` a Swarm cluster volume */ - Type?: string; + Type?: MountTypeEnum; /** Name is the name reference to the underlying data defined by `Source` e.g., the volume name. */ Name?: string; /** Source location of the mount. For volumes, this contains the storage location of the volume (within `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains the source (host) part of the bind-mount. For `tmpfs` mount points, this field is empty. */ @@ -4309,10 +4300,7 @@ export interface ClusterVolumeSpecAccessModeSecrets { /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ Secret?: string; } -/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ -export interface Topology { - Segments?: Record; -} +export type Topology = Record; /** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { /** A list of required topologies, at least one of which the volume must be accessible from. */ @@ -6577,8 +6565,7 @@ export interface CreateOnboardingKey { expires?: I64; /** * Optionally specify an existing private key, otherwise - * generate fresh key. This key is not stored directly, - * only the public key. + * generate fresh key. */ private_key?: string; /** Default tags to apply to Servers created using this key. */ @@ -7147,31 +7134,18 @@ export interface UserGroupToml { } /** Specifies resources to sync on Komodo */ export interface ResourcesToml { - /** Declare a swarm */ swarms?: ResourceToml<_PartialSwarmConfig>[]; - /** Declare a server */ servers?: ResourceToml<_PartialServerConfig>[]; - /** Declare a stack */ - stacks?: ResourceToml<_PartialStackConfig>[]; - /** Declare a deployment */ deployments?: ResourceToml<_PartialDeploymentConfig>[]; - /** Declare a build */ + stacks?: ResourceToml<_PartialStackConfig>[]; builds?: ResourceToml<_PartialBuildConfig>[]; - /** Declare a repo */ repos?: ResourceToml<_PartialRepoConfig>[]; - /** Declare a procedure */ procedures?: ResourceToml<_PartialProcedureConfig>[]; - /** Declare an action */ actions?: ResourceToml<_PartialActionConfig>[]; - /** Declare an alerter */ alerters?: ResourceToml<_PartialAlerterConfig>[]; - /** Declare a builder */ builders?: ResourceToml<_PartialBuilderConfig>[]; - /** Declare a resource sync */ resource_syncs?: ResourceToml<_PartialResourceSyncConfig>[]; - /** Declare a user group */ user_groups?: UserGroupToml[]; - /** Declare a variable */ variables?: Variable[]; } /** @@ -7436,7 +7410,7 @@ export interface GetCoreInfoResponse { enable_fancy_toml: boolean; /** TZ identifier Core is using, if manually set. */ timezone: string; - /** Public key for Core / Periphery authentication. */ + /** Default public key allowing this Core to authenticate to Periphery agents. */ public_key: string; } /** Get a specific deployment by name or id. Response: [Deployment]. */ diff --git a/ui/src/resources/server/index.tsx b/ui/src/resources/server/index.tsx index 2026c6b78..595f7d10e 100644 --- a/ui/src/resources/server/index.tsx +++ b/ui/src/resources/server/index.tsx @@ -198,7 +198,7 @@ export const ServerComponents: RequiredResourceComponents< - + {coreCount ? `${coreCount} Core${coreCount === 1 ? "" : "s"}` : "N/A"} @@ -227,12 +227,13 @@ export const ServerComponents: RequiredResourceComponents< - + {temp !== undefined ? `${temp.toFixed(1)}°C` : "N/A"} - CPU Temperature: {temp !== undefined ? `${temp.toFixed(1)}°C` : "N/A"} + CPU Temperature:{" "} + {temp !== undefined ? `${temp.toFixed(1)}°C` : "N/A"} ); diff --git a/ui/src/resources/server/table/stats.tsx b/ui/src/resources/server/table/stats.tsx index 51c8d726d..f67f6e360 100644 --- a/ui/src/resources/server/table/stats.tsx +++ b/ui/src/resources/server/table/stats.tsx @@ -97,17 +97,11 @@ function TempCell({ id }: { id: string }) { ? "Warning" : "Critical"; return ( - - CPU Temperature: {temp?.toFixed(1)}°C - - } - /> + + + {temp === undefined ? "N/A" : temp.toFixed(1) + "°C"} + + ); } From d0e192edaa5705ca1e4b9c4efdf37d0ea340bc83 Mon Sep 17 00:00:00 2001 From: Thiago Alves Cavalcante Date: Sat, 16 May 2026 12:37:44 -0300 Subject: [PATCH 4/4] feat(ui): implement host temperature monitoring and polish stats layout - Integrated cpu_temp monitoring across periphery, core and ui. - Optimized DataTable layout using fixed table-layout for monitoring stats. - Fixed global checkbox column width to 45px across all dashboard tables. - Restored standard v2.2.0 formatting for LoadAvg and Network cells. - Added CACHE_BUST to single-arch Dockerfile for reliable UI builds. --- bin/core/single-arch.Dockerfile | 1 + client/core/ts/src/types.ts | 42 ++++++-- ui/public/client/types.d.ts | 42 ++++++-- ui/src/index.scss | 60 +++++++++++ ui/src/resources/server/index.tsx | 8 +- ui/src/resources/server/table/stats.tsx | 131 ++++++++++++++---------- ui/src/ui/stat-cell.tsx | 74 +++++++++++++ 7 files changed, 283 insertions(+), 75 deletions(-) create mode 100644 ui/src/ui/stat-cell.tsx 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/client/core/ts/src/types.ts b/client/core/ts/src/types.ts index 79a562c4e..01e4528dd 100644 --- a/client/core/ts/src/types.ts +++ b/client/core/ts/src/types.ts @@ -612,6 +612,7 @@ export interface ImageRegistryConfig { export interface SystemCommand { path?: string; command?: string; + shell_mode?: boolean; } /** The build configuration. */ @@ -1427,6 +1428,8 @@ export enum DeploymentState { Created = "created", /** Server mode only. Container is in restart loop */ Restarting = "restarting", + /** Server mode only. Container is in the process of stopping */ + Stopping = "stopping", /** Server mode only. Container is being removed */ Removing = "removing", /** Server mode only. Container is paused */ @@ -2470,6 +2473,12 @@ export interface StackConfig { * Komodo will redeploy the whole Stack (all services). */ auto_update_all_services?: boolean; + /** + * Ignore certain services during Global Auto Update polling. + * Services listed here are skipped only in the global auto-update flow. + * Manual checks still include all services. + */ + auto_update_skip_services?: string[]; /** Whether to run `docker compose down` before `compose up`. */ destroy_before_deploy?: boolean; /** Whether to skip secret interpolation into the stack environment variables. */ @@ -2958,6 +2967,7 @@ export enum ContainerStateStatusEnum { Paused = "paused", Restarting = "restarting", Exited = "exited", + Stopping = "stopping", Removing = "removing", Dead = "dead", Empty = "", @@ -3089,8 +3099,7 @@ export interface RestartPolicy { MaximumRetryCount?: I64; } -export enum MountTypeEnum { - Empty = "", +export enum MountType { Bind = "bind", Volume = "volume", Image = "image", @@ -3162,7 +3171,7 @@ export interface Mount { * - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container. * - `cluster` a Swarm cluster volume */ - Type?: MountTypeEnum; + Type?: MountType; /** Whether the mount should be read-only. */ ReadOnly?: boolean; /** The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`. */ @@ -3335,7 +3344,7 @@ export interface GraphDriverData { /** MountPoint represents a mount point configuration inside the container. This is used for reporting the mountpoints in use by a container. */ export interface MountPoint { /** The mount type: - `bind` a mount of a file or directory from the host into the container. - `volume` a docker volume with the given `Name`. - `tmpfs` a `tmpfs`. - `npipe` a named pipe from the host into the container. - `cluster` a Swarm cluster volume */ - Type?: MountTypeEnum; + Type?: string; /** Name is the name reference to the underlying data defined by `Source` e.g., the volume name. */ Name?: string; /** Source location of the mount. For volumes, this contains the storage location of the volume (within `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains the source (host) part of the bind-mount. For `tmpfs` mount points, this field is empty. */ @@ -4295,7 +4304,10 @@ export interface ClusterVolumeSpecAccessModeSecrets { Secret?: string; } -export type Topology = Record; +/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ +export interface Topology { + Segments?: Record; +} /** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { @@ -6818,7 +6830,8 @@ export interface CreateOnboardingKey { expires?: I64; /** * Optionally specify an existing private key, otherwise - * generate fresh key. + * generate fresh key. This key is not stored directly, + * only the public key. */ private_key?: string; /** Default tags to apply to Servers created using this key. */ @@ -7440,18 +7453,31 @@ export interface UserGroupToml { /** Specifies resources to sync on Komodo */ export interface ResourcesToml { + /** Declare a swarm */ swarms?: ResourceToml<_PartialSwarmConfig>[]; + /** Declare a server */ servers?: ResourceToml<_PartialServerConfig>[]; - deployments?: ResourceToml<_PartialDeploymentConfig>[]; + /** Declare a stack */ stacks?: ResourceToml<_PartialStackConfig>[]; + /** Declare a deployment */ + deployments?: ResourceToml<_PartialDeploymentConfig>[]; + /** Declare a build */ builds?: ResourceToml<_PartialBuildConfig>[]; + /** Declare a repo */ repos?: ResourceToml<_PartialRepoConfig>[]; + /** Declare a procedure */ procedures?: ResourceToml<_PartialProcedureConfig>[]; + /** Declare an action */ actions?: ResourceToml<_PartialActionConfig>[]; + /** Declare an alerter */ alerters?: ResourceToml<_PartialAlerterConfig>[]; + /** Declare a builder */ builders?: ResourceToml<_PartialBuilderConfig>[]; + /** Declare a resource sync */ resource_syncs?: ResourceToml<_PartialResourceSyncConfig>[]; + /** Declare a user group */ user_groups?: UserGroupToml[]; + /** Declare a variable */ variables?: Variable[]; } @@ -7740,7 +7766,7 @@ export interface GetCoreInfoResponse { enable_fancy_toml: boolean; /** TZ identifier Core is using, if manually set. */ timezone: string; - /** Default public key allowing this Core to authenticate to Periphery agents. */ + /** Public key for Core / Periphery authentication. */ public_key: string; } diff --git a/ui/public/client/types.d.ts b/ui/public/client/types.d.ts index 1cb5f8b01..5078a38ec 100644 --- a/ui/public/client/types.d.ts +++ b/ui/public/client/types.d.ts @@ -607,6 +607,7 @@ export interface ImageRegistryConfig { export interface SystemCommand { path?: string; command?: string; + shell_mode?: boolean; } /** The build configuration. */ export interface BuildConfig { @@ -1568,6 +1569,8 @@ export declare enum DeploymentState { Created = "created", /** Server mode only. Container is in restart loop */ Restarting = "restarting", + /** Server mode only. Container is in the process of stopping */ + Stopping = "stopping", /** Server mode only. Container is being removed */ Removing = "removing", /** Server mode only. Container is paused */ @@ -2614,6 +2617,12 @@ export interface StackConfig { * Komodo will redeploy the whole Stack (all services). */ auto_update_all_services?: boolean; + /** + * Ignore certain services during Global Auto Update polling. + * Services listed here are skipped only in the global auto-update flow. + * Manual checks still include all services. + */ + auto_update_skip_services?: string[]; /** Whether to run `docker compose down` before `compose up`. */ destroy_before_deploy?: boolean; /** Whether to skip secret interpolation into the stack environment variables. */ @@ -3075,6 +3084,7 @@ export declare enum ContainerStateStatusEnum { Paused = "paused", Restarting = "restarting", Exited = "exited", + Stopping = "stopping", Removing = "removing", Dead = "dead", Empty = "" @@ -3191,8 +3201,7 @@ export interface RestartPolicy { /** If `on-failure` is used, the number of times to retry before giving up. */ MaximumRetryCount?: I64; } -export declare enum MountTypeEnum { - Empty = "", +export declare enum MountType { Bind = "bind", Volume = "volume", Image = "image", @@ -3258,7 +3267,7 @@ export interface Mount { * - `tmpfs` Create a tmpfs with the given options. The mount source cannot be specified for tmpfs. - `npipe` Mounts a named pipe from the host into the container. Must exist prior to creating the container. * - `cluster` a Swarm cluster volume */ - Type?: MountTypeEnum; + Type?: MountType; /** Whether the mount should be read-only. */ ReadOnly?: boolean; /** The consistency requirement for the mount: `default`, `consistent`, `cached`, or `delegated`. */ @@ -3426,7 +3435,7 @@ export interface GraphDriverData { /** MountPoint represents a mount point configuration inside the container. This is used for reporting the mountpoints in use by a container. */ export interface MountPoint { /** The mount type: - `bind` a mount of a file or directory from the host into the container. - `volume` a docker volume with the given `Name`. - `tmpfs` a `tmpfs`. - `npipe` a named pipe from the host into the container. - `cluster` a Swarm cluster volume */ - Type?: MountTypeEnum; + Type?: string; /** Name is the name reference to the underlying data defined by `Source` e.g., the volume name. */ Name?: string; /** Source location of the mount. For volumes, this contains the storage location of the volume (within `/var/lib/docker/volumes/`). For bind-mounts, and `npipe`, this contains the source (host) part of the bind-mount. For `tmpfs` mount points, this field is empty. */ @@ -4300,7 +4309,10 @@ export interface ClusterVolumeSpecAccessModeSecrets { /** Secret is the swarm Secret object from which to read data. This can be a Secret name or ID. The Secret data is retrieved by swarm and used as the value of the key-value pair passed to the plugin. */ Secret?: string; } -export type Topology = Record; +/** A map of topological domains to topological segments. For in depth details, see documentation for the Topology object in the CSI specification. */ +export interface Topology { + Segments?: Record; +} /** Requirements for the accessible topology of the volume. These fields are optional. For an in-depth description of what these fields mean, see the CSI specification. */ export interface ClusterVolumeSpecAccessModeAccessibilityRequirements { /** A list of required topologies, at least one of which the volume must be accessible from. */ @@ -6565,7 +6577,8 @@ export interface CreateOnboardingKey { expires?: I64; /** * Optionally specify an existing private key, otherwise - * generate fresh key. + * generate fresh key. This key is not stored directly, + * only the public key. */ private_key?: string; /** Default tags to apply to Servers created using this key. */ @@ -7134,18 +7147,31 @@ export interface UserGroupToml { } /** Specifies resources to sync on Komodo */ export interface ResourcesToml { + /** Declare a swarm */ swarms?: ResourceToml<_PartialSwarmConfig>[]; + /** Declare a server */ servers?: ResourceToml<_PartialServerConfig>[]; - deployments?: ResourceToml<_PartialDeploymentConfig>[]; + /** Declare a stack */ stacks?: ResourceToml<_PartialStackConfig>[]; + /** Declare a deployment */ + deployments?: ResourceToml<_PartialDeploymentConfig>[]; + /** Declare a build */ builds?: ResourceToml<_PartialBuildConfig>[]; + /** Declare a repo */ repos?: ResourceToml<_PartialRepoConfig>[]; + /** Declare a procedure */ procedures?: ResourceToml<_PartialProcedureConfig>[]; + /** Declare an action */ actions?: ResourceToml<_PartialActionConfig>[]; + /** Declare an alerter */ alerters?: ResourceToml<_PartialAlerterConfig>[]; + /** Declare a builder */ builders?: ResourceToml<_PartialBuilderConfig>[]; + /** Declare a resource sync */ resource_syncs?: ResourceToml<_PartialResourceSyncConfig>[]; + /** Declare a user group */ user_groups?: UserGroupToml[]; + /** Declare a variable */ variables?: Variable[]; } /** @@ -7410,7 +7436,7 @@ export interface GetCoreInfoResponse { enable_fancy_toml: boolean; /** TZ identifier Core is using, if manually set. */ timezone: string; - /** Default public key allowing this Core to authenticate to Periphery agents. */ + /** Public key for Core / Periphery authentication. */ public_key: string; } /** Get a specific deployment by name or id. Response: [Deployment]. */ 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/resources/server/index.tsx b/ui/src/resources/server/index.tsx index 595f7d10e..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"} @@ -255,7 +255,7 @@ export const ServerComponents: RequiredResourceComponents< - + {one?.toFixed(2) ?? "N/A"} @@ -279,7 +279,7 @@ export const ServerComponents: RequiredResourceComponents< - + {stats?.mem_total_gb.toFixed(2).concat(" GB") ?? "N/A"} @@ -307,7 +307,7 @@ export const ServerComponents: RequiredResourceComponents< - + {diskTotalGb?.toFixed(2).concat(" GB") ?? "N/A"} diff --git a/ui/src/resources/server/table/stats.tsx b/ui/src/resources/server/table/stats.tsx index f67f6e360..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: 180, + size: 170, cell: ({ row }) => , }, { header: "Temp", - size: 80, + size: 170, cell: ({ row }) => , }, { header: "Memory", - size: 180, + size: 170, cell: ({ row }) => , }, { header: "Disk", - size: 180, + 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 }) => , }, ]} @@ -77,59 +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" = - cpu < warning ? "Good" : cpu < critical ? "Warning" : "Critical"; - return ; + value < thresholds.cpuWarning + ? "Good" + : value < thresholds.cpuCritical + ? "Warning" + : "Critical"; + + return ; } function TempCell({ id }: { id: string }) { const stats = useServerStats(id); - const temp = stats?.cpu_temp; + const value = stats?.cpu_temp; + const intent: "Good" | "Warning" | "Critical" = - temp === undefined + value === undefined ? "Good" - : temp < 65 + : value < 65 ? "Good" - : temp < 80 + : value < 80 ? "Warning" : "Critical"; + return ( - - - {temp === undefined ? "N/A" : temp.toFixed(1) + "°C"} - - + ); } 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 ( } @@ -143,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"} ); @@ -177,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} + + )} + + ); +}