mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-28 16:36:16 +01:00
Compare commits
6 Commits
3eede6bead
...
extra-disk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bec07359c | ||
|
|
9eefacd482 | ||
|
|
aaa788bc2f | ||
|
|
85b786d11a | ||
|
|
0cb7dba050 | ||
|
|
c98472ca0b |
@@ -166,6 +166,7 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
||||
}
|
||||
|
||||
data.Stats.ExtraFs = make(map[string]*system.FsStats)
|
||||
data.Info.ExtraFsPct = make(map[string]float64)
|
||||
for name, stats := range a.fsStats {
|
||||
if !stats.Root && stats.DiskTotal > 0 {
|
||||
// Use custom name if available, otherwise use device name
|
||||
@@ -174,6 +175,11 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
||||
key = stats.Name
|
||||
}
|
||||
data.Stats.ExtraFs[key] = stats
|
||||
// Add percentage info to Info struct for dashboard
|
||||
if stats.DiskTotal > 0 {
|
||||
pct := twoDecimals((stats.DiskUsed / stats.DiskTotal) * 100)
|
||||
data.Info.ExtraFsPct[key] = pct
|
||||
}
|
||||
}
|
||||
}
|
||||
slog.Debug("Extra FS", "data", data.Stats.ExtraFs)
|
||||
|
||||
@@ -40,13 +40,18 @@ type UserNotificationSettings struct {
|
||||
}
|
||||
|
||||
type SystemAlertStats struct {
|
||||
Cpu float64 `json:"cpu"`
|
||||
Mem float64 `json:"mp"`
|
||||
Disk float64 `json:"dp"`
|
||||
NetSent float64 `json:"ns"`
|
||||
NetRecv float64 `json:"nr"`
|
||||
Temperatures map[string]float32 `json:"t"`
|
||||
LoadAvg [3]float64 `json:"la"`
|
||||
Cpu float64 `json:"cpu"`
|
||||
Mem float64 `json:"mp"`
|
||||
Disk float64 `json:"dp"`
|
||||
NetSent float64 `json:"ns"`
|
||||
NetRecv float64 `json:"nr"`
|
||||
GPU map[string]SystemAlertGPUData `json:"g"`
|
||||
Temperatures map[string]float32 `json:"t"`
|
||||
LoadAvg [3]float64 `json:"la"`
|
||||
}
|
||||
|
||||
type SystemAlertGPUData struct {
|
||||
Usage float64 `json:"u"`
|
||||
}
|
||||
|
||||
type SystemAlertData struct {
|
||||
|
||||
@@ -64,6 +64,8 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
case "LoadAvg15":
|
||||
val = data.Info.LoadAvg[2]
|
||||
unit = ""
|
||||
case "GPU":
|
||||
val = data.Info.GpuPct
|
||||
}
|
||||
|
||||
triggered := alertRecord.GetBool("triggered")
|
||||
@@ -206,6 +208,17 @@ func (am *AlertManager) HandleSystemAlerts(systemRecord *core.Record, data *syst
|
||||
alert.val += stats.LoadAvg[1]
|
||||
case "LoadAvg15":
|
||||
alert.val += stats.LoadAvg[2]
|
||||
case "GPU":
|
||||
if len(stats.GPU) == 0 {
|
||||
continue
|
||||
}
|
||||
maxUsage := 0.0
|
||||
for _, gpu := range stats.GPU {
|
||||
if gpu.Usage > maxUsage {
|
||||
maxUsage = gpu.Usage
|
||||
}
|
||||
}
|
||||
alert.val += maxUsage
|
||||
default:
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -99,6 +99,7 @@ type FsStats struct {
|
||||
MaxDiskWriteBytes uint64 `json:"wbm,omitempty" cbor:"-"`
|
||||
}
|
||||
|
||||
|
||||
type NetIoStats struct {
|
||||
BytesRecv uint64
|
||||
BytesSent uint64
|
||||
@@ -146,6 +147,7 @@ type Info struct {
|
||||
// TODO: remove load fields in future release in favor of load avg array
|
||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
||||
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||
}
|
||||
|
||||
// Final data structure to return to the hub
|
||||
|
||||
@@ -75,6 +75,7 @@ func init() {
|
||||
"Disk",
|
||||
"Temperature",
|
||||
"Bandwidth",
|
||||
"GPU",
|
||||
"LoadAvg1",
|
||||
"LoadAvg5",
|
||||
"LoadAvg15"
|
||||
|
||||
@@ -20,6 +20,7 @@ import {
|
||||
WifiIcon,
|
||||
} from "lucide-react"
|
||||
import { memo, useMemo, useRef, useState } from "react"
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from "../ui/tooltip"
|
||||
import { isReadOnlyUser, pb } from "@/lib/api"
|
||||
import { ConnectionType, connectionTypeLabels, MeterState, SystemStatus } from "@/lib/enums"
|
||||
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
|
||||
@@ -153,7 +154,7 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
|
||||
accessorFn: ({ info }) => info.dp,
|
||||
id: "disk",
|
||||
name: () => t`Disk`,
|
||||
cell: TableCellWithMeter,
|
||||
cell: DiskCellWithMultiple,
|
||||
Icon: HardDriveIcon,
|
||||
header: sortableHeader,
|
||||
},
|
||||
@@ -354,6 +355,74 @@ function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
|
||||
)
|
||||
}
|
||||
|
||||
function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
|
||||
const { info: sysInfo, status } = info.row.original
|
||||
const rootDiskPct = sysInfo.dp
|
||||
const extraFsData = sysInfo.efs
|
||||
const extraFsCount = extraFsData ? Object.keys(extraFsData).length : 0
|
||||
|
||||
function getMeterClass(pct: number) {
|
||||
const threshold = getMeterState(pct)
|
||||
return cn(
|
||||
"h-full",
|
||||
(status !== SystemStatus.Up && STATUS_COLORS.paused) ||
|
||||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
|
||||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
|
||||
STATUS_COLORS.down
|
||||
)
|
||||
}
|
||||
|
||||
// No extra disks - show simple meter
|
||||
if (extraFsCount === 0) {
|
||||
return TableCellWithMeter(info)
|
||||
}
|
||||
|
||||
// Has extra disks - show with tooltip
|
||||
return (
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<Link href={getPagePath($router, "system", { id: info.row.original.id })} tabIndex={-1} className="flex flex-col gap-0.5 w-full relative z-10">
|
||||
<div className="flex gap-2 items-center tabular-nums tracking-tight">
|
||||
<span className="min-w-8 shrink-0">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>
|
||||
<span className="flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
|
||||
<span className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span>
|
||||
{extraFsData && Object.entries(extraFsData).slice(0, 2).map(([_name, pct], index) => (
|
||||
<span key={index} className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span>
|
||||
))}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
</TooltipTrigger>
|
||||
<TooltipContent side="right" className="max-w-xs pb-2">
|
||||
<div className="flex flex-col gap-1.5">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase tabular-nums">{t`Root`}</div>
|
||||
<div className="flex gap-2 items-center tabular-nums text-xs">
|
||||
<span className="min-w-7">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>
|
||||
<span className="flex-1 min-w-12 grid bg-muted/50 h-2 rounded-sm overflow-hidden">
|
||||
<span className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
{extraFsData && Object.entries(extraFsData).map(([name, pct]) => {
|
||||
return (
|
||||
<div key={name} className="flex flex-col gap-0.5">
|
||||
<div className="text-[0.65rem] text-muted-foreground uppercase tracking-wider truncate">{name}</div>
|
||||
<div className="flex gap-2 items-center tabular-nums text-xs">
|
||||
<span className="min-w-7">{decimalString(pct, pct >= 10 ? 1 : 2)}%</span>
|
||||
<span className="flex-1 min-w-12 grid bg-muted/50 h-2 rounded-sm overflow-hidden">
|
||||
<span className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</TooltipContent>
|
||||
</Tooltip>
|
||||
)
|
||||
}
|
||||
|
||||
export function IndicatorDot({ system, className }: { system: SystemRecord; className?: ClassValue }) {
|
||||
className ||= STATUS_COLORS[system.status as keyof typeof STATUS_COLORS] || ""
|
||||
return (
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { t } from "@lingui/core/macro"
|
||||
import { CpuIcon, HardDriveIcon, HourglassIcon, MemoryStickIcon, ServerIcon, ThermometerIcon } from "lucide-react"
|
||||
import type { RecordSubscription } from "pocketbase"
|
||||
import { EthernetIcon } from "@/components/ui/icons"
|
||||
import { EthernetIcon, GpuIcon } from "@/components/ui/icons"
|
||||
import { $alerts } from "@/lib/stores"
|
||||
import type { AlertInfo, AlertRecord } from "@/types"
|
||||
import { pb } from "./api"
|
||||
@@ -41,6 +41,12 @@ export const alertInfo: Record<string, AlertInfo> = {
|
||||
desc: () => t`Triggers when combined up/down exceeds a threshold`,
|
||||
max: 125,
|
||||
},
|
||||
GPU: {
|
||||
name: () => t`GPU Usage`,
|
||||
unit: "%",
|
||||
icon: GpuIcon,
|
||||
desc: () => t`Triggers when GPU usage exceeds a threshold`,
|
||||
},
|
||||
Temperature: {
|
||||
name: () => t`Temperature`,
|
||||
unit: "°C",
|
||||
|
||||
27
internal/site/src/types.d.ts
vendored
27
internal/site/src/types.d.ts
vendored
@@ -77,8 +77,11 @@ export interface SystemInfo {
|
||||
os?: Os
|
||||
/** connection type */
|
||||
ct?: ConnectionType
|
||||
/** extra filesystem percentages */
|
||||
efs?: Record<string, number>
|
||||
}
|
||||
|
||||
|
||||
export interface SystemStats {
|
||||
/** cpu percent */
|
||||
cpu: number
|
||||
@@ -301,18 +304,18 @@ export interface ChartData {
|
||||
chartTime: ChartTimes
|
||||
}
|
||||
|
||||
// interface AlertInfo {
|
||||
// name: () => string
|
||||
// unit: string
|
||||
// icon: any
|
||||
// desc: () => string
|
||||
// max?: number
|
||||
// min?: number
|
||||
// step?: number
|
||||
// start?: number
|
||||
// /** Single value description (when there's only one value, like status) */
|
||||
// singleDesc?: () => string
|
||||
// }
|
||||
export interface AlertInfo {
|
||||
name: () => string
|
||||
unit: string
|
||||
icon: any
|
||||
desc: () => string
|
||||
max?: number
|
||||
min?: number
|
||||
step?: number
|
||||
start?: number
|
||||
/** Single value description (when there's only one value, like status) */
|
||||
singleDesc?: () => string
|
||||
}
|
||||
|
||||
export type AlertMap = Record<string, Map<string, AlertRecord>>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user