Compare commits

...

14 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
henrygd
09cd8d0db9 add check for battery array length (#1076) 2025-08-25 21:34:34 -04:00
henrygd
36f1a0c53b update synctest.run to synctest.test 2025-08-25 21:25:19 -04:00
henrygd
0b0e94e045 remove pocketbase imports from ghupdate 2025-08-25 21:16:43 -04:00
henrygd
20ca6edf81 0.12.4 release :) 2025-08-25 20:23:38 -04:00
henrygd
1990f8c6df use mirror for asset download in update command (#1035) 2025-08-25 20:18:31 -04:00
23 changed files with 318 additions and 151 deletions

View File

@@ -15,7 +15,7 @@ require (
github.com/nicholas-fedor/shoutrrr v0.8.17 github.com/nicholas-fedor/shoutrrr v0.8.17
github.com/pocketbase/dbx v1.11.0 github.com/pocketbase/dbx v1.11.0
github.com/pocketbase/pocketbase v0.29.3 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/cast v1.9.2
github.com/spf13/cobra v1.9.1 github.com/spf13/cobra v1.9.1
github.com/stretchr/testify v1.11.0 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 h1:73kH8U+JUqXU8lRuOHeVHaa/SZPifC7BkcraZVejAe8=
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs= 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/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.6 h1:kLysI2JsKorfaFPcYmcJqbzROzsBWEOAtw6A7dIfqXs=
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U= 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 h1:SsGfm7M8QOFtEzumm7UZrZdLLquNdzFYfIbEXntcFbE=
github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo= github.com/spf13/cast v1.9.2/go.mod h1:jNfB8QC9IA6ZuY2ZjDp0KtFO2LZZlg4S/7bzP6qqeHo=
github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo= github.com/spf13/cobra v1.9.1 h1:CXSaggrXdbHK9CF+8ywj8Amf7PBRmPCOJugH954Nnlo=

View File

@@ -36,7 +36,6 @@ type Agent struct {
server *ssh.Server // SSH server server *ssh.Server // SSH server
dataDir string // Directory for persisting data dataDir string // Directory for persisting data
keys []gossh.PublicKey // SSH public keys 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. // NewAgent creates a new agent with the given data directory for persisting data.

View File

@@ -14,7 +14,7 @@ import (
) )
func TestSessionCache_GetSet(t *testing.T) { func TestSessionCache_GetSet(t *testing.T) {
synctest.Run(func() { synctest.Test(t, func(t *testing.T) {
cache := NewSessionCache(69 * time.Second) cache := NewSessionCache(69 * time.Second)
testData := &system.CombinedData{ testData := &system.CombinedData{

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 {
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

@@ -39,7 +39,7 @@ func TestHealth(t *testing.T) {
// This test uses synctest to simulate time passing. // This test uses synctest to simulate time passing.
// NOTE: This test requires GOEXPERIMENT=synctest to run. // NOTE: This test requires GOEXPERIMENT=synctest to run.
t.Run("check with simulated time", func(t *testing.T) { t.Run("check with simulated time", func(t *testing.T) {
synctest.Run(func() { synctest.Test(t, func(t *testing.T) {
// Update the file to set the initial timestamp. // Update the file to set the initial timestamp.
require.NoError(t, Update(), "Update() failed inside synctest") require.NoError(t, Update(), "Update() failed inside synctest")

View File

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

View File

@@ -0,0 +1,140 @@
package ghupdate
import (
"archive/tar"
"archive/zip"
"compress/gzip"
"fmt"
"io"
"os"
"path/filepath"
"strings"
)
// extract extracts an archive file to the destination directory.
// Supports .zip and .tar.gz files based on the file extension.
func extract(srcPath, destDir string) error {
if strings.HasSuffix(srcPath, ".tar.gz") {
return extractTarGz(srcPath, destDir)
}
// Default to zip extraction
return extractZip(srcPath, destDir)
}
// extractTarGz extracts a tar.gz archive to the destination directory.
func extractTarGz(srcPath, destDir string) error {
src, err := os.Open(srcPath)
if err != nil {
return err
}
defer src.Close()
gz, err := gzip.NewReader(src)
if err != nil {
return err
}
defer gz.Close()
tr := tar.NewReader(gz)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if header.Typeflag == tar.TypeDir {
if err := os.MkdirAll(filepath.Join(destDir, header.Name), 0755); err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(filepath.Join(destDir, header.Name)), 0755); err != nil {
return err
}
outFile, err := os.Create(filepath.Join(destDir, header.Name))
if err != nil {
return err
}
if _, err := io.Copy(outFile, tr); err != nil {
outFile.Close()
return err
}
outFile.Close()
}
return nil
}
// extractZip extracts the zip archive at "src" to "dest".
//
// Note that only dirs and regular files will be extracted.
// Symbolic links, named pipes, sockets, or any other irregular files
// are skipped because they come with too many edge cases and ambiguities.
func extractZip(src, dest string) error {
zr, err := zip.OpenReader(src)
if err != nil {
return err
}
defer zr.Close()
// normalize dest path to check later for Zip Slip
dest = filepath.Clean(dest) + string(os.PathSeparator)
for _, f := range zr.File {
err := extractFile(f, dest)
if err != nil {
return err
}
}
return nil
}
// extractFile extracts the provided zipFile into "basePath/zipFileName" path,
// creating all the necessary path directories.
func extractFile(zipFile *zip.File, basePath string) error {
path := filepath.Join(basePath, zipFile.Name)
// check for Zip Slip
if !strings.HasPrefix(path, basePath) {
return fmt.Errorf("invalid file path: %s", path)
}
r, err := zipFile.Open()
if err != nil {
return err
}
defer r.Close()
// allow only dirs or regular files
if zipFile.FileInfo().IsDir() {
if err := os.MkdirAll(path, os.ModePerm); err != nil {
return err
}
} else if zipFile.FileInfo().Mode().IsRegular() {
// ensure that the file path directories are created
if err := os.MkdirAll(filepath.Dir(path), os.ModePerm); err != nil {
return err
}
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, zipFile.Mode())
if err != nil {
return err
}
defer f.Close()
_, err = io.Copy(f, r)
if err != nil {
return err
}
}
return nil
}

View File

@@ -4,9 +4,7 @@
package ghupdate package ghupdate
import ( import (
"archive/tar"
"beszel" "beszel"
"compress/gzip"
"context" "context"
"encoding/json" "encoding/json"
"fmt" "fmt"
@@ -19,8 +17,6 @@ import (
"strings" "strings"
"github.com/blang/semver" "github.com/blang/semver"
"github.com/pocketbase/pocketbase/core"
"github.com/pocketbase/pocketbase/tools/archive"
) )
// Minimal color functions using ANSI escape codes // Minimal color functions using ANSI escape codes
@@ -70,8 +66,13 @@ type Config struct {
DataDir string DataDir string
} }
type updater struct {
config Config
currentVersion string
}
func Update(config Config) (updated bool, err error) { func Update(config Config) (updated bool, err error) {
p := &plugin{ p := &updater{
currentVersion: beszel.Version, currentVersion: beszel.Version,
config: config, config: config,
} }
@@ -79,12 +80,7 @@ func Update(config Config) (updated bool, err error) {
return p.update() return p.update()
} }
type plugin struct { func (p *updater) update() (updated bool, err error) {
config Config
currentVersion string
}
func (p *plugin) update() (updated bool, err error) {
ColorPrint(ColorYellow, "Fetching release information...") ColorPrint(ColorYellow, "Fetching release information...")
if p.config.DataDir == "" { if p.config.DataDir == "" {
@@ -108,6 +104,8 @@ func (p *plugin) update() (updated bool, err error) {
} }
var latest *release var latest *release
var useMirror bool
latest, err = fetchLatestRelease( latest, err = fetchLatestRelease(
p.config.Context, p.config.Context,
p.config.HttpClient, p.config.HttpClient,
@@ -116,6 +114,7 @@ func (p *plugin) update() (updated bool, err error) {
// if the first fetch fails, try the beszel.dev API (fallback for China) // if the first fetch fails, try the beszel.dev API (fallback for China)
if err != nil { if err != nil {
ColorPrint(ColorYellow, "Failed to fetch release. Trying beszel.dev mirror...") ColorPrint(ColorYellow, "Failed to fetch release. Trying beszel.dev mirror...")
useMirror = true
latest, err = fetchLatestRelease( latest, err = fetchLatestRelease(
p.config.Context, p.config.Context,
p.config.HttpClient, p.config.HttpClient,
@@ -140,14 +139,14 @@ func (p *plugin) update() (updated bool, err error) {
return false, err return false, err
} }
releaseDir := filepath.Join(p.config.DataDir, core.LocalTempDirName) releaseDir := filepath.Join(p.config.DataDir, ".beszel_update")
defer os.RemoveAll(releaseDir) defer os.RemoveAll(releaseDir)
ColorPrintf(ColorYellow, "Downloading %s...", asset.Name) ColorPrintf(ColorYellow, "Downloading %s...", asset.Name)
// download the release asset // download the release asset
assetPath := filepath.Join(releaseDir, asset.Name) assetPath := filepath.Join(releaseDir, asset.Name)
if err := downloadFile(p.config.Context, p.config.HttpClient, asset.DownloadUrl, assetPath); err != nil { if err := downloadFile(p.config.Context, p.config.HttpClient, asset.DownloadUrl, assetPath, useMirror); err != nil {
return false, err return false, err
} }
@@ -156,15 +155,9 @@ func (p *plugin) update() (updated bool, err error) {
extractDir := filepath.Join(releaseDir, "extracted_"+asset.Name) extractDir := filepath.Join(releaseDir, "extracted_"+asset.Name)
defer os.RemoveAll(extractDir) defer os.RemoveAll(extractDir)
// Extract based on file extension // Extract the archive (automatically detects format)
if strings.HasSuffix(asset.Name, ".tar.gz") { if err := extract(assetPath, extractDir); err != nil {
if err := extractTarGz(assetPath, extractDir); err != nil { return false, err
return false, err
}
} else {
if err := archive.Extract(assetPath, extractDir); err != nil {
return false, err
}
} }
ColorPrint(ColorYellow, "Replacing the executable...") ColorPrint(ColorYellow, "Replacing the executable...")
@@ -275,7 +268,11 @@ func downloadFile(
client HttpClient, client HttpClient,
url string, url string,
destPath string, destPath string,
useMirror bool,
) error { ) error {
if useMirror {
url = strings.Replace(url, "github.com", "gh.beszel.dev", 1)
}
req, err := http.NewRequestWithContext(ctx, "GET", url, nil) req, err := http.NewRequestWithContext(ctx, "GET", url, nil)
if err != nil { if err != nil {
return err return err
@@ -350,52 +347,3 @@ func archiveSuffix(binaryName, goos, goarch string) string {
} }
return fmt.Sprintf("%s_%s_%s.tar.gz", binaryName, goos, goarch) return fmt.Sprintf("%s_%s_%s.tar.gz", binaryName, goos, goarch)
} }
func extractTarGz(srcPath, destDir string) error {
src, err := os.Open(srcPath)
if err != nil {
return err
}
defer src.Close()
gz, err := gzip.NewReader(src)
if err != nil {
return err
}
defer gz.Close()
tr := tar.NewReader(gz)
for {
header, err := tr.Next()
if err == io.EOF {
break
}
if err != nil {
return err
}
if header.Typeflag == tar.TypeDir {
if err := os.MkdirAll(filepath.Join(destDir, header.Name), 0755); err != nil {
return err
}
continue
}
if err := os.MkdirAll(filepath.Dir(filepath.Join(destDir, header.Name)), 0755); err != nil {
return err
}
outFile, err := os.Create(filepath.Join(destDir, header.Name))
if err != nil {
return err
}
defer outFile.Close()
if _, err := io.Copy(outFile, tr); err != nil {
return err
}
}
return nil
}

View File

@@ -1,6 +1,9 @@
package ghupdate package ghupdate
import "testing" import (
"path/filepath"
"testing"
)
func TestReleaseFindAssetBySuffix(t *testing.T) { func TestReleaseFindAssetBySuffix(t *testing.T) {
r := release{ r := release{
@@ -21,3 +24,22 @@ func TestReleaseFindAssetBySuffix(t *testing.T) {
t.Fatalf("Expected asset with id %d, got %v", 2, asset) t.Fatalf("Expected asset with id %d, got %v", 2, asset)
} }
} }
func TestExtractFailure(t *testing.T) {
testDir := t.TempDir()
// Test with missing zip file
missingZipPath := filepath.Join(testDir, "missing_test.zip")
extractedPath := filepath.Join(testDir, "zip_extract")
if err := extract(missingZipPath, extractedPath); err == nil {
t.Fatal("Expected Extract to fail due to missing zip file")
}
// Test with missing tar.gz file
missingTarPath := filepath.Join(testDir, "missing_test.tar.gz")
if err := extract(missingTarPath, extractedPath); err == nil {
t.Fatal("Expected Extract to fail due to missing tar.gz file")
}
}

View File

@@ -29,7 +29,7 @@ func TestSystemManagerNew(t *testing.T) {
user, err := tests.CreateUser(hub, "test@test.com", "testtesttest") user, err := tests.CreateUser(hub, "test@test.com", "testtesttest")
require.NoError(t, err) require.NoError(t, err)
synctest.Run(func() { synctest.Test(t, func(t *testing.T) {
sm.Initialize() sm.Initialize()
record, err := tests.CreateRecord(hub, "systems", map[string]any{ record, err := tests.CreateRecord(hub, "systems", map[string]any{
@@ -110,9 +110,11 @@ func TestSystemManagerNew(t *testing.T) {
err = hub.Delete(record) err = hub.Delete(record)
require.NoError(t, err) require.NoError(t, err)
assert.False(t, sm.HasSystem(record.Id), "System should not exist in the store after deletion") assert.False(t, sm.HasSystem(record.Id), "System should not exist in the store after deletion")
})
testOld(t, hub) testOld(t, hub)
synctest.Test(t, func(t *testing.T) {
time.Sleep(time.Second) time.Sleep(time.Second)
synctest.Wait() synctest.Wait()

View File

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

View File

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

View File

@@ -22,7 +22,7 @@ export function LangToggle() {
{languages.map(({ lang, label, e }) => ( {languages.map(({ lang, label, e }) => (
<DropdownMenuItem <DropdownMenuItem
key={lang} 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)} onClick={() => dynamicActivate(lang)}
> >
<span>{e}</span> {label} <span>{e}</span> {label}

View File

@@ -318,22 +318,18 @@ function sortableHeader(context: HeaderContext<SystemRecord, unknown>) {
function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) { function TableCellWithMeter(info: CellContext<SystemRecord, unknown>) {
const val = Number(info.getValue()) || 0 const val = Number(info.getValue()) || 0
const threshold = getMeterState(val) 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 ( return (
<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">{decimalString(val, val >= 10 ? 1 : 2)}%</span> <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="grow min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
<span <span className={meterClass} style={{ width: `${val}%` }}></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> </span>
</div> </div>
) )

View File

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

View File

@@ -3,26 +3,47 @@ import * as TooltipPrimitive from "@radix-ui/react-tooltip"
import { cn } from "@/lib/utils" 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< function TooltipContent({
React.ElementRef<typeof TooltipPrimitive.Content>, className,
React.ComponentPropsWithoutRef<typeof TooltipPrimitive.Content> sideOffset = 0,
>(({ className, sideOffset = 4, ...props }, ref) => ( children,
<TooltipPrimitive.Content ...props
ref={ref} }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
sideOffset={sideOffset} return (
className={cn( <TooltipPrimitive.Portal>
"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", <TooltipPrimitive.Content
className data-slot="tooltip-content"
)} sideOffset={sideOffset}
{...props} 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
TooltipContent.displayName = TooltipPrimitive.Content.displayName )}
{...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 } export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }

View File

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

View File

@@ -3,7 +3,7 @@ package beszel
import "github.com/blang/semver" import "github.com/blang/semver"
const ( const (
Version = "0.12.3" Version = "0.12.5"
AppName = "beszel" 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. - **Load average** - Host system.
- **Temperature** - Host system sensors. - **Temperature** - Host system sensors.
- **GPU usage / temperature / power draw** - Nvidia and AMD only. Must use binary agent. - **GPU usage / temperature / power draw** - Nvidia and AMD only. Must use binary agent.
- **Battery** - Host system battery charge.
## Help and discussion ## Help and discussion

View File

@@ -2,6 +2,8 @@
- Add battery charge monitoring. - Add battery charge monitoring.
- Add fallback mirror to the `update` commands. (#1035)
- Fix blank token field in insecure contexts. - Fix blank token field in insecure contexts.
- Allow opening internal router links in new tab. - Allow opening internal router links in new tab.