Compare commits

...

9 Commits

Author SHA1 Message Date
henrygd
5f4d3dc730 0.12.5 release :) 2025-08-26 21:04:46 -04:00
henrygd
8fa9aece63 change long german translation 2025-08-26 20:50:35 -04:00
henrygd
2f1a022e2a refactor: use width for meters instead of scale 2025-08-26 20:49:31 -04:00
henrygd
4815cd29bc ghupdate: rename plugin struct 2025-08-26 18:41:42 -04:00
henrygd
e49bfaf5d7 downgrade gopsutil to fix freebsd bug (#1083) 2025-08-26 18:40:32 -04:00
henrygd
b13915b76f freebsd: fix battery-related bug (#1081) 2025-08-26 18:39:42 -04:00
henrygd
e2a57dc43b update tooltip component for tailwind 4 2025-08-26 16:16:38 -04:00
henrygd
7222224b40 add battery to supported metrics 2025-08-25 23:15:19 -04:00
henrygd
02ff475b84 improve language toggle selected style 2025-08-25 22:14:48 -04:00
17 changed files with 135 additions and 82 deletions

View File

@@ -15,7 +15,7 @@ require (
github.com/nicholas-fedor/shoutrrr v0.8.17
github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.29.3
github.com/shirou/gopsutil/v4 v4.25.7
github.com/shirou/gopsutil/v4 v4.25.6
github.com/spf13/cast v1.9.2
github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.11.0

View File

@@ -97,8 +97,8 @@ github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qq
github.com/rogpeppe/go-internal v1.9.0 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
github.com/shirou/gopsutil/v4 v4.25.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.6/go.mod h1:PfybzyydfZcN+JMMjkF6Zb8Mq1A/VcogFFg7hj50W9c=
github.com/spf13/cast v1.9.2 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=

View File

@@ -36,7 +36,6 @@ type Agent struct {
server *ssh.Server // SSH server
dataDir string // Directory for persisting data
keys []gossh.PublicKey // SSH public keys
hasBattery bool // true if agent has access to battery stats
}
// NewAgent creates a new agent with the given data directory for persisting data.

View File

@@ -1,24 +0,0 @@
package agent
import "github.com/distatus/battery"
// getBatteryStats returns the current battery percent and charge state
func getBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
batteries, err := battery.GetAll()
if err != nil || len(batteries) == 0 {
return batteryPercent, batteryState, err
}
totalCapacity := float64(0)
totalCharge := float64(0)
for _, bat := range batteries {
if bat.Design != 0 {
totalCapacity += bat.Design
} else {
totalCapacity += bat.Full
}
totalCharge += bat.Current
}
batteryPercent = uint8(totalCharge / totalCapacity * 100)
batteryState = uint8(batteries[0].State.Raw)
return batteryPercent, batteryState, nil
}

View File

@@ -0,0 +1,53 @@
//go:build !freebsd
// Package battery provides functions to check if the system has a battery and to get the battery stats.
package battery
import (
"errors"
"log/slog"
"github.com/distatus/battery"
)
var systemHasBattery = false
var haveCheckedBattery = false
// HasReadableBattery checks if the system has a battery and returns true if it does.
func HasReadableBattery() bool {
if haveCheckedBattery {
return systemHasBattery
}
haveCheckedBattery = true
bat, err := battery.Get(0)
if err == nil && bat != nil {
systemHasBattery = true
} else {
slog.Debug("No battery found", "err", err)
}
return systemHasBattery
}
// GetBatteryStats returns the current battery percent and charge state
func GetBatteryStats() (batteryPercent uint8, batteryState uint8, err error) {
if !systemHasBattery {
return batteryPercent, batteryState, errors.ErrUnsupported
}
batteries, err := battery.GetAll()
if err != nil || len(batteries) == 0 {
return batteryPercent, batteryState, err
}
totalCapacity := float64(0)
totalCharge := float64(0)
for _, bat := range batteries {
if bat.Design != 0 {
totalCapacity += bat.Design
} else {
totalCapacity += bat.Full
}
totalCharge += bat.Current
}
batteryPercent = uint8(totalCharge / totalCapacity * 100)
batteryState = uint8(batteries[0].State.Raw)
return batteryPercent, batteryState, nil
}

View File

@@ -0,0 +1,13 @@
//go:build freebsd
package battery
import "errors"
func HasReadableBattery() bool {
return false
}
func GetBatteryStats() (uint8, uint8, error) {
return 0, 0, errors.ErrUnsupported
}

View File

@@ -2,6 +2,7 @@ package agent
import (
"beszel"
"beszel/internal/agent/battery"
"beszel/internal/entities/system"
"bufio"
"fmt"
@@ -64,13 +65,6 @@ func (a *Agent) initializeSystemInfo() {
} else {
a.zfs = true
}
// battery
if _, _, err := getBatteryStats(); err != nil {
slog.Debug("No battery detected", "err", err)
} else {
a.hasBattery = true
}
}
// Returns current info, stats about the host system
@@ -78,8 +72,8 @@ func (a *Agent) getSystemStats() system.Stats {
systemStats := system.Stats{}
// battery
if a.hasBattery {
systemStats.Battery[0], systemStats.Battery[1], _ = getBatteryStats()
if battery.HasReadableBattery() {
systemStats.Battery[0], systemStats.Battery[1], _ = battery.GetBatteryStats()
}
// cpu percent

View File

@@ -66,8 +66,13 @@ type Config struct {
DataDir string
}
type updater struct {
config Config
currentVersion string
}
func Update(config Config) (updated bool, err error) {
p := &plugin{
p := &updater{
currentVersion: beszel.Version,
config: config,
}
@@ -75,12 +80,7 @@ func Update(config Config) (updated bool, err error) {
return p.update()
}
type plugin struct {
config Config
currentVersion string
}
func (p *plugin) update() (updated bool, err error) {
func (p *updater) update() (updated bool, err error) {
ColorPrint(ColorYellow, "Fetching release information...")
if p.config.DataDir == "" {

View File

@@ -1,12 +1,12 @@
{
"name": "beszel",
"version": "0.12.4",
"version": "0.12.5",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beszel",
"version": "0.12.4",
"version": "0.12.5",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",

View File

@@ -1,7 +1,7 @@
{
"name": "beszel",
"private": true,
"version": "0.12.4",
"version": "0.12.5",
"type": "module",
"scripts": {
"dev": "vite",

View File

@@ -22,7 +22,7 @@ export function LangToggle() {
{languages.map(({ lang, label, e }) => (
<DropdownMenuItem
key={lang}
className={cn("px-2.5 flex gap-2.5", lang === i18n.locale && "font-semibold")}
className={cn("px-2.5 flex gap-2.5 cursor-pointer", lang === i18n.locale && "bg-accent/70 font-medium")}
onClick={() => dynamicActivate(lang)}
>
<span>{e}</span> {label}

View File

@@ -318,22 +318,18 @@ function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
const val = Number(info.getValue()) || 0
const threshold = getMeterState(val)
const meterClass = cn(
"h-full",
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
)
return (
<div className="flex gap-2 items-center tabular-nums tracking-tight">
<span className="min-w-8">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
<span className="grow min-w-8 block bg-muted h-[1em] relative rounded-sm overflow-hidden">
<span
className={cn(
"absolute inset-0 w-full h-full origin-left",
(info.row.original.status !== SystemStatus.Up && STATUS_COLORS.paused) ||
(threshold === MeterState.Good && STATUS_COLORS.up) ||
(threshold === MeterState.Warn && STATUS_COLORS.pending) ||
STATUS_COLORS.down
)}
style={{
transform: `scalex(${val / 100})`,
}}
></span>
<span className="grow min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
<span className={meterClass} style={{ width: `${val}%` }}></span>
</span>
</div>
)

View File

@@ -15,7 +15,7 @@ export function InputCopy({ value, id, name }: { value: string; id: string; name
}
></div>
<TooltipProvider delayDuration={100} disableHoverableContent>
<Tooltip>
<Tooltip disableHoverableContent={true}>
<TooltipTrigger asChild>
<Button
type="button"

View File

@@ -3,26 +3,47 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils"
const TooltipProvider = TooltipPrimitive.Provider
function TooltipProvider({ delayDuration = 0, ...props }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
return <TooltipPrimitive.Provider data-slot="tooltip-provider" delayDuration={delayDuration} {...props} />
}
const Tooltip = TooltipPrimitive.Root
function Tooltip({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
return (
<TooltipProvider>
<TooltipPrimitive.Root data-slot="tooltip" {...props} />
</TooltipProvider>
)
}
const TooltipTrigger = TooltipPrimitive.Trigger
function TooltipTrigger({ ...props }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
}
const TooltipContent = React.forwardRef<
React.ElementRef<typeof TooltipPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content>
>(({ className, sideOffset = 4, ...props }, ref) => (
<TooltipPrimitive.Content
ref={ref}
sideOffset={sideOffset}
className={cn(
"z-50 overflow-hidden rounded-md border bg-popover px-3 py-1.5 text-sm text-popover-foreground shadow-md animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2",
className
)}
{...props}
/>
))
TooltipContent.displayName = TooltipPrimitive.Content.displayName
function TooltipContent({
className,
sideOffset = 0,
children,
...props
}: React.ComponentProps<typeof TooltipPrimitive.Content>) {
return (
<TooltipPrimitive.Portal>
<TooltipPrimitive.Content
data-slot="tooltip-content"
sideOffset={sideOffset}
className={cn(
"bg-popover text-popover-foreground border animate-in fade-in-0 zoom-in-95 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[side=bottom]:slide-in-from-top-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 z-50 w-fit origin-(--radix-tooltip-content-transform-origin) rounded-md px-3 py-1.5 text-sm text-balance",
className
)}
{...props}
>
{children}
<TooltipPrimitive.Arrow
className="bg-popover border z-50 fill-popover size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] will-change-transform"
style={{ clipPath: "inset(25% 0 0 25%)" }}
/>
</TooltipPrimitive.Content>
</TooltipPrimitive.Portal>
)
}
export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

@@ -601,7 +601,7 @@ msgstr "Durchschnittliche Systemlast 5 Min"
#. Short label for load average
#: src/components/systems-table/systems-table-columns.tsx
msgid "Load Avg"
msgstr "Durchschnittliche Last"
msgstr "Systemlast"
#: src/components/navbar.tsx
msgid "Log Out"

View File

@@ -3,7 +3,7 @@ package beszel
import "github.com/blang/semver"
const (
Version = "0.12.4"
Version = "0.12.5"
AppName = "beszel"
)

View File

@@ -49,6 +49,7 @@ The [quick start guide](https://beszel.dev/guide/getting-started) and other docu
- **Load average** - Host system.
- **Temperature** - Host system sensors.
- **GPU usage / temperature / power draw** - Nvidia and AMD only. Must use binary agent.
- **Battery** - Host system battery charge.
## Help and discussion