From d74ed8065b9f6b9b6498a81bd86398ea1654433a Mon Sep 17 00:00:00 2001 From: "kiloconnect[bot]" <240665456+kiloconnect[bot]@users.noreply.github.com> Date: Sat, 28 Feb 2026 08:26:00 +0000 Subject: [PATCH 1/2] Add secondary disk and network stats to system cards --- src/components/home/SystemStats.tsx | 48 +++++++++++++++++---- src/components/home/SystemStatsProvider.tsx | 27 ++++++++++++ src/components/home/store.ts | 6 +++ 3 files changed, 72 insertions(+), 9 deletions(-) diff --git a/src/components/home/SystemStats.tsx b/src/components/home/SystemStats.tsx index 23ffe0d..ffa3523 100644 --- a/src/components/home/SystemStats.tsx +++ b/src/components/home/SystemStats.tsx @@ -1,8 +1,7 @@ import type { FieldPath } from 'juststore' -import { Clock, Cpu, HardDrive, type LucideIcon, MemoryStick } from 'lucide-react' +import { Clock, Cpu, HardDrive, type LucideIcon, MemoryStick, Wifi } from 'lucide-react' import { Suspense } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' -import { useIsMobile } from '@/hooks/use-mobile' import { formatDuration } from '@/lib/format' import SystemStatsProvider from './SystemStatsProvider' import SystemStatValue from './SystemStatValue' @@ -33,8 +32,10 @@ function SystemStatsMobile() { } function SystemStatsDesktop() { + const hasSecondaryDisk = Boolean(store.systemInfo.secondaryPartitionUsageDesc.use()) + return ( -
+
{statsProps.map(stat => ( @@ -46,11 +47,28 @@ function SystemStatsDesktop() {
- + {stat.key === 'rootPartitionUsage' ? ( + <> + + {hasSecondaryDisk && ( + + )} + + ) : ( + + )} ))} @@ -62,7 +80,7 @@ type StatProp = { label: string mobileLabel: string icon: LucideIcon - mobileValueMode?: 'percent' | 'description' | 'duration' + mobileValueMode?: 'percent' | 'description' | 'duration' | 'text' type: 'text' | 'progress' | 'duration' color: string key: FieldPath @@ -77,6 +95,9 @@ function MobileStatRow({ stat }: { stat: StatProp }) { if (stat.mobileValueMode === 'duration') { return formatDuration(Number(value), { unit: 's' }) } + if (stat.mobileValueMode === 'text') { + return String(value) + } return `${value}%` })() @@ -130,4 +151,13 @@ const statsProps: StatProp[] = [ key: 'rootPartitionUsage', descriptionKey: 'rootPartitionUsageDesc', }, + { + label: 'Network', + mobileLabel: 'Net', + icon: Wifi, + mobileValueMode: 'text', + type: 'text', + color: 'bg-chart-4', + key: 'networkSpeedSummary', + }, ] as const diff --git a/src/components/home/SystemStatsProvider.tsx b/src/components/home/SystemStatsProvider.tsx index 9aacc58..9642c32 100644 --- a/src/components/home/SystemStatsProvider.tsx +++ b/src/components/home/SystemStatsProvider.tsx @@ -15,8 +15,11 @@ export default function SystemStatsProvider() { cpuAverage: Math.round(data.cpu_average * 100) / 100, rootPartitionUsage: Math.round(getDiskUsage(data.disks, '/') ?? 0 * 100), rootPartitionUsageDesc: getDiskUsageDesc(data.disks, '/'), + secondaryPartitionUsage: Math.round(getSecondaryDiskUsage(data.disks, '/') ?? 0 * 100), + secondaryPartitionUsageDesc: getSecondaryDiskUsageDesc(data.disks, '/'), memoryUsage: Math.round(data.memory.used_percent * 100) / 100, memoryUsageDesc: getMemoryUsageDesc(data.memory), + networkSpeedSummary: getNetworkSpeedSummary(data), }), }) @@ -44,6 +47,30 @@ function getDiskUsageDesc(disks: Record, path: string) { return `${formatBytes(disk.used, { precision: 1 })} / ${formatBytes(disk.total, { precision: 1 })}` } +function getSecondaryDisk(disks: Record, path: string) { + const allDisks = Object.values(disks) + const primaryDisk = getDisk(disks, path) + return allDisks.find(d => d.path !== primaryDisk?.path) +} + +function getSecondaryDiskUsage(disks: Record, path: string) { + return getSecondaryDisk(disks, path)?.used_percent +} + +function getSecondaryDiskUsageDesc(disks: Record, path: string) { + const disk = getSecondaryDisk(disks, path) + if (!disk) { + return '' + } + return `${disk.path}: ${formatBytes(disk.used, { precision: 1 })} / ${formatBytes(disk.total, { precision: 1 })}` +} + function getMemoryUsageDesc(memory: MemVirtualMemoryStat) { return `${formatBytes(memory.used, { precision: 1 })} / ${formatBytes(memory.total, { precision: 1 })}` } + +function getNetworkSpeedSummary(data: SystemInfo) { + const uploadSpeed = data.network?.upload_speed ?? 0 + const downloadSpeed = data.network?.download_speed ?? 0 + return `↑ ${formatBytes(uploadSpeed, { precision: 0, unit: '/s' })} ↓ ${formatBytes(downloadSpeed, { precision: 0, unit: '/s' })}` +} diff --git a/src/components/home/store.ts b/src/components/home/store.ts index 738127c..5a02f1e 100644 --- a/src/components/home/store.ts +++ b/src/components/home/store.ts @@ -40,8 +40,11 @@ type SystemInfoSimple = { cpuAverage: number rootPartitionUsage: number rootPartitionUsageDesc: string + secondaryPartitionUsage: number + secondaryPartitionUsageDesc: string memoryUsage: number memoryUsageDesc: string + networkSpeedSummary: string } export const store = createStore('homepage', { @@ -50,8 +53,11 @@ export const store = createStore('homepage', { cpuAverage: 0, rootPartitionUsage: 0, rootPartitionUsageDesc: '', + secondaryPartitionUsage: 0, + secondaryPartitionUsageDesc: '', memoryUsage: 0, memoryUsageDesc: '', + networkSpeedSummary: '—', }, homepageCategories: [], searchQuery: '', From 05661882531da173da43f34584fd10d5214b9902 Mon Sep 17 00:00:00 2001 From: yusing Date: Sat, 28 Feb 2026 19:07:09 +0800 Subject: [PATCH 2/2] feat(ui): show separate upload and download speeds replace the combined network speed summary with dedicated upload and download values in the home system stats cards. improve stat rendering for responsive layouts by hiding network on mobile, stacking network directions on desktop, and refining text-like value styling with directional icons. also improve secondary disk selection by avoiding duplicate matches to the primary disk and using the disk key in the secondary disk description. --- src/components/home/SystemStatValue.tsx | 40 +++++++++-- src/components/home/SystemStats.tsx | 74 ++++++++++++++------- src/components/home/SystemStatsProvider.tsx | 33 ++++++--- src/components/home/store.ts | 6 +- 4 files changed, 113 insertions(+), 40 deletions(-) diff --git a/src/components/home/SystemStatValue.tsx b/src/components/home/SystemStatValue.tsx index 51f0a4b..caa84dc 100644 --- a/src/components/home/SystemStatValue.tsx +++ b/src/components/home/SystemStatValue.tsx @@ -1,9 +1,12 @@ +import { IconArrowDown, IconArrowUp } from '@tabler/icons-react' import { type FieldPath, Render } from 'juststore' -import { formatDuration } from '@/lib/format' +import { formatBytes, formatDuration } from '@/lib/format' import { cn } from '@/lib/utils' import { Progress } from '../ui/progress' import { type Store, store } from './store' +export type SystemStatValueType = 'text' | 'progress' | 'duration' | 'upload' | 'download' + export default function SystemStatValue({ valueKey, descriptionKey, @@ -11,7 +14,7 @@ export default function SystemStatValue({ }: { valueKey: FieldPath descriptionKey?: FieldPath - type: 'text' | 'progress' | 'duration' + type: SystemStatValueType }) { const state = store.systemInfo[valueKey] return ( @@ -35,7 +38,7 @@ function DisplayValue({ type, }: { valueKey: FieldPath - type: 'text' | 'progress' | 'duration' + type: SystemStatValueType }) { const displayValue = store.systemInfo[valueKey].useCompute(value => { if (type === 'duration') { @@ -44,10 +47,37 @@ function DisplayValue({ if (type === 'progress') { return `${value}%` } + if (type === 'upload') { + type = 'text' + return ( + <> + {' '} + {formatBytes(Number(value), { precision: 0, unit: '/s' })} + + ) + } + if (type === 'download') { + type = 'text' + return ( + <> + {' '} + {formatBytes(Number(value), { precision: 0, unit: '/s' })} + + ) + } return String(value) }) + const isTextLike = type === 'text' || type === 'upload' || type === 'download' return ( -
+
{displayValue}
) @@ -58,7 +88,7 @@ function Description({ type, }: { descriptionKey?: FieldPath - type: 'text' | 'progress' | 'duration' + type: SystemStatValueType }) { const value = store.systemInfo[descriptionKey ?? 'uptime'].use() return ( diff --git a/src/components/home/SystemStats.tsx b/src/components/home/SystemStats.tsx index ffa3523..b3fa800 100644 --- a/src/components/home/SystemStats.tsx +++ b/src/components/home/SystemStats.tsx @@ -3,8 +3,9 @@ import { Clock, Cpu, HardDrive, type LucideIcon, MemoryStick, Wifi } from 'lucid import { Suspense } from 'react' import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card' import { formatDuration } from '@/lib/format' +import { cn } from '@/lib/utils' import SystemStatsProvider from './SystemStatsProvider' -import SystemStatValue from './SystemStatValue' +import SystemStatValue, { type SystemStatValueType } from './SystemStatValue' import { type Store, store } from './store' export default function SystemStats() { @@ -20,10 +21,13 @@ export default function SystemStats() { } function SystemStatsMobile() { + const stats = statsProps.filter(stat => { + return !('hideOnMobile' in stat && stat.hideOnMobile) + }) return ( - {statsProps.map(stat => ( + {stats.map(stat => ( ))} @@ -35,9 +39,13 @@ function SystemStatsDesktop() { const hasSecondaryDisk = Boolean(store.systemInfo.secondaryPartitionUsageDesc.use()) return ( -
+
{statsProps.map(stat => ( - +
@@ -47,21 +55,28 @@ function SystemStatsDesktop() {
- {stat.key === 'rootPartitionUsage' ? ( - <> - - {hasSecondaryDisk && ( + {stat.key === 'rootPartitionUsage' && hasSecondaryDisk ? ( +
+
- )} - +
+
+ +
+
+ ) : stat.key === 'networkSpeedUpload' ? ( +
+ + +
) : ( descriptionKey?: FieldPath + secondaryKey?: FieldPath + secondaryDescriptionKey?: FieldPath format?: (value: number) => string } @@ -98,6 +116,9 @@ function MobileStatRow({ stat }: { stat: StatProp }) { if (stat.mobileValueMode === 'text') { return String(value) } + if (stat.mobileValueMode === 'networkSpeed') { + return null + } return `${value}%` })() @@ -112,7 +133,7 @@ function MobileStatRow({ stat }: { stat: StatProp }) { ) } -const statsProps: StatProp[] = [ +const statsProps = [ { label: 'Uptime', mobileLabel: 'Up', @@ -121,6 +142,7 @@ const statsProps: StatProp[] = [ type: 'duration', color: 'text-primary', key: 'uptime', + descriptionKey: undefined, }, { label: 'CPU Usage', @@ -130,6 +152,7 @@ const statsProps: StatProp[] = [ type: 'progress', color: 'bg-chart-1', key: 'cpuAverage', + descriptionKey: undefined, }, { label: 'Memory', @@ -150,14 +173,19 @@ const statsProps: StatProp[] = [ color: 'bg-chart-3', key: 'rootPartitionUsage', descriptionKey: 'rootPartitionUsageDesc', + secondaryKey: 'secondaryPartitionUsage', + secondaryDescriptionKey: 'secondaryPartitionUsageDesc', }, { + hideOnMobile: true, label: 'Network', mobileLabel: 'Net', icon: Wifi, - mobileValueMode: 'text', + mobileValueMode: 'networkSpeed', type: 'text', color: 'bg-chart-4', - key: 'networkSpeedSummary', + key: 'networkSpeedUpload', + secondaryKey: 'networkSpeedDownload', + descriptionKey: undefined, }, -] as const +] as const satisfies StatProp[] diff --git a/src/components/home/SystemStatsProvider.tsx b/src/components/home/SystemStatsProvider.tsx index 9642c32..368ec2b 100644 --- a/src/components/home/SystemStatsProvider.tsx +++ b/src/components/home/SystemStatsProvider.tsx @@ -1,7 +1,7 @@ import { useWebSocketApi } from '@/hooks/websocket' import type { DiskUsageStat, MemVirtualMemoryStat, StatsResponse, SystemInfo } from '@/lib/api' -import { store } from './store' import { formatBytes } from '@/lib/format' +import { store } from './store' export default function SystemStatsProvider() { useWebSocketApi({ @@ -19,7 +19,8 @@ export default function SystemStatsProvider() { secondaryPartitionUsageDesc: getSecondaryDiskUsageDesc(data.disks, '/'), memoryUsage: Math.round(data.memory.used_percent * 100) / 100, memoryUsageDesc: getMemoryUsageDesc(data.memory), - networkSpeedSummary: getNetworkSpeedSummary(data), + networkSpeedUpload: data.network?.upload_speed ?? 0, + networkSpeedDownload: data.network?.download_speed ?? 0, }), }) @@ -50,7 +51,24 @@ function getDiskUsageDesc(disks: Record, path: string) { function getSecondaryDisk(disks: Record, path: string) { const allDisks = Object.values(disks) const primaryDisk = getDisk(disks, path) - return allDisks.find(d => d.path !== primaryDisk?.path) + if (!primaryDisk) { + return undefined + } + return allDisks.find(d => !isSameDisk(d, primaryDisk)) +} + +function isSameDisk(disk: DiskUsageStat, otherDisk: DiskUsageStat) { + if (disk.path === otherDisk.path) { + return true + } + if ( + disk.fstype === otherDisk.fstype && + disk.total === otherDisk.total && + disk.used === otherDisk.used + ) { + return true + } + return false } function getSecondaryDiskUsage(disks: Record, path: string) { @@ -62,15 +80,10 @@ function getSecondaryDiskUsageDesc(disks: Record, path: s if (!disk) { return '' } - return `${disk.path}: ${formatBytes(disk.used, { precision: 1 })} / ${formatBytes(disk.total, { precision: 1 })}` + const key = Object.entries(disks).find(([_, d]) => Object.is(d, disk))?.[0] + return `${key}: ${formatBytes(disk.used, { precision: 1 })} / ${formatBytes(disk.total, { precision: 1 })}` } function getMemoryUsageDesc(memory: MemVirtualMemoryStat) { return `${formatBytes(memory.used, { precision: 1 })} / ${formatBytes(memory.total, { precision: 1 })}` } - -function getNetworkSpeedSummary(data: SystemInfo) { - const uploadSpeed = data.network?.upload_speed ?? 0 - const downloadSpeed = data.network?.download_speed ?? 0 - return `↑ ${formatBytes(uploadSpeed, { precision: 0, unit: '/s' })} ↓ ${formatBytes(downloadSpeed, { precision: 0, unit: '/s' })}` -} diff --git a/src/components/home/store.ts b/src/components/home/store.ts index 5a02f1e..4454721 100644 --- a/src/components/home/store.ts +++ b/src/components/home/store.ts @@ -44,7 +44,8 @@ type SystemInfoSimple = { secondaryPartitionUsageDesc: string memoryUsage: number memoryUsageDesc: string - networkSpeedSummary: string + networkSpeedUpload: number + networkSpeedDownload: number } export const store = createStore('homepage', { @@ -57,7 +58,8 @@ export const store = createStore('homepage', { secondaryPartitionUsageDesc: '', memoryUsage: 0, memoryUsageDesc: '', - networkSpeedSummary: '—', + networkSpeedUpload: 0, + networkSpeedDownload: 0, }, homepageCategories: [], searchQuery: '',