mirror of
https://github.com/henrygd/beszel.git
synced 2026-03-24 14:36:17 +01:00
Compare commits
5 Commits
b722ccc5bc
...
extra-disk
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1bec07359c | ||
|
|
9eefacd482 | ||
|
|
85b786d11a | ||
|
|
0cb7dba050 | ||
|
|
c98472ca0b |
2
.github/workflows/docker-images.yml
vendored
2
.github/workflows/docker-images.yml
vendored
@@ -33,7 +33,6 @@ jobs:
|
|||||||
password_secret: DOCKERHUB_TOKEN
|
password_secret: DOCKERHUB_TOKEN
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=edge
|
type=raw,value=edge
|
||||||
type=raw,value=latest
|
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
@@ -100,7 +99,6 @@ jobs:
|
|||||||
password_secret: GITHUB_TOKEN
|
password_secret: GITHUB_TOKEN
|
||||||
tags: |
|
tags: |
|
||||||
type=raw,value=edge
|
type=raw,value=edge
|
||||||
type=raw,value=latest
|
|
||||||
type=semver,pattern={{version}}
|
type=semver,pattern={{version}}
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
type=semver,pattern={{major}}.{{minor}}
|
||||||
type=semver,pattern={{major}}
|
type=semver,pattern={{major}}
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ func (a *Agent) gatherStats(cacheTimeMs uint16) *system.CombinedData {
|
|||||||
key = stats.Name
|
key = stats.Name
|
||||||
}
|
}
|
||||||
data.Stats.ExtraFs[key] = stats
|
data.Stats.ExtraFs[key] = stats
|
||||||
// Add percentages to Info struct for dashboard
|
// Add percentage info to Info struct for dashboard
|
||||||
if stats.DiskTotal > 0 {
|
if stats.DiskTotal > 0 {
|
||||||
pct := twoDecimals((stats.DiskUsed / stats.DiskTotal) * 100)
|
pct := twoDecimals((stats.DiskUsed / stats.DiskTotal) * 100)
|
||||||
data.Info.ExtraFsPct[key] = pct
|
data.Info.ExtraFsPct[key] = pct
|
||||||
|
|||||||
@@ -24,12 +24,11 @@ import (
|
|||||||
// SmartManager manages data collection for SMART devices
|
// SmartManager manages data collection for SMART devices
|
||||||
type SmartManager struct {
|
type SmartManager struct {
|
||||||
sync.Mutex
|
sync.Mutex
|
||||||
SmartDataMap map[string]*smart.SmartData
|
SmartDataMap map[string]*smart.SmartData
|
||||||
SmartDevices []*DeviceInfo
|
SmartDevices []*DeviceInfo
|
||||||
refreshMutex sync.Mutex
|
refreshMutex sync.Mutex
|
||||||
lastScanTime time.Time
|
lastScanTime time.Time
|
||||||
binPath string
|
binPath string
|
||||||
excludedDevices map[string]struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type scanOutput struct {
|
type scanOutput struct {
|
||||||
@@ -186,7 +185,6 @@ func (sm *SmartManager) ScanDevices(force bool) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
finalDevices := mergeDeviceLists(currentDevices, scannedDevices, configuredDevices)
|
finalDevices := mergeDeviceLists(currentDevices, scannedDevices, configuredDevices)
|
||||||
finalDevices = sm.filterExcludedDevices(finalDevices)
|
|
||||||
sm.updateSmartDevices(finalDevices)
|
sm.updateSmartDevices(finalDevices)
|
||||||
|
|
||||||
if len(finalDevices) == 0 {
|
if len(finalDevices) == 0 {
|
||||||
@@ -234,47 +232,6 @@ func (sm *SmartManager) parseConfiguredDevices(config string) ([]*DeviceInfo, er
|
|||||||
return devices, nil
|
return devices, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sm *SmartManager) refreshExcludedDevices() {
|
|
||||||
rawValue, _ := GetEnv("EXCLUDE_SMART")
|
|
||||||
sm.excludedDevices = make(map[string]struct{})
|
|
||||||
|
|
||||||
for entry := range strings.SplitSeq(rawValue, ",") {
|
|
||||||
device := strings.TrimSpace(entry)
|
|
||||||
if device == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
sm.excludedDevices[device] = struct{}{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sm *SmartManager) isExcludedDevice(deviceName string) bool {
|
|
||||||
_, exists := sm.excludedDevices[deviceName]
|
|
||||||
return exists
|
|
||||||
}
|
|
||||||
|
|
||||||
func (sm *SmartManager) filterExcludedDevices(devices []*DeviceInfo) []*DeviceInfo {
|
|
||||||
if devices == nil {
|
|
||||||
return []*DeviceInfo{}
|
|
||||||
}
|
|
||||||
|
|
||||||
excluded := sm.excludedDevices
|
|
||||||
if len(excluded) == 0 {
|
|
||||||
return devices
|
|
||||||
}
|
|
||||||
|
|
||||||
filtered := make([]*DeviceInfo, 0, len(devices))
|
|
||||||
for _, device := range devices {
|
|
||||||
if device == nil || device.Name == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, skip := excluded[device.Name]; skip {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
filtered = append(filtered, device)
|
|
||||||
}
|
|
||||||
return filtered
|
|
||||||
}
|
|
||||||
|
|
||||||
// detectSmartOutputType inspects sections that are unique to each smartctl
|
// detectSmartOutputType inspects sections that are unique to each smartctl
|
||||||
// JSON schema (NVMe, ATA/SATA, SCSI) to determine which parser should be used
|
// JSON schema (NVMe, ATA/SATA, SCSI) to determine which parser should be used
|
||||||
// when the reported device type is ambiguous or missing.
|
// when the reported device type is ambiguous or missing.
|
||||||
@@ -421,10 +378,6 @@ func (sm *SmartManager) parseSmartOutput(deviceInfo *DeviceInfo, output []byte)
|
|||||||
// Uses -n standby to avoid waking up sleeping disks, but bypasses standby mode
|
// Uses -n standby to avoid waking up sleeping disks, but bypasses standby mode
|
||||||
// for initial data collection when no cached data exists
|
// for initial data collection when no cached data exists
|
||||||
func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
||||||
if deviceInfo != nil && sm.isExcludedDevice(deviceInfo.Name) {
|
|
||||||
return errNoValidSmartData
|
|
||||||
}
|
|
||||||
|
|
||||||
// slog.Info("collecting SMART data", "device", deviceInfo.Name, "type", deviceInfo.Type, "has_existing_data", sm.hasDataForDevice(deviceInfo.Name))
|
// slog.Info("collecting SMART data", "device", deviceInfo.Name, "type", deviceInfo.Type, "has_existing_data", sm.hasDataForDevice(deviceInfo.Name))
|
||||||
|
|
||||||
// Check if we have any existing data for this device
|
// Check if we have any existing data for this device
|
||||||
@@ -456,10 +409,10 @@ func (sm *SmartManager) CollectSmart(deviceInfo *DeviceInfo) error {
|
|||||||
|
|
||||||
if !hasValidData {
|
if !hasValidData {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Info("smartctl failed", "device", deviceInfo.Name, "err", err)
|
slog.Debug("smartctl failed", "device", deviceInfo.Name, "err", err)
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
slog.Info("no valid SMART data found", "device", deviceInfo.Name)
|
slog.Debug("no valid SMART data found", "device", deviceInfo.Name)
|
||||||
return errNoValidSmartData
|
return errNoValidSmartData
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -962,7 +915,6 @@ func NewSmartManager() (*SmartManager, error) {
|
|||||||
sm := &SmartManager{
|
sm := &SmartManager{
|
||||||
SmartDataMap: make(map[string]*smart.SmartData),
|
SmartDataMap: make(map[string]*smart.SmartData),
|
||||||
}
|
}
|
||||||
sm.refreshExcludedDevices()
|
|
||||||
path, err := sm.detectSmartctl()
|
path, err := sm.detectSmartctl()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
slog.Debug(err.Error())
|
slog.Debug(err.Error())
|
||||||
|
|||||||
@@ -588,195 +588,3 @@ func TestIsVirtualDeviceScsi(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestRefreshExcludedDevices(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
envValue string
|
|
||||||
expectedDevs map[string]struct{}
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "empty env",
|
|
||||||
envValue: "",
|
|
||||||
expectedDevs: map[string]struct{}{},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "single device",
|
|
||||||
envValue: "/dev/sda",
|
|
||||||
expectedDevs: map[string]struct{}{
|
|
||||||
"/dev/sda": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "multiple devices",
|
|
||||||
envValue: "/dev/sda,/dev/sdb,/dev/nvme0",
|
|
||||||
expectedDevs: map[string]struct{}{
|
|
||||||
"/dev/sda": {},
|
|
||||||
"/dev/sdb": {},
|
|
||||||
"/dev/nvme0": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "devices with whitespace",
|
|
||||||
envValue: " /dev/sda , /dev/sdb , /dev/nvme0 ",
|
|
||||||
expectedDevs: map[string]struct{}{
|
|
||||||
"/dev/sda": {},
|
|
||||||
"/dev/sdb": {},
|
|
||||||
"/dev/nvme0": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "duplicate devices",
|
|
||||||
envValue: "/dev/sda,/dev/sdb,/dev/sda",
|
|
||||||
expectedDevs: map[string]struct{}{
|
|
||||||
"/dev/sda": {},
|
|
||||||
"/dev/sdb": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "empty entries and whitespace",
|
|
||||||
envValue: "/dev/sda,, /dev/sdb , , ",
|
|
||||||
expectedDevs: map[string]struct{}{
|
|
||||||
"/dev/sda": {},
|
|
||||||
"/dev/sdb": {},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
if tt.envValue != "" {
|
|
||||||
t.Setenv("EXCLUDE_SMART", tt.envValue)
|
|
||||||
} else {
|
|
||||||
// Ensure env var is not set for empty test
|
|
||||||
os.Unsetenv("EXCLUDE_SMART")
|
|
||||||
}
|
|
||||||
|
|
||||||
sm := &SmartManager{}
|
|
||||||
sm.refreshExcludedDevices()
|
|
||||||
|
|
||||||
assert.Equal(t, tt.expectedDevs, sm.excludedDevices)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestIsExcludedDevice(t *testing.T) {
|
|
||||||
sm := &SmartManager{
|
|
||||||
excludedDevices: map[string]struct{}{
|
|
||||||
"/dev/sda": {},
|
|
||||||
"/dev/nvme0": {},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
deviceName string
|
|
||||||
expectedBool bool
|
|
||||||
}{
|
|
||||||
{"excluded device sda", "/dev/sda", true},
|
|
||||||
{"excluded device nvme0", "/dev/nvme0", true},
|
|
||||||
{"non-excluded device sdb", "/dev/sdb", false},
|
|
||||||
{"non-excluded device nvme1", "/dev/nvme1", false},
|
|
||||||
{"empty device name", "", false},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
result := sm.isExcludedDevice(tt.deviceName)
|
|
||||||
assert.Equal(t, tt.expectedBool, result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterExcludedDevices(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
excludedDevs map[string]struct{}
|
|
||||||
inputDevices []*DeviceInfo
|
|
||||||
expectedDevs []*DeviceInfo
|
|
||||||
expectedLength int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "no exclusions",
|
|
||||||
excludedDevs: map[string]struct{}{},
|
|
||||||
inputDevices: []*DeviceInfo{
|
|
||||||
{Name: "/dev/sda"},
|
|
||||||
{Name: "/dev/sdb"},
|
|
||||||
{Name: "/dev/nvme0"},
|
|
||||||
},
|
|
||||||
expectedDevs: []*DeviceInfo{
|
|
||||||
{Name: "/dev/sda"},
|
|
||||||
{Name: "/dev/sdb"},
|
|
||||||
{Name: "/dev/nvme0"},
|
|
||||||
},
|
|
||||||
expectedLength: 3,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "some devices excluded",
|
|
||||||
excludedDevs: map[string]struct{}{
|
|
||||||
"/dev/sda": {},
|
|
||||||
"/dev/nvme0": {},
|
|
||||||
},
|
|
||||||
inputDevices: []*DeviceInfo{
|
|
||||||
{Name: "/dev/sda"},
|
|
||||||
{Name: "/dev/sdb"},
|
|
||||||
{Name: "/dev/nvme0"},
|
|
||||||
{Name: "/dev/nvme1"},
|
|
||||||
},
|
|
||||||
expectedDevs: []*DeviceInfo{
|
|
||||||
{Name: "/dev/sdb"},
|
|
||||||
{Name: "/dev/nvme1"},
|
|
||||||
},
|
|
||||||
expectedLength: 2,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "all devices excluded",
|
|
||||||
excludedDevs: map[string]struct{}{
|
|
||||||
"/dev/sda": {},
|
|
||||||
"/dev/sdb": {},
|
|
||||||
},
|
|
||||||
inputDevices: []*DeviceInfo{
|
|
||||||
{Name: "/dev/sda"},
|
|
||||||
{Name: "/dev/sdb"},
|
|
||||||
},
|
|
||||||
expectedDevs: []*DeviceInfo{},
|
|
||||||
expectedLength: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "nil devices",
|
|
||||||
excludedDevs: map[string]struct{}{},
|
|
||||||
inputDevices: nil,
|
|
||||||
expectedDevs: []*DeviceInfo{},
|
|
||||||
expectedLength: 0,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "filter nil and empty name devices",
|
|
||||||
excludedDevs: map[string]struct{}{
|
|
||||||
"/dev/sda": {},
|
|
||||||
},
|
|
||||||
inputDevices: []*DeviceInfo{
|
|
||||||
{Name: "/dev/sda"},
|
|
||||||
nil,
|
|
||||||
{Name: ""},
|
|
||||||
{Name: "/dev/sdb"},
|
|
||||||
},
|
|
||||||
expectedDevs: []*DeviceInfo{
|
|
||||||
{Name: "/dev/sdb"},
|
|
||||||
},
|
|
||||||
expectedLength: 1,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
sm := &SmartManager{
|
|
||||||
excludedDevices: tt.excludedDevs,
|
|
||||||
}
|
|
||||||
|
|
||||||
result := sm.filterExcludedDevices(tt.inputDevices)
|
|
||||||
|
|
||||||
assert.Len(t, result, tt.expectedLength)
|
|
||||||
assert.Equal(t, tt.expectedDevs, result)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -281,9 +281,9 @@ func (am *AlertManager) sendSystemAlert(alert SystemAlertData) {
|
|||||||
alert.name = after + "m Load"
|
alert.name = after + "m Load"
|
||||||
}
|
}
|
||||||
|
|
||||||
// make title alert name lowercase if not CPU or GPU
|
// make title alert name lowercase if not CPU
|
||||||
titleAlertName := alert.name
|
titleAlertName := alert.name
|
||||||
if titleAlertName != "CPU" && titleAlertName != "GPU" {
|
if titleAlertName != "CPU" {
|
||||||
titleAlertName = strings.ToLower(titleAlertName)
|
titleAlertName = strings.ToLower(titleAlertName)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -99,6 +99,7 @@ type FsStats struct {
|
|||||||
MaxDiskWriteBytes uint64 `json:"wbm,omitempty" cbor:"-"`
|
MaxDiskWriteBytes uint64 `json:"wbm,omitempty" cbor:"-"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
type NetIoStats struct {
|
type NetIoStats struct {
|
||||||
BytesRecv uint64
|
BytesRecv uint64
|
||||||
BytesSent uint64
|
BytesSent uint64
|
||||||
@@ -144,8 +145,8 @@ type Info struct {
|
|||||||
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
|
LoadAvg15 float64 `json:"l15,omitempty" cbor:"17,keyasint,omitempty"`
|
||||||
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
BandwidthBytes uint64 `json:"bb" cbor:"18,keyasint"`
|
||||||
// TODO: remove load fields in future release in favor of load avg array
|
// TODO: remove load fields in future release in favor of load avg array
|
||||||
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
LoadAvg [3]float64 `json:"la,omitempty" cbor:"19,keyasint"`
|
||||||
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
ConnectionType ConnectionType `json:"ct,omitempty" cbor:"20,keyasint,omitempty,omitzero"`
|
||||||
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
ExtraFsPct map[string]float64 `json:"efs,omitempty" cbor:"21,keyasint,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -356,19 +356,11 @@ function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
|
function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
|
||||||
const { info: sysInfo, status, id } = info.row.original
|
const { info: sysInfo, status } = info.row.original
|
||||||
const extraFs = Object.entries(sysInfo.efs ?? {})
|
|
||||||
|
|
||||||
// No extra disks - show basic meter
|
|
||||||
if (extraFs.length === 0) {
|
|
||||||
return TableCellWithMeter(info)
|
|
||||||
}
|
|
||||||
|
|
||||||
const rootDiskPct = sysInfo.dp
|
const rootDiskPct = sysInfo.dp
|
||||||
|
const extraFsData = sysInfo.efs
|
||||||
// sort extra disks by percentage descending
|
const extraFsCount = extraFsData ? Object.keys(extraFsData).length : 0
|
||||||
extraFs.sort((a, b) => b[1] - a[1])
|
|
||||||
|
|
||||||
function getMeterClass(pct: number) {
|
function getMeterClass(pct: number) {
|
||||||
const threshold = getMeterState(pct)
|
const threshold = getMeterState(pct)
|
||||||
return cn(
|
return cn(
|
||||||
@@ -380,42 +372,45 @@ function DiskCellWithMultiple(info: CellContext<SystemRecord, unknown>) {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// No extra disks - show simple meter
|
||||||
|
if (extraFsCount === 0) {
|
||||||
|
return TableCellWithMeter(info)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Has extra disks - show with tooltip
|
||||||
return (
|
return (
|
||||||
<Tooltip>
|
<Tooltip>
|
||||||
<TooltipTrigger asChild>
|
<TooltipTrigger asChild>
|
||||||
<Link href={getPagePath($router, "system", { id })} tabIndex={-1} className="flex flex-col gap-0.5 w-full relative z-10">
|
<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">
|
<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="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="flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
|
||||||
{/* Root disk */}
|
|
||||||
<span className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span>
|
<span className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span>
|
||||||
{/* Extra disks */}
|
{extraFsData && Object.entries(extraFsData).slice(0, 2).map(([_name, pct], index) => (
|
||||||
{extraFs.map(([_name, pct], index) => (
|
|
||||||
<span key={index} className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span>
|
<span key={index} className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span>
|
||||||
))}
|
))}
|
||||||
|
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent side="right" className="max-w-xs pb-2">
|
<TooltipContent side="right" className="max-w-xs pb-2">
|
||||||
<div className="grid gap-1.5">
|
<div className="flex flex-col gap-1.5">
|
||||||
<div className="grid gap-0.5">
|
<div className="flex flex-col gap-0.5">
|
||||||
<div className="text-[0.65rem] text-muted-foreground uppercase tracking-wide tabular-nums">{t`Root`}</div>
|
<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">
|
<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="min-w-7">{decimalString(rootDiskPct, rootDiskPct >= 10 ? 1 : 2)}%</span>
|
||||||
<span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden">
|
<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 className={getMeterClass(rootDiskPct)} style={{ width: `${rootDiskPct}%` }}></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{extraFs.map(([name, pct]) => {
|
{extraFsData && Object.entries(extraFsData).map(([name, pct]) => {
|
||||||
return (
|
return (
|
||||||
<div key={name} className="grid gap-0.5">
|
<div key={name} className="flex flex-col gap-0.5">
|
||||||
<div className="text-[0.65rem] max-w-40 text-muted-foreground uppercase tracking-wide truncate">{name}</div>
|
<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">
|
<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="min-w-7">{decimalString(pct, pct >= 10 ? 1 : 2)}%</span>
|
||||||
<span className="flex-1 min-w-12 grid bg-muted h-2.5 rounded-sm overflow-hidden">
|
<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 className={getMeterClass(pct)} style={{ width: `${pct}%` }}></span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: fr\n"
|
"Language: fr\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2025-11-11 19:25\n"
|
"PO-Revision-Date: 2025-10-28 22:59\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: French\n"
|
"Language-Team: French\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n > 1);\n"
|
||||||
@@ -990,7 +990,7 @@ msgstr "Réinitialiser le mot de passe"
|
|||||||
#: src/components/alerts-history-columns.tsx
|
#: src/components/alerts-history-columns.tsx
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
msgid "Resolved"
|
msgid "Resolved"
|
||||||
msgstr "Résolu"
|
msgstr "Résolution"
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "Resume"
|
msgid "Resume"
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ msgstr ""
|
|||||||
"Language: sv\n"
|
"Language: sv\n"
|
||||||
"Project-Id-Version: beszel\n"
|
"Project-Id-Version: beszel\n"
|
||||||
"Report-Msgid-Bugs-To: \n"
|
"Report-Msgid-Bugs-To: \n"
|
||||||
"PO-Revision-Date: 2025-11-10 01:57\n"
|
"PO-Revision-Date: 2025-10-28 23:00\n"
|
||||||
"Last-Translator: \n"
|
"Last-Translator: \n"
|
||||||
"Language-Team: Swedish\n"
|
"Language-Team: Swedish\n"
|
||||||
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
"Plural-Forms: nplurals=2; plural=(n != 1);\n"
|
||||||
@@ -113,11 +113,11 @@ msgstr "Justera visningsalternativ för diagram."
|
|||||||
#: src/components/command-palette.tsx
|
#: src/components/command-palette.tsx
|
||||||
#: src/components/command-palette.tsx
|
#: src/components/command-palette.tsx
|
||||||
msgid "Admin"
|
msgid "Admin"
|
||||||
msgstr "Admin"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "Agent"
|
msgid "Agent"
|
||||||
msgstr "Agent"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/command-palette.tsx
|
#: src/components/command-palette.tsx
|
||||||
#: src/components/routes/settings/alerts-history-data-table.tsx
|
#: src/components/routes/settings/alerts-history-data-table.tsx
|
||||||
@@ -448,7 +448,7 @@ msgstr "Urladdar"
|
|||||||
|
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
msgid "Disk"
|
msgid "Disk"
|
||||||
msgstr "Disk"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
msgid "Disk I/O"
|
msgid "Disk I/O"
|
||||||
@@ -625,7 +625,7 @@ msgstr "FreeBSD kommando"
|
|||||||
#. Context: Battery state
|
#. Context: Battery state
|
||||||
#: src/lib/i18n.ts
|
#: src/lib/i18n.ts
|
||||||
msgid "Full"
|
msgid "Full"
|
||||||
msgstr "Full"
|
msgstr ""
|
||||||
|
|
||||||
#. Context: General settings
|
#. Context: General settings
|
||||||
#: src/components/routes/settings/general.tsx
|
#: src/components/routes/settings/general.tsx
|
||||||
@@ -746,7 +746,7 @@ msgstr "Manuella installationsinstruktioner"
|
|||||||
#. Chart select field. Please try to keep this short.
|
#. Chart select field. Please try to keep this short.
|
||||||
#: src/components/routes/system.tsx
|
#: src/components/routes/system.tsx
|
||||||
msgid "Max 1 min"
|
msgid "Max 1 min"
|
||||||
msgstr "Max 1 min"
|
msgstr ""
|
||||||
|
|
||||||
#: src/components/containers-table/containers-table-columns.tsx
|
#: src/components/containers-table/containers-table-columns.tsx
|
||||||
#: src/components/systems-table/systems-table-columns.tsx
|
#: src/components/systems-table/systems-table-columns.tsx
|
||||||
@@ -938,7 +938,7 @@ msgstr "Vänligen logga in på ditt konto"
|
|||||||
|
|
||||||
#: src/components/add-system.tsx
|
#: src/components/add-system.tsx
|
||||||
msgid "Port"
|
msgid "Port"
|
||||||
msgstr "Port"
|
msgstr ""
|
||||||
|
|
||||||
#. Power On Time
|
#. Power On Time
|
||||||
#: src/components/routes/system/smart-table.tsx
|
#: src/components/routes/system/smart-table.tsx
|
||||||
@@ -1183,7 +1183,7 @@ msgstr "Växla tema"
|
|||||||
#: src/components/add-system.tsx
|
#: src/components/add-system.tsx
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Token"
|
msgid "Token"
|
||||||
msgstr "Nyckel"
|
msgstr "Token"
|
||||||
|
|
||||||
#: src/components/command-palette.tsx
|
#: src/components/command-palette.tsx
|
||||||
#: src/components/routes/settings/layout.tsx
|
#: src/components/routes/settings/layout.tsx
|
||||||
@@ -1260,7 +1260,7 @@ msgstr "Enhetsinställningar"
|
|||||||
#: src/components/command-palette.tsx
|
#: src/components/command-palette.tsx
|
||||||
#: src/components/routes/settings/tokens-fingerprints.tsx
|
#: src/components/routes/settings/tokens-fingerprints.tsx
|
||||||
msgid "Universal token"
|
msgid "Universal token"
|
||||||
msgstr "Universell nyckel"
|
msgstr "Universal token"
|
||||||
|
|
||||||
#. Context: Battery state
|
#. Context: Battery state
|
||||||
#: src/lib/i18n.ts
|
#: src/lib/i18n.ts
|
||||||
|
|||||||
1
internal/site/src/types.d.ts
vendored
1
internal/site/src/types.d.ts
vendored
@@ -81,6 +81,7 @@ export interface SystemInfo {
|
|||||||
efs?: Record<string, number>
|
efs?: Record<string, number>
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export interface SystemStats {
|
export interface SystemStats {
|
||||||
/** cpu percent */
|
/** cpu percent */
|
||||||
cpu: number
|
cpu: number
|
||||||
|
|||||||
Reference in New Issue
Block a user