Compare commits

...

25 Commits

Author SHA1 Message Date
henrygd
0c32be3bea 0.12.6 release :) 2025-08-29 17:24:45 -04:00
henrygd
81d43fbf6e refactor: small style improvements 2025-08-29 17:23:47 -04:00
henrygd
96f441de40 Virtualize All Systems table to improve performance with hundreds of systems (#1100)
- Also truncate long system names in tables and alerts sheet. (#1104)
2025-08-29 16:16:45 -04:00
henrygd
0e95caaee9 update command ui component 2025-08-29 15:04:26 -04:00
Sven van Ginkel
7697a12b42 fix alignment for metrics (#1109) 2025-08-29 14:00:17 -04:00
henrygd
94245a9ba4 fix update mirror and make opt-in with --china-mirrors (#1035) 2025-08-29 13:46:24 -04:00
henrygd
b084814aea auth form: fix border style and add theme toggle 2025-08-28 21:17:44 -04:00
Impact
cce74246ee Use older cuda image for increased compatibility (#1103) 2025-08-28 20:49:52 -04:00
henrygd
a3420b8c67 add max 1 min memory 2025-08-28 20:07:22 -04:00
henrygd
e1bb17ee9e update locale files 2025-08-28 18:23:40 -04:00
henrygd
52983f60b7 refactor: add api module and page preloading 2025-08-28 18:23:24 -04:00
henrygd
1f053fd85d update 2025-08-28 17:31:18 -04:00
Sven van Ginkel
a989d121d3 [Feature] Add Status Filtering to Systems Table (#927) 2025-08-28 17:30:44 -04:00
Sven van Ginkel
50d2406423 [Bug] Fix system table in Safari (#1092)
Co-authored-by: henrygd <hank@henrygd.me>
2025-08-28 12:07:27 -04:00
Sven van Ginkel
059d2d0a5b Add missing os.Chmod step to hub update command (#1093) 2025-08-27 13:00:03 -04:00
henrygd
621bef30b5 update changelog 2025-08-26 21:26:18 -04:00
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
84 changed files with 1282 additions and 636 deletions

View File

@@ -4,12 +4,12 @@ import (
"beszel"
"beszel/internal/agent"
"beszel/internal/agent/health"
"flag"
"fmt"
"log"
"os"
"strings"
"github.com/spf13/pflag"
"golang.org/x/crypto/ssh"
)
@@ -17,43 +17,24 @@ import (
type cmdOptions struct {
key string // key is the public key(s) for SSH authentication.
listen string // listen is the address or port to listen on.
// TODO: add hubURL and token
// hubURL string // hubURL is the URL of the hub to use.
// token string // token is the token to use for authentication.
}
// parse parses the command line flags and populates the config struct.
// It returns true if a subcommand was handled and the program should exit.
func (opts *cmdOptions) parse() bool {
flag.StringVar(&opts.key, "key", "", "Public key(s) for SSH authentication")
flag.StringVar(&opts.listen, "listen", "", "Address or port to listen on")
flag.Usage = func() {
builder := strings.Builder{}
builder.WriteString("Usage: ")
builder.WriteString(os.Args[0])
builder.WriteString(" [command] [flags]\n")
builder.WriteString("\nCommands:\n")
builder.WriteString(" health Check if the agent is running\n")
builder.WriteString(" help Display this help message\n")
builder.WriteString(" update Update to the latest version\n")
builder.WriteString("\nFlags:\n")
fmt.Print(builder.String())
flag.PrintDefaults()
}
subcommand := ""
if len(os.Args) > 1 {
subcommand = os.Args[1]
}
// Subcommands that don't require any pflag parsing
switch subcommand {
case "-v", "version":
fmt.Println(beszel.AppName+"-agent", beszel.Version)
return true
case "help":
flag.Usage()
return true
case "update":
agent.Update()
return true
case "health":
err := health.Check()
if err != nil {
@@ -63,7 +44,57 @@ func (opts *cmdOptions) parse() bool {
return true
}
flag.Parse()
// pflag.CommandLine.ParseErrorsWhitelist.UnknownFlags = true
pflag.StringVarP(&opts.key, "key", "k", "", "Public key(s) for SSH authentication")
pflag.StringVarP(&opts.listen, "listen", "l", "", "Address or port to listen on")
// pflag.StringVarP(&opts.hubURL, "hub-url", "u", "", "URL of the hub to use")
// pflag.StringVarP(&opts.token, "token", "t", "", "Token to use for authentication")
chinaMirrors := pflag.BoolP("china-mirrors", "c", false, "Use mirror for update (gh.beszel.dev) instead of GitHub")
help := pflag.BoolP("help", "h", false, "Show this help message")
// Convert old single-dash long flags to double-dash for backward compatibility
flagsToConvert := []string{"key", "listen"}
for i, arg := range os.Args {
for _, flag := range flagsToConvert {
singleDash := "-" + flag
doubleDash := "--" + flag
if arg == singleDash {
os.Args[i] = doubleDash
break
} else if strings.HasPrefix(arg, singleDash+"=") {
os.Args[i] = doubleDash + arg[len(singleDash):]
break
}
}
}
pflag.Usage = func() {
builder := strings.Builder{}
builder.WriteString("Usage: ")
builder.WriteString(os.Args[0])
builder.WriteString(" [command] [flags]\n")
builder.WriteString("\nCommands:\n")
builder.WriteString(" health Check if the agent is running\n")
// builder.WriteString(" help Display this help message\n")
builder.WriteString(" update Update to the latest version\n")
builder.WriteString("\nFlags:\n")
fmt.Print(builder.String())
pflag.PrintDefaults()
}
// Parse all arguments with pflag
pflag.Parse()
// Must run after pflag.Parse()
switch {
case *help || subcommand == "help":
pflag.Usage()
return true
case subcommand == "update":
agent.Update(*chinaMirrors)
return true
}
return false
}

View File

@@ -3,11 +3,11 @@ package main
import (
"beszel/internal/agent"
"crypto/ed25519"
"flag"
"os"
"path/filepath"
"testing"
"github.com/spf13/pflag"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"golang.org/x/crypto/ssh"
@@ -245,7 +245,7 @@ func TestParseFlags(t *testing.T) {
oldArgs := os.Args
defer func() {
os.Args = oldArgs
flag.CommandLine = flag.NewFlagSet(os.Args[0], flag.ExitOnError)
pflag.CommandLine = pflag.NewFlagSet(os.Args[0], pflag.ExitOnError)
}()
tests := []struct {
@@ -269,6 +269,22 @@ func TestParseFlags(t *testing.T) {
listen: "",
},
},
{
name: "key flag double dash",
args: []string{"cmd", "--key", "testkey"},
expected: cmdOptions{
key: "testkey",
listen: "",
},
},
{
name: "key flag short",
args: []string{"cmd", "-k", "testkey"},
expected: cmdOptions{
key: "testkey",
listen: "",
},
},
{
name: "addr flag only",
args: []string{"cmd", "-listen", ":8080"},
@@ -277,6 +293,22 @@ func TestParseFlags(t *testing.T) {
listen: ":8080",
},
},
{
name: "addr flag double dash",
args: []string{"cmd", "--listen", ":8080"},
expected: cmdOptions{
key: "",
listen: ":8080",
},
},
{
name: "addr flag short",
args: []string{"cmd", "-l", ":8080"},
expected: cmdOptions{
key: "",
listen: ":8080",
},
},
{
name: "both flags",
args: []string{"cmd", "-key", "testkey", "-listen", ":8080"},
@@ -290,12 +322,12 @@ func TestParseFlags(t *testing.T) {
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// Reset flags for each test
flag.CommandLine = flag.NewFlagSet(tt.args[0], flag.ExitOnError)
pflag.CommandLine = pflag.NewFlagSet(tt.args[0], pflag.ExitOnError)
os.Args = tt.args
var opts cmdOptions
opts.parse()
flag.Parse()
pflag.Parse()
assert.Equal(t, tt.expected, opts)
})

View File

@@ -45,11 +45,13 @@ func getBaseApp() *pocketbase.PocketBase {
baseApp.RootCmd.Use = beszel.AppName
baseApp.RootCmd.Short = ""
// add update command
baseApp.RootCmd.AddCommand(&cobra.Command{
updateCmd := &cobra.Command{
Use: "update",
Short: "Update " + beszel.AppName + " to the latest version",
Run: hub.Update,
})
}
updateCmd.Flags().Bool("china-mirrors", false, "Use mirror (gh.beszel.dev) instead of GitHub")
baseApp.RootCmd.AddCommand(updateCmd)
// add health command
baseApp.RootCmd.AddCommand(newHealthCmd())

View File

@@ -15,7 +15,7 @@ RUN CGO_ENABLED=0 GOGC=75 GOOS=$TARGETOS GOARCH=$TARGETARCH go build -ldflags "-
# --------------------------
# Final image: GPU-enabled agent with nvidia-smi
# --------------------------
FROM nvidia/cuda:12.9.1-base-ubuntu22.04
FROM nvidia/cuda:12.2.2-base-ubuntu22.04
COPY --from=builder /agent /agent
ENTRYPOINT ["/agent"]

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

@@ -60,7 +60,7 @@ func detectRestarter() restarter {
// Update checks GitHub for a newer release of beszel-agent, applies it,
// fixes SELinux context if needed, and restarts the service.
func Update() error {
func Update(useMirror bool) error {
exePath, _ := os.Executable()
dataDir, err := getDataDir()
@@ -70,6 +70,7 @@ func Update() error {
updated, err := ghupdate.Update(ghupdate.Config{
ArchiveExecutable: "beszel-agent",
DataDir: dataDir,
UseMirror: useMirror,
})
if err != nil {
log.Fatal(err)
@@ -99,6 +100,8 @@ func Update() error {
if err := r.Restart(); err != nil {
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: failed to restart service: %v", err)
ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually.")
} else {
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
}
} else {
ghupdate.ColorPrint(ghupdate.ColorYellow, "No supported init system detected; please restart manually if needed.")

View File

@@ -39,6 +39,7 @@ type Stats struct {
// TODO: remove other load fields in future release in favor of load avg array
LoadAvg [3]float64 `json:"la,omitempty" cbor:"28,keyasint"`
Battery [2]uint8 `json:"bat,omitzero" cbor:"29,keyasint,omitzero"` // [percent, charge state, current]
MaxMem float64 `json:"mm,omitempty" cbor:"30,keyasint,omitempty"`
}
type GPUData struct {

View File

@@ -23,7 +23,7 @@ import (
const (
colorReset = "\033[0m"
ColorYellow = "\033[33m"
colorGreen = "\033[32m"
ColorGreen = "\033[32m"
colorCyan = "\033[36m"
colorGray = "\033[90m"
)
@@ -64,10 +64,19 @@ type Config struct {
// The data directory to use when fetching and downloading the latest release.
DataDir string
// UseMirror specifies whether to use the beszel.dev mirror instead of GitHub API.
// When false (default), always uses api.github.com. When true, uses gh.beszel.dev.
UseMirror bool
}
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 +84,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 == "" {
@@ -106,21 +110,19 @@ func (p *plugin) update() (updated bool, err error) {
var latest *release
var useMirror bool
// Determine the API endpoint based on UseMirror flag
apiURL := fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", p.config.Owner, p.config.Repo)
if p.config.UseMirror {
useMirror = true
apiURL = fmt.Sprintf("https://gh.beszel.dev/repos/%s/%s/releases/latest?api=true", p.config.Owner, p.config.Repo)
ColorPrint(ColorYellow, "Using mirror for update.")
}
latest, err = fetchLatestRelease(
p.config.Context,
p.config.HttpClient,
fmt.Sprintf("https://api.github.com/repos/%s/%s/releases/latest", p.config.Owner, p.config.Repo),
apiURL,
)
// if the first fetch fails, try the beszel.dev API (fallback for China)
if err != nil {
ColorPrint(ColorYellow, "Failed to fetch release. Trying beszel.dev mirror...")
useMirror = true
latest, err = fetchLatestRelease(
p.config.Context,
p.config.HttpClient,
fmt.Sprintf("https://gh.beszel.dev/repos/%s/%s/releases/latest?api=true", p.config.Owner, p.config.Repo),
)
}
if err != nil {
return false, err
}
@@ -129,7 +131,7 @@ func (p *plugin) update() (updated bool, err error) {
newVersion := semver.MustParse(strings.TrimPrefix(latest.Tag, "v"))
if newVersion.LTE(currentVersion) {
ColorPrintf(colorGreen, "You already have the latest version %s.", p.currentVersion)
ColorPrintf(ColorGreen, "You already have the latest version %s.", p.currentVersion)
return false, nil
}
@@ -209,14 +211,11 @@ func (p *plugin) update() (updated bool, err error) {
}
ColorPrint(colorGray, "---")
ColorPrint(colorGreen, "Update completed successfully! You can start the executable as usual.")
ColorPrint(ColorGreen, "Update completed successfully!")
// print the release notes
if latest.Body != "" {
fmt.Print("\n")
ColorPrintf(colorCyan, "Here is a list with some of the %s changes:", latest.Tag)
// remove the update command note to avoid "stuttering"
// (@todo consider moving to a config option)
releaseNotes := strings.TrimSpace(strings.Replace(latest.Body, "> _To update the prebuilt executable you can run `./"+p.config.ArchiveExecutable+" update`._", "", 1))
ColorPrint(colorCyan, releaseNotes)
fmt.Print("\n")

View File

@@ -11,7 +11,7 @@ import (
)
// Update updates beszel to the latest version
func Update(_ *cobra.Command, _ []string) {
func Update(cmd *cobra.Command, _ []string) {
dataDir := os.TempDir()
// set dataDir to ./beszel_data if it exists
@@ -19,9 +19,13 @@ func Update(_ *cobra.Command, _ []string) {
dataDir = "./beszel_data"
}
// Check if china-mirrors flag is set
useMirror, _ := cmd.Flags().GetBool("china-mirrors")
updated, err := ghupdate.Update(ghupdate.Config{
ArchiveExecutable: "beszel",
DataDir: dataDir,
UseMirror: useMirror,
})
if err != nil {
log.Fatal(err)
@@ -30,6 +34,14 @@ func Update(_ *cobra.Command, _ []string) {
return
}
// make sure the file is executable
exePath, err := os.Executable()
if err == nil {
if err := os.Chmod(exePath, 0755); err != nil {
fmt.Printf("Warning: failed to set executable permissions: %v\n", err)
}
}
// Try to restart the service if it's running
restartService()
}
@@ -41,13 +53,13 @@ func restartService() {
// Check if beszel service exists and is active
cmd := exec.Command("systemctl", "is-active", "beszel.service")
if err := cmd.Run(); err == nil {
fmt.Println("Restarting beszel service...")
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel service...")
restartCmd := exec.Command("systemctl", "restart", "beszel.service")
if err := restartCmd.Run(); err != nil {
fmt.Printf("Warning: Failed to restart service: %v\n", err)
fmt.Println("Please restart the service manually: sudo systemctl restart beszel")
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: Failed to restart service: %v\n", err)
ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually: sudo systemctl restart beszel")
} else {
fmt.Println("Service restarted successfully")
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
}
return
}
@@ -57,17 +69,17 @@ func restartService() {
if _, err := exec.LookPath("rc-service"); err == nil {
cmd := exec.Command("rc-service", "beszel", "status")
if err := cmd.Run(); err == nil {
fmt.Println("Restarting beszel service...")
ghupdate.ColorPrint(ghupdate.ColorYellow, "Restarting beszel service...")
restartCmd := exec.Command("rc-service", "beszel", "restart")
if err := restartCmd.Run(); err != nil {
fmt.Printf("Warning: Failed to restart service: %v\n", err)
fmt.Println("Please restart the service manually: sudo rc-service beszel restart")
ghupdate.ColorPrintf(ghupdate.ColorYellow, "Warning: Failed to restart service: %v\n", err)
ghupdate.ColorPrint(ghupdate.ColorYellow, "Please restart the service manually: sudo rc-service beszel restart")
} else {
fmt.Println("Service restarted successfully")
ghupdate.ColorPrint(ghupdate.ColorGreen, "Service restarted successfully")
}
return
}
}
fmt.Println("Note: Service restart not attempted. If running as a service, restart manually.")
ghupdate.ColorPrint(ghupdate.ColorYellow, "Service restart not attempted. If running as a service, restart manually.")
}

View File

@@ -214,6 +214,7 @@ func (rm *RecordManager) AverageSystemStats(db dbx.Builder, records RecordIds) *
sum.Battery[1] = stats.Battery[1]
// Set peak values
sum.MaxCpu = max(sum.MaxCpu, stats.MaxCpu, stats.Cpu)
sum.MaxMem = max(sum.MaxMem, stats.MaxMem, stats.MemUsed)
sum.MaxNetworkSent = max(sum.MaxNetworkSent, stats.MaxNetworkSent, stats.NetworkSent)
sum.MaxNetworkRecv = max(sum.MaxNetworkRecv, stats.MaxNetworkRecv, stats.NetworkRecv)
sum.MaxDiskReadPs = max(sum.MaxDiskReadPs, stats.MaxDiskReadPs, stats.DiskReadPs)

Binary file not shown.

View File

@@ -1,12 +1,12 @@
{
"name": "beszel",
"version": "0.12.4",
"version": "0.12.6",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "beszel",
"version": "0.12.4",
"version": "0.12.6",
"dependencies": {
"@henrygd/queue": "^1.0.7",
"@henrygd/semaphore": "^0.0.2",
@@ -30,6 +30,7 @@
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",
@@ -3130,6 +3131,23 @@
"react-dom": ">=16.8"
}
},
"node_modules/@tanstack/react-virtual": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/react-virtual/-/react-virtual-3.13.12.tgz",
"integrity": "sha512-Gd13QdxPSukP8ZrkbgS2RwoZseTTbQPLnQEn7HY/rqtM+8Zt95f7xKC7N0EsKs7aoz0WzZ+fditZux+F8EzYxA==",
"license": "MIT",
"dependencies": {
"@tanstack/virtual-core": "3.13.12"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
},
"peerDependencies": {
"react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0",
"react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0"
}
},
"node_modules/@tanstack/table-core": {
"version": "8.21.3",
"resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz",
@@ -3143,6 +3161,16 @@
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@tanstack/virtual-core": {
"version": "3.13.12",
"resolved": "https://registry.npmjs.org/@tanstack/virtual-core/-/virtual-core-3.13.12.tgz",
"integrity": "sha512-1YBOJfRHV4sXUmWsFSf5rQor4Ss82G8dQWLRbnk3GA4jeP8hQt1hxXh0tmflpC0dz3VgEv/1+qwPyLeWkQuPFA==",
"license": "MIT",
"funding": {
"type": "github",
"url": "https://github.com/sponsors/tannerlinsley"
}
},
"node_modules/@types/bun": {
"version": "1.2.20",
"resolved": "https://registry.npmjs.org/@types/bun/-/bun-1.2.20.tgz",

View File

@@ -1,7 +1,7 @@
{
"name": "beszel",
"private": true,
"version": "0.12.4",
"version": "0.12.6",
"type": "module",
"scripts": {
"dev": "vite",
@@ -33,6 +33,7 @@
"@radix-ui/react-toast": "^1.2.15",
"@radix-ui/react-tooltip": "^1.2.8",
"@tanstack/react-table": "^8.21.3",
"@tanstack/react-virtual": "^3.13.12",
"class-variance-authority": "^0.7.1",
"clsx": "^2.1.1",
"cmdk": "^1.1.1",

View File

@@ -13,8 +13,9 @@ import {
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { $publicKey, pb } from "@/lib/stores"
import { cn, generateToken, isReadOnlyUser, tokenMap, useLocalStorage } from "@/lib/utils"
import { $publicKey } from "@/lib/stores"
import { cn, generateToken, tokenMap, useLocalStorage } from "@/lib/utils"
import { pb, isReadOnlyUser } from "@/lib/api"
import { useStore } from "@nanostores/react"
import { ChevronDownIcon, ExternalLinkIcon, PlusIcon } from "lucide-react"
import { memo, useEffect, useRef, useState } from "react"
@@ -132,7 +133,7 @@ export const SystemDialog = ({ setOpen, system }: { setOpen: (open: boolean) =>
>
<Tabs defaultValue={tab} onValueChange={setTab}>
<DialogHeader>
<DialogTitle className="mb-2">
<DialogTitle className="mb-2 max-w-100 truncate pr-8">
{system ? `${t`Edit`} ${system?.name}` : <Trans>Add New System</Trans>}
</DialogTitle>
<TabsList className="grid w-full grid-cols-2">

View File

@@ -16,7 +16,9 @@ export const alertsHistoryColumns: ColumnDef<AlertsHistoryRecord>[] = [
<Trans>System</Trans>
</Button>
),
cell: ({ row }) => <span className="ps-2">{row.original.expand?.system?.name || row.original.system}</span>,
cell: ({ row }) => (
<div className="ps-2 max-w-60 truncate">{row.original.expand?.system?.name || row.original.system}</div>
),
filterFn: (row, _, filterValue) => {
const display = row.original.expand?.system?.name || row.original.system || ""
return display.toLowerCase().includes(filterValue.toLowerCase())

View File

@@ -1,6 +1,6 @@
import { t } from "@lingui/core/macro"
import { Trans, Plural } from "@lingui/react/macro"
import { $alerts, $systems, pb } from "@/lib/stores"
import { $alerts, $systems } from "@/lib/stores"
import { cn, debounce } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts"
import { Switch } from "@/components/ui/switch"
@@ -15,6 +15,7 @@ import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
import { ServerIcon, GlobeIcon } from "lucide-react"
import { $router, Link } from "@/components/router"
import { DialogHeader } from "@/components/ui/dialog"
import { pb } from "@/lib/api"
const Slider = lazy(() => import("@/components/ui/slider"))
@@ -95,7 +96,7 @@ export const AlertDialogContent = memo(function AlertDialogContent({ system }: {
<TabsList className="mb-1 -mt-0.5">
<TabsTrigger value="system">
<ServerIcon className="me-2 h-3.5 w-3.5" />
{system.name}
<span className="truncate max-w-60">{system.name}</span>
</TabsTrigger>
<TabsTrigger value="global">
<GlobeIcon className="me-1.5 h-3.5 w-3.5" />

View File

@@ -7,7 +7,7 @@ import { useMemo } from "react"
export type DataPoint = {
label: string
dataKey: (data: SystemStatsRecord) => number | undefined
color: string
color: number | string
opacity: number
}

View File

@@ -6,7 +6,7 @@ import { ChartData } from "@/types"
import { useLingui } from "@lingui/react/macro"
import { Unit } from "@/lib/enums"
export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
export default memo(function MemChart({ chartData, showMax }: { chartData: ChartData; showMax: boolean }) {
const { yAxisWidth, updateYAxisWidth } = useYAxisWidth()
const { t } = useLingui()
@@ -66,7 +66,7 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
<Area
name={t`Used`}
order={3}
dataKey="stats.mu"
dataKey={({ stats }) => (showMax ? stats?.mm : stats?.mu)}
type="monotoneX"
fill="var(--chart-2)"
fillOpacity={0.4}
@@ -74,31 +74,31 @@ export default memo(function MemChart({ chartData }: { chartData: ChartData }) {
stackId="1"
isAnimationActive={false}
/>
{chartData.systemStats.at(-1)?.stats.mz && (
<Area
name="ZFS ARC"
order={2}
dataKey="stats.mz"
type="monotoneX"
fill="hsla(175 60% 45% / 0.8)"
fillOpacity={0.5}
stroke="hsla(175 60% 45% / 0.8)"
stackId="1"
isAnimationActive={false}
/>
)}
{/* {chartData.systemStats.at(-1)?.stats.mz && ( */}
<Area
name="ZFS ARC"
order={2}
dataKey={({ stats }) => (showMax ? null : stats?.mz)}
type="monotoneX"
fill="hsla(175 60% 45% / 0.8)"
fillOpacity={0.5}
stroke="hsla(175 60% 45% / 0.8)"
stackId="1"
isAnimationActive={false}
/>
{/* )} */}
<Area
name={t`Cache / Buffers`}
order={1}
dataKey="stats.mb"
dataKey={({ stats }) => (showMax ? null : stats?.mb)}
type="monotoneX"
fill="hsla(160 60% 45% / 0.5)"
fillOpacity={0.4}
// strokeOpacity={1}
stroke="hsla(160 60% 45% / 0.5)"
stackId="1"
isAnimationActive={false}
/>
{/* <ChartLegend content={<ChartLegendContent />} /> */}
</AreaChart>
</ChartContainer>
</div>

View File

@@ -23,11 +23,13 @@ import {
} from "@/components/ui/command"
import { memo, useEffect, useMemo } from "react"
import { $systems } from "@/lib/stores"
import { getHostDisplayValue, isAdmin, listen } from "@/lib/utils"
import { getHostDisplayValue, listen } from "@/lib/utils"
import { $router, basePath, navigate, prependBasePath } from "./router"
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { getPagePath } from "@nanostores/router"
import { DialogDescription } from "@radix-ui/react-dialog"
import { isAdmin } from "@/lib/api"
export default memo(function CommandPalette({ open, setOpen }: { open: boolean; setOpen: (open: boolean) => void }) {
useEffect(() => {
@@ -54,11 +56,9 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
)
return (
<CommandDialog open={open} onOpenChange={setOpen}>
<DialogDescription className="sr-only">Command palette</DialogDescription>
<CommandInput placeholder={t`Search for systems or settings...`} />
<CommandList>
<CommandEmpty>
<Trans>No results found.</Trans>
</CommandEmpty>
{systems.length > 0 && (
<>
<CommandGroup>
@@ -71,7 +71,7 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
}}
>
<Server className="me-2 size-4" />
<span>{system.name}</span>
<span className="max-w-60 truncate">{system.name}</span>
<CommandShortcut>{getHostDisplayValue(system)}</CommandShortcut>
</CommandItem>
))}
@@ -214,6 +214,9 @@ export default memo(function CommandPalette({ open, setOpen }: { open: boolean;
</CommandGroup>
</>
)}
<CommandEmpty>
<Trans>No results found.</Trans>
</CommandEmpty>
</CommandList>
</CommandDialog>
)

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

@@ -5,7 +5,7 @@ import { buttonVariants } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { LoaderCircle, LockIcon, LogInIcon, MailIcon } from "lucide-react"
import { $authenticated, pb } from "@/lib/stores"
import { $authenticated } from "@/lib/stores"
import * as v from "valibot"
import { toast } from "../ui/use-toast"
import { Dialog, DialogContent, DialogTrigger, DialogHeader, DialogTitle } from "@/components/ui/dialog"
@@ -13,6 +13,7 @@ import { useCallback, useEffect, useState } from "react"
import { AuthMethodsList, AuthProviderInfo, OAuth2AuthConfig } from "pocketbase"
import { $router, Link, prependBasePath } from "../router"
import { getPagePath } from "@nanostores/router"
import { pb } from "@/lib/api"
const honeypot = v.literal("")
const emailSchema = v.pipe(v.string(), v.email(t`Invalid email address.`))

View File

@@ -1,5 +1,5 @@
import { Trans } from "@lingui/react/macro";
import { t } from "@lingui/core/macro";
import { Trans } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { LoaderCircle, MailIcon, SendHorizonalIcon } from "lucide-react"
import { Input } from "../ui/input"
import { Label } from "../ui/label"
@@ -7,9 +7,9 @@ import { useCallback, useState } from "react"
import { toast } from "../ui/use-toast"
import { buttonVariants } from "../ui/button"
import { cn } from "@/lib/utils"
import { pb } from "@/lib/stores"
import { Dialog, DialogHeader } from "../ui/dialog"
import { DialogContent, DialogTrigger, DialogTitle } from "../ui/dialog"
import { pb } from "@/lib/api"
const showLoginFaliedToast = () => {
toast({

View File

@@ -1,13 +1,14 @@
import { t } from "@lingui/core/macro";
import { t } from "@lingui/core/macro"
import { UserAuthForm } from "@/components/login/auth-form"
import { Logo } from "../logo"
import { useEffect, useMemo, useState } from "react"
import { pb } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import ForgotPassword from "./forgot-pass-form"
import { $router } from "../router"
import { AuthMethodsList } from "pocketbase"
import { useTheme } from "../theme-provider"
import { pb } from "@/lib/api"
import { ModeToggle } from "../mode-toggle"
export default function () {
const page = useStore($router)
@@ -50,8 +51,11 @@ export default function () {
<div
className="grid gap-5 w-full px-4 mx-auto"
// @ts-ignore
style={{ maxWidth: "22em", "--border": theme == "light" ? "30 8% 80%" : "220 3% 20%" }}
style={{ maxWidth: "22em", "--border": theme == "light" ? "hsl(30, 8%, 70%)" : "hsl(220, 3%, 25%)" }}
>
<div className="absolute top-3 right-3">
<ModeToggle />
</div>
<div className="text-center">
<h1 className="mb-3">
<Logo className="h-7 fill-foreground mx-auto" />

View File

@@ -15,8 +15,8 @@ import { $router, basePath, Link, prependBasePath } from "./router"
import { LangToggle } from "./lang-toggle"
import { ModeToggle } from "./mode-toggle"
import { Logo } from "./logo"
import { pb } from "@/lib/stores"
import { cn, isReadOnlyUser, isAdmin, logOut } from "@/lib/utils"
import { cn, runOnce } from "@/lib/utils"
import { isReadOnlyUser, isAdmin, logOut, pb } from "@/lib/api"
import {
DropdownMenu,
DropdownMenuTrigger,
@@ -36,12 +36,17 @@ const isMac = navigator.platform.toUpperCase().indexOf("MAC") >= 0
export default function Navbar() {
return (
<div className="flex items-center h-14 md:h-16 bg-card px-4 pe-3 sm:px-6 border border-border/60 bt-0 rounded-md my-4">
<Link href={basePath} aria-label="Home" className="p-2 ps-0 me-3">
<Link
href={basePath}
aria-label="Home"
className="p-2 ps-0 me-3"
onMouseEnter={runOnce(() => import("@/components/routes/home"))}
>
<Logo className="h-[1.1rem] md:h-5 fill-foreground" />
</Link>
<SearchButton />
<div className="flex items-center ms-auto">
<div className="flex items-center ms-auto" onMouseEnter={() => import("@/components/routes/settings/general")}>
<LangToggle />
<ModeToggle />
<Link

View File

@@ -1,18 +1,20 @@
import { Suspense, lazy, memo, useEffect, useMemo } from "react"
import { Suspense, memo, useEffect, useMemo } from "react"
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card"
import { $alerts, $systems, pb } from "@/lib/stores"
import { $alerts, $systems } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { GithubIcon } from "lucide-react"
import { Separator } from "../ui/separator"
import { getSystemNameFromId, updateRecordList, updateSystemList } from "@/lib/utils"
import { getSystemNameFromId } from "@/lib/utils"
import { pb, updateRecordList, updateSystemList } from "@/lib/api"
import { AlertRecord, SystemRecord } from "@/types"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { $router, Link } from "../router"
import { Plural, Trans, useLingui } from "@lingui/react/macro"
import { getPagePath } from "@nanostores/router"
import { alertInfo } from "@/lib/alerts"
import SystemsTable from "@/components/systems-table/systems-table"
const SystemsTable = lazy(() => import("../systems-table/systems-table"))
// const SystemsTable = lazy(() => import("../systems-table/systems-table"))
export default memo(function () {
const { t } = useLingui()
@@ -42,7 +44,7 @@ export default memo(function () {
<SystemsTable />
</Suspense>
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 text-xs opacity-80">
<div className="flex gap-1.5 justify-end items-center pe-3 sm:pe-6 mt-3.5 mb-4 text-xs opacity-80">
<a
href="https://github.com/henrygd/beszel"
target="_blank"

View File

@@ -1,4 +1,4 @@
import { pb } from "@/lib/stores"
import { pb } from "@/lib/api"
import { cn, formatDuration, formatShortDate } from "@/lib/utils"
import { alertInfo } from "@/lib/alerts"
import { AlertsHistoryRecord } from "@/types"
@@ -273,13 +273,13 @@ export default function AlertsHistoryDataTable() {
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
<tr key={headerGroup.id} className="border-border/50">
{headerGroup.headers.map((header) => (
<TableHead className="px-2" key={header.id}>
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
</TableHead>
))}
</TableRow>
</tr>
))}
</TableHeader>
<TableBody>

View File

@@ -1,17 +1,16 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { isAdmin } from "@/lib/utils"
import { Separator } from "@/components/ui/separator"
import { Button } from "@/components/ui/button"
import { redirectPage } from "@nanostores/router"
import { $router } from "@/components/router"
import { AlertCircleIcon, FileSlidersIcon, LoaderCircleIcon } from "lucide-react"
import { Alert, AlertDescription, AlertTitle } from "@/components/ui/alert"
import { pb } from "@/lib/stores"
import { useState } from "react"
import { Textarea } from "@/components/ui/textarea"
import { toast } from "@/components/ui/use-toast"
import clsx from "clsx"
import { isAdmin, pb } from "@/lib/api"
export default function ConfigYaml() {
const [configContent, setConfigContent] = useState<string>("")

View File

@@ -1,6 +1,6 @@
import { t } from "@lingui/core/macro"
import { Trans } from "@lingui/react/macro"
import { useEffect } from "react"
import { lazy, useEffect } from "react"
import { Separator } from "../../ui/separator"
import { SidebarNav } from "./sidebar-nav.tsx"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card.tsx"
@@ -8,15 +8,23 @@ import { useStore } from "@nanostores/react"
import { $router } from "@/components/router.tsx"
import { getPagePath, redirectPage } from "@nanostores/router"
import { BellIcon, FileSlidersIcon, FingerprintIcon, SettingsIcon, AlertOctagonIcon } from "lucide-react"
import { $userSettings, pb } from "@/lib/stores.ts"
import { $userSettings } from "@/lib/stores.ts"
import { toast } from "@/components/ui/use-toast.ts"
import { UserSettings } from "@/types"
import General from "./general.tsx"
import Notifications from "./notifications.tsx"
import ConfigYaml from "./config-yaml.tsx"
import { useLingui } from "@lingui/react/macro"
import Fingerprints from "./tokens-fingerprints.tsx"
import AlertsHistoryDataTable from "./alerts-history-data-table"
import { pb } from "@/lib/api"
const generalSettingsImport = () => import("./general.tsx")
const notificationsSettingsImport = () => import("./notifications.tsx")
const configYamlSettingsImport = () => import("./config-yaml.tsx")
const fingerprintsSettingsImport = () => import("./tokens-fingerprints.tsx")
const alertsHistoryDataTableSettingsImport = () => import("./alerts-history-data-table.tsx")
const GeneralSettings = lazy(generalSettingsImport)
const NotificationsSettings = lazy(notificationsSettingsImport)
const ConfigYamlSettings = lazy(configYamlSettingsImport)
const FingerprintsSettings = lazy(fingerprintsSettingsImport)
const AlertsHistoryDataTableSettings = lazy(alertsHistoryDataTableSettingsImport)
export async function saveSettings(newSettings: Partial<UserSettings>) {
try {
@@ -59,23 +67,27 @@ export default function SettingsLayout() {
title: t`Notifications`,
href: getPagePath($router, "settings", { name: "notifications" }),
icon: BellIcon,
preload: notificationsSettingsImport,
},
{
title: t`Tokens & Fingerprints`,
href: getPagePath($router, "settings", { name: "tokens" }),
icon: FingerprintIcon,
noReadOnly: true,
preload: fingerprintsSettingsImport,
},
{
title: t`Alert History`,
href: getPagePath($router, "settings", { name: "alert-history" }),
icon: AlertOctagonIcon,
preload: alertsHistoryDataTableSettingsImport,
},
{
title: t`YAML Config`,
href: getPagePath($router, "settings", { name: "config" }),
icon: FileSlidersIcon,
admin: true,
preload: configYamlSettingsImport,
},
]
@@ -90,7 +102,7 @@ export default function SettingsLayout() {
}, [])
return (
<Card className="pt-5 px-4 pb-8 min-h-96 sm:pt-6 sm:px-7">
<Card className="pt-5 px-4 pb-8 min-h-96 mb-14 sm:pt-6 sm:px-7">
<CardHeader className="p-0">
<CardTitle className="mb-1">
<Trans>Settings</Trans>
@@ -120,14 +132,14 @@ function SettingsContent({ name }: { name: string }) {
switch (name) {
case "general":
return <General userSettings={userSettings} />
return <GeneralSettings userSettings={userSettings} />
case "notifications":
return <Notifications userSettings={userSettings} />
return <NotificationsSettings userSettings={userSettings} />
case "config":
return <ConfigYaml />
return <ConfigYamlSettings />
case "tokens":
return <Fingerprints />
return <FingerprintsSettings />
case "alert-history":
return <AlertsHistoryDataTable />
return <AlertsHistoryDataTableSettings />
}
}

View File

@@ -3,7 +3,6 @@ import { Trans } from "@lingui/react/macro"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { pb } from "@/lib/stores"
import { Separator } from "@/components/ui/separator"
import { Card } from "@/components/ui/card"
import { BellIcon, LoaderCircleIcon, PlusIcon, SaveIcon, Trash2Icon } from "lucide-react"
@@ -13,8 +12,8 @@ import { InputTags } from "@/components/ui/input-tags"
import { UserSettings } from "@/types"
import { saveSettings } from "./layout"
import * as v from "valibot"
import { isAdmin } from "@/lib/utils"
import { prependBasePath } from "@/components/router"
import { isAdmin, pb } from "@/lib/api"
interface ShoutrrrUrlCardProps {
url: string

View File

@@ -1,5 +1,6 @@
import React from "react"
import { cn, isAdmin, isReadOnlyUser } from "@/lib/utils"
import { cn } from "@/lib/utils"
import { isAdmin, isReadOnlyUser } from "@/lib/api"
import { buttonVariants } from "../../ui/button"
import { $router, Link, navigate } from "../../router"
import { useStore } from "@nanostores/react"
@@ -13,6 +14,7 @@ interface SidebarNavProps extends React.HTMLAttributes<HTMLElement> {
icon?: React.FC<React.SVGProps<SVGSVGElement>>
admin?: boolean
noReadOnly?: boolean
preload?: () => Promise<{ default: React.ComponentType<any> }>
}[]
}
@@ -52,6 +54,7 @@ export function SidebarNav({ className, items, ...props }: SidebarNavProps) {
}
return (
<Link
onMouseEnter={() => item.preload?.()}
key={item.href}
href={item.href}
className={cn(

View File

@@ -1,6 +1,6 @@
import { Trans, useLingui } from "@lingui/react/macro"
import { t } from "@lingui/core/macro"
import { $publicKey, pb } from "@/lib/stores"
import { $publicKey } from "@/lib/stores"
import { memo, useEffect, useMemo, useState } from "react"
import { Table, TableCell, TableHead, TableBody, TableRow, TableHeader } from "@/components/ui/table"
import { FingerprintRecord } from "@/types"
@@ -14,7 +14,8 @@ import {
Trash2Icon,
} from "lucide-react"
import { toast } from "@/components/ui/use-toast"
import { cn, copyToClipboard, generateToken, getHubURL, isReadOnlyUser, tokenMap } from "@/lib/utils"
import { cn, copyToClipboard, generateToken, getHubURL, tokenMap } from "@/lib/utils"
import { isReadOnlyUser, pb } from "@/lib/api"
import {
DropdownMenu,
DropdownMenuContent,
@@ -271,7 +272,7 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec
<div className="rounded-md border overflow-hidden w-full mt-4">
<Table>
<TableHeader>
<TableRow>
<tr className="border-border/50">
{headerCols.map((col) => (
<TableHead key={col.label} style={{ minWidth: col.w }}>
<span className="flex items-center gap-2">
@@ -287,12 +288,14 @@ const SectionTable = memo(({ fingerprints = [] }: { fingerprints: FingerprintRec
</span>
</TableHead>
)}
</TableRow>
</tr>
</TableHeader>
<TableBody className="whitespace-pre">
{fingerprints.map((fingerprint, i) => (
<TableRow key={i}>
<TableCell className="font-medium ps-5 py-2">{fingerprint.expand.system.name}</TableCell>
<TableCell className="font-medium ps-5 py-2 max-w-60 truncate">
{fingerprint.expand.system.name}
</TableCell>
<TableCell className="font-mono text-[0.95em] py-2">{fingerprint.token}</TableCell>
<TableCell className="font-mono text-[0.95em] py-2">{fingerprint.fingerprint}</TableCell>
{!isReadOnly && (

View File

@@ -2,7 +2,6 @@ import { t } from "@lingui/core/macro"
import { Plural, Trans } from "@lingui/react/macro"
import {
$systems,
pb,
$chartTime,
$containerFilter,
$userSettings,
@@ -12,7 +11,7 @@ import {
} from "@/lib/stores"
import { ChartData, ChartTimes, ContainerStatsRecord, GPUData, SystemRecord, SystemStatsRecord } from "@/types"
import { ChartType, Unit, Os, SystemStatus } from "@/lib/enums"
import React, { lazy, memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
import React, { memo, useCallback, useEffect, useMemo, useRef, useState, type JSX } from "react"
import { Card, CardHeader, CardTitle, CardDescription } from "../ui/card"
import { useStore } from "@nanostores/react"
import Spinner from "../spinner"
@@ -24,12 +23,12 @@ import {
decimalString,
formatBytes,
getHostDisplayValue,
getPbTimestamp,
listen,
parseSemVer,
toFixedFloat,
useLocalStorage,
} from "@/lib/utils"
import { getPbTimestamp, pb } from "@/lib/api"
import { Separator } from "../ui/separator"
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from "../ui/tooltip"
import { Button } from "../ui/button"
@@ -42,15 +41,14 @@ import { useLingui } from "@lingui/react/macro"
import { $router, navigate } from "../router"
import { getPagePath } from "@nanostores/router"
import { batteryStateTranslations } from "@/lib/i18n"
const AreaChartDefault = lazy(() => import("../charts/area-chart"))
const ContainerChart = lazy(() => import("../charts/container-chart"))
const MemChart = lazy(() => import("../charts/mem-chart"))
const DiskChart = lazy(() => import("../charts/disk-chart"))
const SwapChart = lazy(() => import("../charts/swap-chart"))
const TemperatureChart = lazy(() => import("../charts/temperature-chart"))
const GpuPowerChart = lazy(() => import("../charts/gpu-power-chart"))
const LoadAverageChart = lazy(() => import("../charts/load-average-chart"))
import AreaChartDefault from "@/components/charts/area-chart"
import ContainerChart from "@/components/charts/container-chart"
import MemChart from "@/components/charts/mem-chart"
import DiskChart from "@/components/charts/disk-chart"
import SwapChart from "@/components/charts/swap-chart"
import TemperatureChart from "@/components/charts/temperature-chart"
import GpuPowerChart from "@/components/charts/gpu-power-chart"
import LoadAverageChart from "@/components/charts/load-average-chart"
const cache = new Map<string, any>()
@@ -287,9 +285,11 @@ export default function SystemDetail({ name }: { name: string }) {
value: system.info.k,
},
}
let uptime: React.ReactNode
if (system.info.u < 172800) {
if (system.info.u < 3600) {
const mins = Math.trunc(system.info.u / 60)
uptime = <Plural value={mins} one="# minute" other="# minutes" />
} else if (system.info.u < 172800) {
const hours = Math.trunc(system.info.u / 3600)
uptime = <Plural value={hours} one="# hour" other="# hours" />
} else {
@@ -317,7 +317,7 @@ export default function SystemDetail({ name }: { name: string }) {
Icon: any
hide?: boolean
}[]
}, [system.info])
}, [system.info, t])
/** Space for tooltip if more than 12 containers */
useEffect(() => {
@@ -391,7 +391,7 @@ export default function SystemDetail({ name }: { name: string }) {
return (
<>
<div id="chartwrap" className="grid gap-4 mb-10 overflow-x-clip">
<div id="chartwrap" className="grid gap-4 mb-14 overflow-x-clip">
{/* system info */}
<Card>
<div className="grid xl:flex gap-4 px-4 sm:px-6 pt-3 sm:pt-4 pb-5">
@@ -486,7 +486,7 @@ export default function SystemDetail({ name }: { name: string }) {
{
label: t`CPU Usage`,
dataKey: ({ stats }) => (showMax ? stats?.cpum : stats?.cpu),
color: "1",
color: 1,
opacity: 0.4,
},
]}
@@ -512,8 +512,9 @@ export default function SystemDetail({ name }: { name: string }) {
grid={grid}
title={t`Memory Usage`}
description={t`Precise utilization at the recorded time`}
cornerEl={maxValSelect}
>
<MemChart chartData={chartData} />
<MemChart chartData={chartData} showMax={showMax} />
</ChartCard>
{containerFilterBar && (
@@ -546,13 +547,13 @@ export default function SystemDetail({ name }: { name: string }) {
{
label: t({ message: "Write", comment: "Disk write" }),
dataKey: ({ stats }) => (showMax ? stats?.dwm : stats?.dw),
color: "3",
color: 3,
opacity: 0.3,
},
{
label: t({ message: "Read", comment: "Disk read" }),
dataKey: ({ stats }) => (showMax ? stats?.drm : stats?.dr),
color: "1",
color: 1,
opacity: 0.3,
},
]}
@@ -587,7 +588,7 @@ export default function SystemDetail({ name }: { name: string }) {
}
return data?.stats?.b?.[0] ?? data?.stats?.ns * 1024 * 1024
},
color: "5",
color: 5,
opacity: 0.2,
},
{
@@ -598,7 +599,7 @@ export default function SystemDetail({ name }: { name: string }) {
}
return data?.stats?.b?.[1] ?? data?.stats?.nr * 1024 * 1024
},
color: "2",
color: 2,
opacity: 0.2,
},
]}
@@ -687,7 +688,7 @@ export default function SystemDetail({ name }: { name: string }) {
{
label: t`Charge`,
dataKey: ({ stats }) => stats?.bat?.[0],
color: "1",
color: 1,
opacity: 0.35,
},
]}
@@ -730,7 +731,7 @@ export default function SystemDetail({ name }: { name: string }) {
{
label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.u ?? 0,
color: "1",
color: 1,
opacity: 0.35,
},
]}
@@ -750,7 +751,7 @@ export default function SystemDetail({ name }: { name: string }) {
{
label: t`Usage`,
dataKey: ({ stats }) => stats?.g?.[id]?.mu ?? 0,
color: "2",
color: 2,
opacity: 0.25,
},
]}
@@ -802,13 +803,13 @@ export default function SystemDetail({ name }: { name: string }) {
{
label: t`Write`,
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "wm" : "w"] ?? 0,
color: "3",
color: 3,
opacity: 0.3,
},
{
label: t`Read`,
dataKey: ({ stats }) => stats?.efs?.[extraFsName]?.[showMax ? "rm" : "r"] ?? 0,
color: "1",
color: 1,
opacity: 0.3,
},
]}

View File

@@ -23,12 +23,11 @@ import {
formatBytes,
formatTemperature,
getMeterState,
isReadOnlyUser,
parseSemVer,
} from "@/lib/utils"
import { EthernetIcon, GpuIcon, HourglassIcon, ThermometerIcon } from "../ui/icons"
import { useStore } from "@nanostores/react"
import { $userSettings, pb } from "@/lib/stores"
import { $longestSystemNameLen, $userSettings } from "@/lib/stores"
import { Trans, useLingui } from "@lingui/react/macro"
import { useMemo, useRef, useState } from "react"
import { memo } from "react"
@@ -57,6 +56,7 @@ import { t } from "@lingui/core/macro"
import { MeterState, SystemStatus } from "@/lib/enums"
import { $router, Link } from "../router"
import { getPagePath } from "@nanostores/router"
import { isReadOnlyUser, pb } from "@/lib/api"
const STATUS_COLORS = {
[SystemStatus.Up]: "bg-green-500",
@@ -72,7 +72,8 @@ const STATUS_COLORS = {
export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnDef<SystemRecord>[] {
return [
{
size: 200,
// size: 200,
size: 100,
minSize: 0,
accessorKey: "name",
id: "system",
@@ -111,11 +112,15 @@ export default function SystemsTableColumns(viewMode: "table" | "grid"): ColumnD
Icon: ServerIcon,
cell: (info) => {
const { name } = info.row.original
const longestName = useStore($longestSystemNameLen)
return (
<>
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1 md:pe-5">
<span className="flex gap-2 items-center font-medium text-sm text-nowrap md:ps-1">
<IndicatorDot system={info.row.original} />
{name}
{/* NOTE: change to 1 ch if switching to monospace font */}
<span className="truncate" style={{ width: `${longestName / 1.1}ch` }}>
{name}
</span>
</span>
<Link
href={getPagePath($router, "system", { name })}
@@ -318,22 +323,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>
<div className="flex gap-2 items-center tabular-nums tracking-tight w-full">
<span className="min-w-8 shrink-0">{decimalString(val, val >= 10 ? 1 : 2)}%</span>
<span className="flex-1 min-w-8 grid bg-muted h-[1em] rounded-sm overflow-hidden">
<span className={meterClass} style={{ width: `${val}%` }}></span>
</span>
</div>
)

View File

@@ -11,11 +11,8 @@ import {
Row,
Table as TableType,
} from "@tanstack/react-table"
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
import { Button } from "@/components/ui/button"
import {
DropdownMenu,
DropdownMenuCheckboxItem,
@@ -36,33 +33,51 @@ import {
ArrowUpIcon,
Settings2Icon,
EyeIcon,
FilterIcon,
} from "lucide-react"
import { memo, useEffect, useMemo, useState } from "react"
import { memo, useEffect, useMemo, useRef, useState } from "react"
import { $systems } from "@/lib/stores"
import { useStore } from "@nanostores/react"
import { cn, useLocalStorage } from "@/lib/utils"
import { cn, runOnce, useLocalStorage } from "@/lib/utils"
import { $router, Link } from "../router"
import { useLingui, Trans } from "@lingui/react/macro"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "../ui/card"
import { Input } from "../ui/input"
import { Input } from "@/components/ui/input"
import { getPagePath } from "@nanostores/router"
import SystemsTableColumns, { ActionsButton, IndicatorDot } from "./systems-table-columns"
import AlertButton from "../alerts/alert-button"
import { SystemStatus } from "@/lib/enums"
import { useVirtualizer, VirtualItem } from "@tanstack/react-virtual"
type ViewMode = "table" | "grid"
type StatusFilter = "all" | "up" | "down" | "paused"
const preloadSystemDetail = runOnce(() => import("@/components/routes/system.tsx"))
export default function SystemsTable() {
const data = useStore($systems)
const { i18n, t } = useLingui()
const [filter, setFilter] = useState<string>()
const [statusFilter, setStatusFilter] = useState<StatusFilter>("all")
const [sorting, setSorting] = useState<SortingState>([{ id: "system", desc: false }])
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([])
const [columnVisibility, setColumnVisibility] = useLocalStorage<VisibilityState>("cols", {})
const [viewMode, setViewMode] = useLocalStorage<ViewMode>("viewMode", window.innerWidth > 1024 ? "table" : "grid")
const [viewMode, setViewMode] = useLocalStorage<ViewMode>(
"viewMode",
// show grid view on mobile if there are less than 200 systems (looks better but table is more efficient)
window.innerWidth < 1024 && data.length < 200 ? "grid" : "table"
)
const locale = i18n.locale
// Filter data based on status filter
const filteredData = useMemo(() => {
if (statusFilter === "all") {
return data
}
return data.filter((system) => system.status === statusFilter)
}, [data, statusFilter])
useEffect(() => {
if (filter !== undefined) {
table.getColumn("system")?.setFilterValue(filter)
@@ -72,7 +87,7 @@ export default function SystemsTable() {
const columnDefs = useMemo(() => SystemsTableColumns(viewMode), [viewMode])
const table = useReactTable({
data,
data: filteredData,
columns: columnDefs,
getCoreRowModel: getCoreRowModel(),
onSortingChange: setSorting,
@@ -111,6 +126,7 @@ export default function SystemsTable() {
<Trans>Updated in real time. Click on a system to view information.</Trans>
</CardDescription>
</div>
<div className="flex gap-2 ms-auto w-full md:w-80">
<Input placeholder={t`Filter...`} onChange={(e) => setFilter(e.target.value)} className="px-4" />
<DropdownMenu>
@@ -121,8 +137,8 @@ export default function SystemsTable() {
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="h-72 md:h-auto min-w-48 md:min-w-auto overflow-y-auto">
<div className="grid grid-cols-1 md:grid-cols-3 divide-y md:divide-s md:divide-y-0">
<div>
<div className="grid grid-cols-1 md:grid-cols-4 divide-y md:divide-s md:divide-y-0">
<div className="border-r">
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<LayoutGridIcon className="size-4" />
<Trans>Layout</Trans>
@@ -144,7 +160,33 @@ export default function SystemsTable() {
</DropdownMenuRadioGroup>
</div>
<div>
<div className="border-r">
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<FilterIcon className="size-4" />
<Trans>Status</Trans>
</DropdownMenuLabel>
<DropdownMenuSeparator />
<DropdownMenuRadioGroup
className="px-1 pb-1"
value={statusFilter}
onValueChange={(value) => setStatusFilter(value as StatusFilter)}
>
<DropdownMenuRadioItem value="all" onSelect={(e) => e.preventDefault()}>
<Trans>All Systems</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="up" onSelect={(e) => e.preventDefault()}>
<Trans>Up</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="down" onSelect={(e) => e.preventDefault()}>
<Trans>Down</Trans>
</DropdownMenuRadioItem>
<DropdownMenuRadioItem value="paused" onSelect={(e) => e.preventDefault()}>
<Trans>Paused</Trans>
</DropdownMenuRadioItem>
</DropdownMenuRadioGroup>
</div>
<div className="border-r">
<DropdownMenuLabel className="pt-2 px-3.5 flex items-center gap-2">
<ArrowUpDownIcon className="size-4" />
<Trans>Sort By</Trans>
@@ -210,7 +252,7 @@ export default function SystemsTable() {
</div>
</CardHeader>
)
}, [visibleColumns.length, sorting, viewMode, locale])
}, [visibleColumns.length, sorting, viewMode, locale, statusFilter])
return (
<Card>
@@ -218,7 +260,7 @@ export default function SystemsTable() {
<div className="p-6 pt-0 max-sm:py-3 max-sm:px-2">
{viewMode === "table" ? (
// table layout
<div className="rounded-md border overflow-hidden">
<div className="rounded-md">
<AllSystemsTable table={table} rows={rows} colLength={visibleColumns.length} />
</div>
) : (
@@ -240,36 +282,78 @@ export default function SystemsTable() {
)
}
const AllSystemsTable = memo(
({ table, rows, colLength }: { table: TableType<SystemRecord>; rows: Row<SystemRecord>[]; colLength: number }) => {
return (
<Table>
<SystemsTableHead table={table} colLength={colLength} />
<TableBody>
{rows.length ? (
rows.map((row) => (
<SystemTableRow key={row.original.id} row={row} length={rows.length} colLength={colLength} />
))
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-24 text-center">
<Trans>No systems found.</Trans>
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
)
}
)
const AllSystemsTable = memo(function ({
table,
rows,
colLength,
}: {
table: TableType<SystemRecord>
rows: Row<SystemRecord>[]
colLength: number
}) {
// The virtualizer will need a reference to the scrollable container element
const scrollRef = useRef<HTMLDivElement>(null)
const virtualizer = useVirtualizer<HTMLDivElement, HTMLTableRowElement>({
count: rows.length,
estimateSize: () => (rows.length > 10 ? 56 : 60),
getScrollElement: () => scrollRef.current,
overscan: 5,
})
const virtualRows = virtualizer.getVirtualItems()
const paddingTop = Math.max(0, virtualRows[0]?.start ?? 0 - virtualizer.options.scrollMargin)
const paddingBottom = Math.max(0, virtualizer.getTotalSize() - (virtualRows[virtualRows.length - 1]?.end ?? 0))
return (
<div
className={cn(
"h-min max-h-[calc(100dvh-17rem)] max-w-full relative overflow-auto border rounded-md",
// don't set min height if there are less than 2 rows, do set if we need to display the empty state
(!rows.length || rows.length > 2) && "min-h-50"
)}
ref={scrollRef}
>
{/* add header height to table size */}
<div style={{ height: `${virtualizer.getTotalSize() + 50}px`, paddingTop, paddingBottom }}>
<table className="text-sm w-full h-full">
<SystemsTableHead table={table} colLength={colLength} />
<TableBody onMouseEnter={preloadSystemDetail}>
{rows.length ? (
virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index] as Row<SystemRecord>
return (
<SystemTableRow
key={row.id}
row={row}
virtualRow={virtualRow}
length={rows.length}
colLength={colLength}
/>
)
})
) : (
<TableRow>
<TableCell colSpan={colLength} className="h-37 text-center pointer-events-none">
<Trans>No systems found.</Trans>
</TableCell>
</TableRow>
)}
</TableBody>
</table>
</div>
</div>
)
})
function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>; colLength: number }) {
const { i18n } = useLingui()
return useMemo(() => {
return (
<TableHeader>
<TableHeader className="sticky top-0 z-20 w-full border-b-2">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
<tr key={headerGroup.id}>
{headerGroup.headers.map((header) => {
return (
<TableHead className="px-1.5" key={header.id}>
@@ -277,41 +361,49 @@ function SystemsTableHead({ table, colLength }: { table: TableType<SystemRecord>
</TableHead>
)
})}
</TableRow>
</tr>
))}
</TableHeader>
)
}, [i18n.locale, colLength])
}
const SystemTableRow = memo(
({ row, length, colLength }: { row: Row<SystemRecord>; length: number; colLength: number }) => {
const system = row.original
const { t } = useLingui()
return useMemo(() => {
return (
<TableRow
// data-state={row.getIsSelected() && "selected"}
className={cn("cursor-pointer transition-opacity relative", {
"opacity-50": system.status === SystemStatus.Paused,
})}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width: cell.column.getSize(),
}}
className={length > 10 ? "py-2" : "py-2.5"}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
}, [system, system.status, colLength, t])
}
)
const SystemTableRow = memo(function ({
row,
virtualRow,
colLength,
}: {
row: Row<SystemRecord>
virtualRow: VirtualItem
length: number
colLength: number
}) {
const system = row.original
const { t } = useLingui()
return useMemo(() => {
return (
<TableRow
// data-state={row.getIsSelected() && "selected"}
className={cn("cursor-pointer transition-opacity relative safari:transform-3d", {
"opacity-50": system.status === SystemStatus.Paused,
})}
>
{row.getVisibleCells().map((cell) => (
<TableCell
key={cell.id}
style={{
width: cell.column.getSize(),
height: virtualRow.size,
}}
className="py-0"
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
)
}, [system, system.status, colLength, t])
})
const SystemCard = memo(
({ row, table, colLength }: { row: Row<SystemRecord>; table: TableType<SystemRecord>; colLength: number }) => {
@@ -321,6 +413,7 @@ const SystemCard = memo(
return useMemo(() => {
return (
<Card
onMouseEnter={preloadSystemDetail}
key={system.id}
className={cn(
"cursor-pointer hover:shadow-md transition-all bg-transparent w-full dark:border-border duration-200 relative",
@@ -330,13 +423,11 @@ const SystemCard = memo(
)}
>
<CardHeader className="py-1 ps-5 pe-3 bg-muted/30 border-b border-border/60">
<div className="flex items-center justify-between gap-2">
<CardTitle className="text-base tracking-normal shrink-1 text-primary/90 flex items-center min-h-10 gap-2.5 min-w-0">
<div className="flex items-center gap-2.5 min-w-0">
<div className="flex items-center gap-2 w-full overflow-hidden">
<CardTitle className="text-base tracking-normal text-primary/90 flex items-center min-w-0 flex-1 gap-2.5">
<div className="flex items-center gap-2.5 min-w-0 flex-1">
<IndicatorDot system={system} />
<CardTitle className="text-[.95em]/normal tracking-normal truncate text-primary/90">
{system.name}
</CardTitle>
<span className="text-[.95em]/normal tracking-normal text-primary/90 truncate">{system.name}</span>
</div>
</CardTitle>
{table.getColumn("actions")?.getIsVisible() && (
@@ -347,23 +438,33 @@ const SystemCard = memo(
)}
</div>
</CardHeader>
<CardContent className="grid gap-2.5 text-sm px-5 pt-3.5 pb-4">
{table.getAllColumns().map((column) => {
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
if (!cell) return null
// @ts-ignore
const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>
return (
<div key={column.id} className="flex items-center gap-3">
{Icon && <Icon className="size-4 text-muted-foreground" />}
<div className="flex items-center gap-3 flex-1">
<span className="text-muted-foreground min-w-16">{name()}:</span>
<div className="flex-1">{flexRender(cell.column.columnDef.cell, cell.getContext())}</div>
</div>
</div>
)
})}
<CardContent className="text-sm px-5 pt-3.5 pb-4">
<div className="grid gap-2.5" style={{ gridTemplateColumns: "24px minmax(80px, max-content) 1fr" }}>
{table.getAllColumns().map((column) => {
if (!column.getIsVisible() || column.id === "system" || column.id === "actions") return null
const cell = row.getAllCells().find((cell) => cell.column.id === column.id)
if (!cell) return null
// @ts-ignore
const { Icon, name } = column.columnDef as ColumnDef<SystemRecord, unknown>
return (
<>
<div key={`${column.id}-icon`} className="flex items-center">
{column.id === "lastSeen" ? (
<EyeIcon className="size-4 text-muted-foreground" />
) : (
Icon && <Icon className="size-4 text-muted-foreground" />
)}
</div>
<div key={`${column.id}-label`} className="flex items-center text-muted-foreground pr-3">
{name()}:
</div>
<div key={`${column.id}-value`} className="flex items-center">
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</div>
</>
)
})}
</div>
</CardContent>
<Link
href={getPagePath($router, "system", { name: row.original.name })}

View File

@@ -1,33 +1,41 @@
import * as React from "react"
import { DialogTitle, type DialogProps } from "@radix-ui/react-dialog"
import { Command as CommandPrimitive } from "cmdk"
import { Search } from "lucide-react"
import { SearchIcon } from "lucide-react"
import { cn } from "@/lib/utils"
import { Dialog, DialogContent } from "@/components/ui/dialog"
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
const Command = React.forwardRef<
React.ElementRef<typeof CommandPrimitive>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive>
>(({ className, ...props }, ref) => (
<CommandPrimitive
ref={ref}
className={cn("flex h-full w-full flex-col overflow-hidden bg-card", className)}
{...props}
/>
))
Command.displayName = CommandPrimitive.displayName
function Command({ className, ...props }: React.ComponentProps<typeof CommandPrimitive>) {
return (
<CommandPrimitive
data-slot="command"
className={cn("bg-card flex h-full w-full flex-col overflow-hidden rounded-md", className)}
{...props}
/>
)
}
interface CommandDialogProps extends DialogProps {}
const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
function CommandDialog({
title = "Command Palette",
description = "Search for a command to run...",
children,
className,
showCloseButton = true,
...props
}: React.ComponentProps<typeof Dialog> & {
title?: string
description?: string
className?: string
showCloseButton?: boolean
}) {
return (
<Dialog {...props}>
<DialogContent className="overflow-hidden p-0 shadow-lg">
<div className="sr-only">
<DialogTitle>Command</DialogTitle>
</div>
<Command className="[&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-group]]:px-2 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
<DialogHeader className="sr-only">
<DialogTitle>{title}</DialogTitle>
<DialogDescription>{description}</DialogDescription>
</DialogHeader>
<DialogContent className={cn("overflow-hidden p-0", className)}>
<Command className="[&_[cmdk-group-heading]]:text-muted-foreground **:data-[slot=command-input-wrapper]:h-12 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group]]:px-2 [&_[cmdk-group]:not([hidden])_~[cmdk-group]]:pt-0 [&_[cmdk-input-wrapper]_svg]:h-5 [&_[cmdk-input-wrapper]_svg]:w-5 [&_[cmdk-input]]:h-12 [&_[cmdk-item]]:px-2 [&_[cmdk-item]]:py-3 [&_[cmdk-item]_svg]:h-5 [&_[cmdk-item]_svg]:w-5">
{children}
</Command>
</DialogContent>
@@ -35,89 +43,81 @@ const CommandDialog = ({ children, ...props }: CommandDialogProps) => {
)
}
const CommandInput = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Input>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Input>
>(({ className, ...props }, ref) => (
<div className="flex items-center border-b px-3" cmdk-input-wrapper="">
<Search className="me-2 h-4 w-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
ref={ref}
function CommandInput({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Input>) {
return (
<div data-slot="command-input-wrapper" className="flex h-9 items-center gap-2 border-b px-3">
<SearchIcon className="size-4 shrink-0 opacity-50" />
<CommandPrimitive.Input
data-slot="command-input"
className={cn(
"placeholder:text-muted-foreground flex h-10 w-full rounded-md bg-transparent py-3 text-sm outline-hidden disabled:cursor-not-allowed disabled:opacity-50",
className
)}
{...props}
/>
</div>
)
}
function CommandList({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.List>) {
return (
<CommandPrimitive.List
data-slot="command-list"
className={cn("max-h-[300px] scroll-py-1 overflow-x-hidden overflow-y-auto", className)}
{...props}
/>
)
}
function CommandEmpty({ ...props }: React.ComponentProps<typeof CommandPrimitive.Empty>) {
return <CommandPrimitive.Empty data-slot="command-empty" className="py-6 text-center text-sm" {...props} />
}
function CommandGroup({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Group>) {
return (
<CommandPrimitive.Group
data-slot="command-group"
className={cn(
"flex h-11 w-full rounded-md bg-transparent py-3 text-sm outline-hidden placeholder:text-muted-foreground disabled:cursor-not-allowed disabled:opacity-50",
"text-foreground [&_[cmdk-group-heading]]:text-muted-foreground overflow-hidden p-1 [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium",
className
)}
{...props}
/>
</div>
))
CommandInput.displayName = CommandPrimitive.Input.displayName
const CommandList = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.List>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.List>
>(({ className, ...props }, ref) => (
<CommandPrimitive.List
ref={ref}
className={cn("max-h-[300px] overflow-y-auto overflow-x-hidden", className)}
{...props}
/>
))
CommandList.displayName = CommandPrimitive.List.displayName
const CommandEmpty = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Empty>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Empty>
>((props, ref) => <CommandPrimitive.Empty ref={ref} className="py-6 text-center text-sm" {...props} />)
CommandEmpty.displayName = CommandPrimitive.Empty.displayName
const CommandGroup = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Group>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Group>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Group
ref={ref}
className={cn(
"overflow-hidden p-1 text-foreground [&_[cmdk-group-heading]]:px-2 [&_[cmdk-group-heading]]:py-1.5 [&_[cmdk-group-heading]]:text-xs [&_[cmdk-group-heading]]:font-medium [&_[cmdk-group-heading]]:text-muted-foreground",
className
)}
{...props}
/>
))
CommandGroup.displayName = CommandPrimitive.Group.displayName
const CommandSeparator = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Separator>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Separator>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Separator ref={ref} className={cn("-mx-1 h-px bg-border", className)} {...props} />
))
CommandSeparator.displayName = CommandPrimitive.Separator.displayName
const CommandItem = React.forwardRef<
React.ElementRef<typeof CommandPrimitive.Item>,
React.ComponentPropsWithoutRef<typeof CommandPrimitive.Item>
>(({ className, ...props }, ref) => (
<CommandPrimitive.Item
ref={ref}
className={cn(
"relative flex cursor-default opacity-70 select-none items-center rounded-sm px-2 py-1.5 text-sm outline-hidden aria-selected:bg-accent/60 aria-selected:opacity-90 data-[disabled='true']:pointer-events-none data-[disabled='true']:opacity-50",
className
)}
{...props}
/>
))
CommandItem.displayName = CommandPrimitive.Item.displayName
const CommandShortcut = ({ className, ...props }: React.HTMLAttributes<HTMLSpanElement>) => {
return <span className={cn("ms-auto text-xs tracking-wide text-muted-foreground", className)} {...props} />
)
}
function CommandSeparator({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Separator>) {
return (
<CommandPrimitive.Separator
data-slot="command-separator"
className={cn("bg-border -mx-1 h-px", className)}
{...props}
/>
)
}
function CommandItem({ className, ...props }: React.ComponentProps<typeof CommandPrimitive.Item>) {
return (
<CommandPrimitive.Item
data-slot="command-item"
className={cn(
"data-[selected=true]:bg-accent/70 data-[selected=true]:text-accent-foreground [&_svg:not([class*='text-'])]:text-muted-foreground relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none data-[disabled=true]:pointer-events-none data-[disabled=true]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
className
)}
{...props}
/>
)
}
function CommandShortcut({ className, ...props }: React.ComponentProps<"span">) {
return (
<span
data-slot="command-shortcut"
className={cn("text-muted-foreground ml-auto text-xs tracking-wide", className)}
{...props}
/>
)
}
CommandShortcut.displayName = "CommandShortcut"
export {
Command,

View File

@@ -79,7 +79,7 @@ const DropdownMenuItem = React.forwardRef<
<DropdownMenuPrimitive.Item
ref={ref}
className={cn(
"relative flex select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
"cursor-pointer relative flex select-none items-center rounded-sm px-2.5 py-1.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
inset && "ps-8",
className
)}
@@ -95,7 +95,7 @@ const DropdownMenuCheckboxItem = React.forwardRef<
<DropdownMenuPrimitive.CheckboxItem
ref={ref}
className={cn(
"relative flex select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
checked={checked}
@@ -118,7 +118,7 @@ const DropdownMenuRadioItem = React.forwardRef<
<DropdownMenuPrimitive.RadioItem
ref={ref}
className={cn(
"relative flex cursor-default select-none items-center rounded-sm py-1.5 ps-8 pe-2 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
"relative flex cursor-pointer select-none items-center rounded-sm py-1.5 ps-8 pe-2.5 text-sm outline-hidden focus:bg-accent/70 focus:text-accent-foreground data-disabled:pointer-events-none data-disabled:opacity-50",
className
)}
{...props}

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

@@ -2,21 +2,19 @@ import * as React from "react"
import { cn } from "@/lib/utils"
export interface InputProps extends React.InputHTMLAttributes<HTMLInputElement> {}
const Input = React.forwardRef<HTMLInputElement, InputProps>(({ className, type, ...props }, ref) => {
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
return (
<input
type={type}
data-slot="input"
className={cn(
"flex h-10 w-full rounded-md border bg-background px-3 py-2 text-sm ring-offset-background file:border-0 file:bg-transparent file:text-sm file:font-medium placeholder:text-muted-foreground focus-visible:outline-hidden focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:cursor-not-allowed disabled:opacity-50",
"aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 aria-invalid:border-destructive",
className
)}
ref={ref}
{...props}
/>
)
})
Input.displayName = "Input"
}
export { Input }

View File

@@ -13,7 +13,11 @@ Table.displayName = "Table"
const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<thead ref={ref} className={cn("bg-muted/30 [&_tr]:border-b", className)} {...props} />
<thead
ref={ref}
className={cn("bg-table-header border-b border-border/50 [&_tr]:border-b", className)}
{...props}
/>
)
)
TableHeader.displayName = "TableHeader"

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

@@ -1,13 +1,15 @@
@import "tailwindcss";
@import "tw-animate-css";
@custom-variant light (&:is(.light *));
@custom-variant dark (&:is(.dark *));
@custom-variant safari (@supports (hanging-punctuation: first) and (-webkit-appearance: none));
:root {
--background: hsl(30 8% 98%);
--foreground: hsl(30 0% 0%);
--foreground: hsl(30 0% 10%);
--card: hsl(30 0% 100%);
--card-foreground: hsl(240 6.67% 2.94%);
--card-foreground: hsl(240 6% 12%);
--popover: hsl(30 0% 100%);
--popover-foreground: hsl(240 10% 6.2%);
--primary: hsl(240 5.88% 10%);
@@ -19,7 +21,7 @@
--accent: hsl(20 23.08% 94%);
--accent-foreground: hsl(240 5.88% 10%);
--destructive: hsl(0 66% 53%);
--destructive-foreground: hsl(0 0% 98.04%);
--destructive-foreground: hsl(0 0% 97%);
--border: hsl(30 8.11% 85.49%);
--input: hsl(30 4.29% 72.55%);
--ring: hsl(30 3.97% 49.41%);
@@ -29,6 +31,7 @@
--chart-3: hsl(30 80% 55%);
--chart-4: hsl(280 65% 60%);
--chart-5: hsl(340 75% 55%);
--table-header: hsl(225, 6%, 97%);
}
.dark {
@@ -48,10 +51,10 @@
--accent: hsl(220 5% 15.5%);
--accent-foreground: hsl(220 2% 98%);
--destructive: hsl(0 62% 46%);
--destructive-foreground: hsl(0 0% 97%);
--border: hsl(220 3% 16%);
--input: hsl(220 4% 22%);
--ring: hsl(220 4% 80%);
--table-header: hsl(220, 6%, 13%);
--radius: 0.8rem;
}
@@ -94,6 +97,7 @@
--color-accent: var(--accent);
--color-accent-foreground: var(--accent-foreground);
--color-destructive: var(--destructive);
--color-destructive-foreground: var(--destructive-foreground);
--color-border: var(--border);
--color-input: var(--input);
--color-ring: var(--ring);
@@ -102,6 +106,7 @@
--color-chart-3: var(--chart-3);
--color-chart-4: var(--chart-4);
--color-chart-5: var(--chart-5);
--color-table-header: var(--table-header);
}
@layer utilities {

View File

@@ -1,9 +1,10 @@
import type { AlertInfo, AlertRecord } from "@/types"
import type { RecordSubscription } from "pocketbase"
import { pb, $alerts } from "@/lib/stores"
import { $alerts } from "@/lib/stores"
import { EthernetIcon } from "@/components/ui/icons"
import { ServerIcon, CpuIcon, MemoryStickIcon, HardDriveIcon, ThermometerIcon, HourglassIcon } from "lucide-react"
import { t } from "@lingui/core/macro"
import { pb } from "./api"
/** Alert info for each alert type */
export const alertInfo: Record<string, AlertInfo> = {

136
beszel/site/src/lib/api.ts Normal file
View File

@@ -0,0 +1,136 @@
import { ChartTimes, SystemRecord, UserSettings } from "@/types"
import { $alerts, $longestSystemNameLen, $systems, $userSettings } from "./stores"
import { toast } from "@/components/ui/use-toast"
import { t } from "@lingui/core/macro"
import { chartTimeData } from "./utils"
import { WritableAtom } from "nanostores"
import { RecordModel, RecordSubscription } from "pocketbase"
import PocketBase from "pocketbase"
import { basePath } from "@/components/router"
/** PocketBase JS Client */
export const pb = new PocketBase(basePath)
export const isAdmin = () => pb.authStore.record?.role === "admin"
export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
const verifyAuth = () => {
pb.collection("users")
.authRefresh()
.catch(() => {
logOut()
toast({
title: t`Failed to authenticate`,
description: t`Please log in again`,
variant: "destructive",
})
})
}
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
export async function logOut() {
$systems.set([])
$alerts.set({})
$userSettings.set({} as UserSettings)
sessionStorage.setItem("lo", "t") // prevent auto login on logout
pb.authStore.clear()
pb.realtime.unsubscribe()
}
/** Fetch or create user settings in database */
export async function updateUserSettings() {
try {
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
$userSettings.set(req.settings)
return
} catch (e) {
console.error("get settings", e)
}
// create user settings if error fetching existing
try {
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
$userSettings.set(createdSettings.settings)
} catch (e) {
console.error("create settings", e)
}
}
/** Update systems / alerts list when records change */
export function updateRecordList<T extends RecordModel>(e: RecordSubscription<T>, $store: WritableAtom<T[]>) {
const curRecords = $store.get()
const newRecords = []
if (e.action === "delete") {
for (const server of curRecords) {
if (server.id !== e.record.id) {
newRecords.push(server)
}
}
} else {
let found = 0
for (const server of curRecords) {
if (server.id === e.record.id) {
found = newRecords.push(e.record)
} else {
newRecords.push(server)
}
}
if (!found) {
newRecords.push(e.record)
}
}
$store.set(newRecords)
}
/** Fetches updated system list from database */
export const updateSystemList = (() => {
let isFetchingSystems = false
return async () => {
if (isFetchingSystems) {
return
}
isFetchingSystems = true
try {
let records = await pb
.collection<SystemRecord>("systems")
.getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
if (records.length) {
// records = [
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ...records,
// ]
// we need to loop once to get the longest name
let longestName = $longestSystemNameLen.get()
for (const { name } of records) {
const nameLen = Math.min(20, name.length)
if (nameLen > longestName) {
$longestSystemNameLen.set(nameLen)
longestName = nameLen
}
}
$systems.set(records)
} else {
verifyAuth()
}
} finally {
isFetchingSystems = false
}
}
})()
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
d ||= chartTimeData[timeString].getOffset(new Date())
const year = d.getUTCFullYear()
const month = String(d.getUTCMonth() + 1).padStart(2, "0")
const day = String(d.getUTCDate()).padStart(2, "0")
const hours = String(d.getUTCHours()).padStart(2, "0")
const minutes = String(d.getUTCMinutes()).padStart(2, "0")
const seconds = String(d.getUTCSeconds()).padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}

View File

@@ -1,11 +1,7 @@
import PocketBase from "pocketbase"
import { atom, map } from "nanostores"
import { AlertMap, ChartTimes, SystemRecord, UserSettings } from "@/types"
import { basePath } from "@/components/router"
import { Unit } from "./enums"
/** PocketBase JS Client */
export const pb = new PocketBase(basePath)
import { pb } from "./api"
/** Store if user is authenticated */
export const $authenticated = atom(pb.authStore.isValid)
@@ -57,3 +53,8 @@ export const $copyContent = atom("")
/** Direction for localization */
export const $direction = atom<"ltr" | "rtl">("ltr")
/** Longest system name length. Used to set table column width. I know this
* is stupid but the table is virtualized and I know this will work.
*/
export const $longestSystemNameLen = atom(8)

View File

@@ -2,14 +2,12 @@ import { t } from "@lingui/core/macro"
import { toast } from "@/components/ui/use-toast"
import { type ClassValue, clsx } from "clsx"
import { twMerge } from "tailwind-merge"
import { $alerts, $copyContent, $systems, $userSettings, pb } from "./stores"
import type { ChartTimeData, ChartTimes, FingerprintRecord, SemVer, SystemRecord, UserSettings } from "@/types"
import { RecordModel, RecordSubscription } from "pocketbase"
import { WritableAtom } from "nanostores"
import { $copyContent, $systems, $userSettings } from "./stores"
import type { ChartTimeData, FingerprintRecord, SemVer, SystemRecord } from "@/types"
import { timeDay, timeHour } from "d3-time"
import { useEffect, useState } from "react"
import { prependBasePath } from "@/components/router"
import { MeterState, Unit } from "./enums"
import { prependBasePath } from "@/components/router"
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs))
@@ -34,52 +32,6 @@ export async function copyToClipboard(content: string) {
}
}
const verifyAuth = () => {
pb.collection("users")
.authRefresh()
.catch(() => {
logOut()
toast({
title: t`Failed to authenticate`,
description: t`Please log in again`,
variant: "destructive",
})
})
}
export const updateSystemList = (() => {
let isFetchingSystems = false
return async () => {
if (isFetchingSystems) {
return
}
isFetchingSystems = true
try {
const records = await pb
.collection<SystemRecord>("systems")
.getFullList({ sort: "+name", fields: "id,name,host,port,info,status" })
if (records.length) {
$systems.set(records)
} else {
verifyAuth()
}
} finally {
isFetchingSystems = false
}
}
})()
/** Logs the user out by clearing the auth store and unsubscribing from realtime updates. */
export async function logOut() {
$systems.set([])
$alerts.set({})
$userSettings.set({} as UserSettings)
sessionStorage.setItem("lo", "t") // prevent auto login on logout
pb.authStore.clear()
pb.realtime.unsubscribe()
}
const hourWithMinutesFormatter = new Intl.DateTimeFormat(undefined, {
hour: "numeric",
minute: "numeric",
@@ -110,47 +62,6 @@ export const updateFavicon = (newIcon: string) => {
;(document.querySelector("link[rel='icon']") as HTMLLinkElement).href = prependBasePath(`/static/${newIcon}`)
}
export const isAdmin = () => pb.authStore.record?.role === "admin"
export const isReadOnlyUser = () => pb.authStore.record?.role === "readonly"
/** Update systems / alerts list when records change */
export function updateRecordList<T extends RecordModel>(e: RecordSubscription<T>, $store: WritableAtom<T[]>) {
const curRecords = $store.get()
const newRecords = []
if (e.action === "delete") {
for (const server of curRecords) {
if (server.id !== e.record.id) {
newRecords.push(server)
}
}
} else {
let found = 0
for (const server of curRecords) {
if (server.id === e.record.id) {
found = newRecords.push(e.record)
} else {
newRecords.push(server)
}
}
if (!found) {
newRecords.push(e.record)
}
}
$store.set(newRecords)
}
export function getPbTimestamp(timeString: ChartTimes, d?: Date) {
d ||= chartTimeData[timeString].getOffset(new Date())
const year = d.getUTCFullYear()
const month = String(d.getUTCMonth() + 1).padStart(2, "0")
const day = String(d.getUTCDate()).padStart(2, "0")
const hours = String(d.getUTCHours()).padStart(2, "0")
const minutes = String(d.getUTCMinutes()).padStart(2, "0")
const seconds = String(d.getUTCSeconds()).padStart(2, "0")
return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`
}
export const chartTimeData: ChartTimeData = {
"1h": {
type: "1m",
@@ -329,24 +240,6 @@ export function formatBytes(
}
}
/** Fetch or create user settings in database */
export async function updateUserSettings() {
try {
const req = await pb.collection("user_settings").getFirstListItem("", { fields: "settings" })
$userSettings.set(req.settings)
return
} catch (e) {
console.error("get settings", e)
}
// create user settings if error fetching existing
try {
const createdSettings = await pb.collection("user_settings").create({ user: pb.authStore.record!.id })
$userSettings.set(createdSettings.settings)
} catch (e) {
console.error("create settings", e)
}
}
export const chartMargin = { top: 12 }
/**
@@ -357,6 +250,21 @@ export const chartMargin = { top: 12 }
*/
export const getHostDisplayValue = (system: SystemRecord): string => system.host.slice(system.host.lastIndexOf("/") + 1)
// export function formatUptimeString(uptimeSeconds: number): string {
// if (!uptimeSeconds || isNaN(uptimeSeconds)) return ""
// if (uptimeSeconds < 3600) {
// const minutes = Math.trunc(uptimeSeconds / 60)
// return plural({ minutes }, { one: "# minute", other: "# minutes" })
// } else if (uptimeSeconds < 172800) {
// const hours = Math.trunc(uptimeSeconds / 3600)
// console.log(hours)
// return plural({ hours }, { one: "# hour", other: "# hours" })
// } else {
// const days = Math.trunc(uptimeSeconds / 86400)
// return plural({ days }, { one: "# day", other: "# days" })
// }
// }
/** Generate a random token for the agent */
export const generateToken = () => {
try {
@@ -448,3 +356,16 @@ export const getSystemNameFromId = (() => {
return sysName
}
})()
/** Run a function only once */
export function runOnce<T extends (...args: any[]) => any>(fn: T): (...args: Parameters<T>) => ReturnType<T> {
let done = false
let result: any
return (...args: any) => {
if (!done) {
result = fn(...args)
done = true
}
return result
}
}

View File

@@ -33,6 +33,10 @@ msgstr "تم تحديد {0} من {1} صف"
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ساعة} other {# ساعات}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 ساعة"
@@ -125,6 +129,7 @@ msgstr "التنبيهات"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "جميع الأنظمة"
@@ -422,6 +427,7 @@ msgstr "التوثيق"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "معطل"
@@ -492,7 +498,7 @@ msgstr "تصدير تكوين الأنظمة الحالية الخاصة بك."
msgid "Fahrenheit (°F)"
msgstr "فهرنهايت (°ف)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "فشل في المصادقة"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "إيقاف مؤقت"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "متوقف مؤقتا"
@@ -774,7 +781,7 @@ msgstr "يرجى إنشاء حساب مسؤول"
msgid "Please enable pop-ups for this site"
msgstr "يرجى تمكين النوافذ المنبثقة لهذا الموقع"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "يرجى تسجيل الدخول مرة أخرى"
@@ -901,6 +908,7 @@ msgstr "الترتيب حسب"
msgid "State"
msgstr "الحالة"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "الحالة"
@@ -1067,6 +1075,7 @@ msgstr "غير معروفة"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "قيد التشغيل"

View File

@@ -33,6 +33,10 @@ msgstr "{0} от {1} селектирани."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# час} other {# часа}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 час"
@@ -125,6 +129,7 @@ msgstr "Тревоги"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Всички системи"
@@ -422,6 +427,7 @@ msgstr "Документация"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Офлайн"
@@ -492,7 +498,7 @@ msgstr "Експортирай конфигурацията на системи
msgid "Fahrenheit (°F)"
msgstr "Фаренхайт (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Неуспешно удостоверяване"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Пауза"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "На пауза"
@@ -774,7 +781,7 @@ msgstr "Моля създай администраторски акаунт"
msgid "Please enable pop-ups for this site"
msgstr "Моля активирай изскачащите прозорци за този сайт"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Моля влез отново"
@@ -901,6 +908,7 @@ msgstr "Сортиране по"
msgid "State"
msgstr "Състояние"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Статус"
@@ -1067,6 +1075,7 @@ msgstr "Неизвестна"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Нагоре"

View File

@@ -33,6 +33,10 @@ msgstr "{0} z {1} vybraných řádků."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# Hodina} few {# Hodiny} many {# Hodin} other {# Hodin}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 hodina"
@@ -125,6 +129,7 @@ msgstr "Výstrahy"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Všechny systémy"
@@ -422,6 +427,7 @@ msgstr "Dokumentace"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Nefunkční"
@@ -492,7 +498,7 @@ msgstr "Exportovat aktuální konfiguraci systémů."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheita (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Ověření se nezdařilo"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pozastavit"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Pozastaveno"
@@ -774,7 +781,7 @@ msgstr "Vytvořte si prosím účet administrátora"
msgid "Please enable pop-ups for this site"
msgstr "Prosím povolte vyskakovací okna pro tento web"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Přihlaste se prosím znovu"
@@ -901,6 +908,7 @@ msgstr "Seřadit podle"
msgid "State"
msgstr "Stav"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Stav"
@@ -1067,6 +1075,7 @@ msgstr "Neznámá"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Funkční"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hour} other {# hours}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 time"
@@ -125,6 +129,7 @@ msgstr "Alarmer"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Alle systemer"
@@ -422,6 +427,7 @@ msgstr "Dokumentation"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Nede"
@@ -492,7 +498,7 @@ msgstr "Eksporter din nuværende systemkonfiguration."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Kunne ikke godkende"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pause"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Sat på pause"
@@ -774,7 +781,7 @@ msgstr "Opret venligst en administratorkonto"
msgid "Please enable pop-ups for this site"
msgstr "Aktiver pop-ups for dette websted"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Log venligst ind igen"
@@ -901,6 +908,7 @@ msgstr "Sorter efter"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1067,6 +1075,7 @@ msgstr "Ukendt"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Oppe"

View File

@@ -33,6 +33,10 @@ msgstr "{0} von {1} Zeile(n) ausgewählt."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# Stunde} other {# Stunden}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 Stunde"
@@ -125,6 +129,7 @@ msgstr "Warnungen"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Alle Systeme"
@@ -422,6 +427,7 @@ msgstr "Dokumentation"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Offline"
@@ -492,7 +498,7 @@ msgstr "Exportiere die aktuelle Systemkonfiguration."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Authentifizierung fehlgeschlagen"
@@ -601,7 +607,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"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pause"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Pausiert"
@@ -774,7 +781,7 @@ msgstr "Bitte erstelle ein Administratorkonto"
msgid "Please enable pop-ups for this site"
msgstr "Bitte aktiviere Pop-ups für diese Seite"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Bitte melde dich erneut an"
@@ -901,6 +908,7 @@ msgstr "Sortieren nach"
msgid "State"
msgstr "Status"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1067,6 +1075,7 @@ msgstr "Unbekannt"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "aktiv"

View File

@@ -28,6 +28,10 @@ msgstr "{0} of {1} row(s) selected."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hour} other {# hours}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr "{mins, plural, one {# minute} other {# minutes}}"
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 hour"
@@ -120,6 +124,7 @@ msgstr "Alerts"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "All Systems"
@@ -417,6 +422,7 @@ msgstr "Documentation"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Down"
@@ -487,7 +493,7 @@ msgstr "Export your current systems configuration."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Failed to authenticate"
@@ -745,6 +751,7 @@ msgid "Pause"
msgstr "Pause"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Paused"
@@ -769,7 +776,7 @@ msgstr "Please create an admin account"
msgid "Please enable pop-ups for this site"
msgstr "Please enable pop-ups for this site"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Please log in again"
@@ -896,6 +903,7 @@ msgstr "Sort By"
msgid "State"
msgstr "State"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1062,6 +1070,7 @@ msgstr "Unknown"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Up"

View File

@@ -33,6 +33,10 @@ msgstr "{0} de {1} fila(s) seleccionada(s)."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hora} other {# horas}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 hora"
@@ -125,6 +129,7 @@ msgstr "Alertas"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Todos los Sistemas"
@@ -422,6 +427,7 @@ msgstr "Documentación"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Abajo"
@@ -492,7 +498,7 @@ msgstr "Exporte la configuración actual de sus sistemas."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Error al autenticar"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pausar"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Pausado"
@@ -774,7 +781,7 @@ msgstr "Por favor, cree una cuenta de administrador"
msgid "Please enable pop-ups for this site"
msgstr "Por favor, habilite las ventanas emergentes para este sitio"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Por favor, inicie sesión de nuevo"
@@ -901,6 +908,7 @@ msgstr "Ordenar por"
msgid "State"
msgstr "Estado"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Estado"
@@ -1067,6 +1075,7 @@ msgstr "Desconocida"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Activo"

View File

@@ -33,6 +33,10 @@ msgstr "{0} از {1} ردیف انتخاب شده است."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ساعت} other {# ساعت}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "۱ ساعت"
@@ -125,6 +129,7 @@ msgstr "هشدارها"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "همه سیستم‌ها"
@@ -422,6 +427,7 @@ msgstr "مستندات"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "قطع"
@@ -492,7 +498,7 @@ msgstr "پیکربندی سیستم‌های فعلی خود را خارج کن
msgid "Fahrenheit (°F)"
msgstr "فارنهایت (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "احراز هویت ناموفق بود"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "توقف"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "مکث شده"
@@ -774,7 +781,7 @@ msgstr "لطفاً یک حساب مدیر ایجاد کنید"
msgid "Please enable pop-ups for this site"
msgstr "لطفاً پنجره‌های بازشو را برای این سایت فعال کنید"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "لطفاً دوباره وارد شوید"
@@ -901,6 +908,7 @@ msgstr "مرتب‌سازی بر اساس"
msgid "State"
msgstr "وضعیت"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "وضعیت"
@@ -1067,6 +1075,7 @@ msgstr "ناشناخته"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "فعال"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# heure} other {# heures}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 heure"
@@ -125,6 +129,7 @@ msgstr "Alertes"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Tous les systèmes"
@@ -422,6 +427,7 @@ msgstr "Documentation"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Injoignable"
@@ -492,7 +498,7 @@ msgstr "Exportez la configuration actuelle de vos systèmes."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Échec de l'authentification"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pause"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "En pause"
@@ -774,7 +781,7 @@ msgstr "Veuillez créer un compte administrateur"
msgid "Please enable pop-ups for this site"
msgstr "Veuillez activer les pop-ups pour ce site"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Veuillez vous reconnecter"
@@ -901,6 +908,7 @@ msgstr "Trier par"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Statut"
@@ -1067,6 +1075,7 @@ msgstr "Inconnue"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Joignable"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# sat} other {# sati}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 sat"
@@ -125,6 +129,7 @@ msgstr "Upozorenja"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Svi Sistemi"
@@ -422,6 +427,7 @@ msgstr "Dokumentacija"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr ""
@@ -492,7 +498,7 @@ msgstr "Izvoz trenutne sistemske konfiguracije."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Provjera autentičnosti nije uspjela"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pauza"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Pauzirano"
@@ -774,7 +781,7 @@ msgstr "Molimo kreirajte administratorski račun"
msgid "Please enable pop-ups for this site"
msgstr "Omogućite skočne prozore za ovu stranicu"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Molimo prijavite se ponovno"
@@ -901,6 +908,7 @@ msgstr "Sortiraj po"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1067,6 +1075,7 @@ msgstr "Nepoznata"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr ""

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# óra} other {# óra}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 óra"
@@ -125,6 +129,7 @@ msgstr "Riasztások"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Minden rendszer"
@@ -422,6 +427,7 @@ msgstr "Dokumentáció"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr ""
@@ -492,7 +498,7 @@ msgstr "Exportálja a jelenlegi rendszerkonfigurációt."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Hitelesítés sikertelen"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Szüneteltetés"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Szüneteltetve"
@@ -774,7 +781,7 @@ msgstr "Kérjük, hozzon létre egy admin fiókot"
msgid "Please enable pop-ups for this site"
msgstr "Kérjük, engedélyezze a felugró ablakokat ezen az oldalon"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Kérjük jelentkezz be újra"
@@ -901,6 +908,7 @@ msgstr "Rendezés"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Állapot"
@@ -1067,6 +1075,7 @@ msgstr "Ismeretlen"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr ""

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# klukkustund} other {# klukkustundir}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 klukkustund"
@@ -125,6 +129,7 @@ msgstr "Tilkynningar"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Öll kerfi"
@@ -422,6 +427,7 @@ msgstr "Skjal"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr ""
@@ -492,7 +498,7 @@ msgstr ""
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Villa í auðkenningu"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pása"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Í bið"
@@ -774,7 +781,7 @@ msgstr "Vinsamlegast búðu til admin aðgang"
msgid "Please enable pop-ups for this site"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Vinsamlegast skráðu þið inn aftur"
@@ -901,6 +908,7 @@ msgstr "Raða eftir"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Staða"
@@ -1067,6 +1075,7 @@ msgstr "Óþekkt"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr ""

View File

@@ -33,6 +33,10 @@ msgstr "{0} di {1} righe selezionate."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ora} other {# ore}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 ora"
@@ -125,6 +129,7 @@ msgstr "Avvisi"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Tutti i Sistemi"
@@ -422,6 +427,7 @@ msgstr "Documentazione"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Offline"
@@ -492,7 +498,7 @@ msgstr "Esporta la configurazione attuale dei tuoi sistemi."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Autenticazione fallita"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pausa"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "In pausa"
@@ -774,7 +781,7 @@ msgstr "Si prega di creare un account amministratore"
msgid "Please enable pop-ups for this site"
msgstr "Si prega di abilitare i pop-up per questo sito"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Si prega di accedere nuovamente"
@@ -901,6 +908,7 @@ msgstr "Ordina per"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Stato"
@@ -1067,6 +1075,7 @@ msgstr "Sconosciuta"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Attivo"

View File

@@ -33,6 +33,10 @@ msgstr "{1}行のうち{0}行が選択されました。"
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# 時間} other {# 時間}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1時間"
@@ -125,6 +129,7 @@ msgstr "アラート"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "すべてのシステム"
@@ -422,6 +427,7 @@ msgstr "ドキュメント"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "停止"
@@ -492,7 +498,7 @@ msgstr "現在のシステム設定をエクスポートします。"
msgid "Fahrenheit (°F)"
msgstr "華氏 (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "認証に失敗しました"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "一時停止"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "一時停止中"
@@ -774,7 +781,7 @@ msgstr "管理者アカウントを作成してください"
msgid "Please enable pop-ups for this site"
msgstr "このサイトのポップアップを有効にしてください"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "再度ログインしてください"
@@ -901,6 +908,7 @@ msgstr "並び替え基準"
msgid "State"
msgstr "状態"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "ステータス"
@@ -1067,6 +1075,7 @@ msgstr "不明"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "正常"

View File

@@ -33,6 +33,10 @@ msgstr "{1}개의 행 중 {0}개가 선택되었습니다."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# 시간} other {# 시간}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1시간"
@@ -125,6 +129,7 @@ msgstr "알림"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "모든 시스템"
@@ -422,6 +427,7 @@ msgstr "문서"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "오프라인"
@@ -492,7 +498,7 @@ msgstr "현재 시스템 구성 내보내기"
msgid "Fahrenheit (°F)"
msgstr "화씨 (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "인증 실패"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "일시 중지"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "일시 정지됨"
@@ -774,7 +781,7 @@ msgstr "관리자 계정을 생성하세요."
msgid "Please enable pop-ups for this site"
msgstr "이 사이트에 대해 팝업을 활성화하세요."
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "다시 로그인하세요."
@@ -901,6 +908,7 @@ msgstr "정렬 기준"
msgid "State"
msgstr "상태"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "상태"
@@ -1067,6 +1075,7 @@ msgstr "알 수 없음"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "온라인"

View File

@@ -33,6 +33,10 @@ msgstr "{0} van de {1} rij(en) geselecteerd."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# uur} other {# uren}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 uur"
@@ -125,6 +129,7 @@ msgstr "Waarschuwingen"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Alle systemen"
@@ -422,6 +427,7 @@ msgstr "Documentatie"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Offline"
@@ -492,7 +498,7 @@ msgstr "Exporteer je huidige systeemconfiguratie."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Authenticatie mislukt"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pauze"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Gepauzeerd"
@@ -774,7 +781,7 @@ msgstr "Maak een beheerdersaccount aan"
msgid "Please enable pop-ups for this site"
msgstr "Activeer pop-ups voor deze website"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Meld je opnieuw aan"
@@ -901,6 +908,7 @@ msgstr "Sorteren op"
msgid "State"
msgstr "Status"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1067,6 +1075,7 @@ msgstr "Onbekend"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Online"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# time} other {# timer}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 time"
@@ -125,6 +129,7 @@ msgstr "Alarmer"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Alle Systemer"
@@ -422,6 +427,7 @@ msgstr "Dokumentasjon"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Nede"
@@ -492,7 +498,7 @@ msgstr "Eksporter din nåværende systemkonfigurasjon"
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Autentisering mislyktes"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pause"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Satt på Pause"
@@ -774,7 +781,7 @@ msgstr "Vennligst opprett en admin-konto"
msgid "Please enable pop-ups for this site"
msgstr "Vennligst aktiver pop-ups for nettsiden"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Vennligst logg inn på nytt"
@@ -901,6 +908,7 @@ msgstr "Sorter Etter"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1067,6 +1075,7 @@ msgstr "Ukjent"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Oppe"

View File

@@ -33,6 +33,10 @@ msgstr "{0} z {1} wybranych wierszy."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {godzinę} few {# godziny} many {# godzin} other {# godziny}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 godzina"
@@ -125,6 +129,7 @@ msgstr "Alerty"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Wszystkie systemy"
@@ -422,6 +427,7 @@ msgstr "Dokumentacja"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Nie działa"
@@ -492,7 +498,7 @@ msgstr "Eksportuj aktualną konfigurację systemów."
msgid "Fahrenheit (°F)"
msgstr "Fahrenheit (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Błąd autoryzacji"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pauza"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Wstrzymane"
@@ -774,7 +781,7 @@ msgstr "Utwórz konto administratora"
msgid "Please enable pop-ups for this site"
msgstr "Włącz wyskakujące okna dla tej strony"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Zaloguj się ponownie"
@@ -901,6 +908,7 @@ msgstr "Sortuj według"
msgid "State"
msgstr "Stan"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1067,6 +1075,7 @@ msgstr "Nieznana"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Działa"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# hora} other {# horas}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 hora"
@@ -125,6 +129,7 @@ msgstr "Alertas"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Todos os Sistemas"
@@ -422,6 +427,7 @@ msgstr "Documentação"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "“Desligado”"
@@ -492,7 +498,7 @@ msgstr "Exporte a configuração atual dos seus sistemas."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Falha na autenticação"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Pausar"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Pausado"
@@ -774,7 +781,7 @@ msgstr "Por favor, crie uma conta de administrador"
msgid "Please enable pop-ups for this site"
msgstr "Por favor, habilite pop-ups para este site"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Por favor, faça login novamente"
@@ -901,6 +908,7 @@ msgstr "Ordenar Por"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1067,6 +1075,7 @@ msgstr "Desconhecida"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "“Ligado”"

View File

@@ -33,6 +33,10 @@ msgstr "Выбрано {0} из {1} строк."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# час} other {# часов}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 час"
@@ -125,6 +129,7 @@ msgstr "Оповещения"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Все системы"
@@ -422,6 +427,7 @@ msgstr "Документация"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Не в сети"
@@ -492,7 +498,7 @@ msgstr "Экспортируйте текущую конфигурацию си
msgid "Fahrenheit (°F)"
msgstr "Фаренгейт (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Не удалось аутентифицировать"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Пауза"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Пауза"
@@ -774,7 +781,7 @@ msgstr "Пожалуйста, создайте учетную запись ад
msgid "Please enable pop-ups for this site"
msgstr "Пожалуйста, включите всплывающие окна для этого сайта"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Пожалуйста, войдите снова"
@@ -901,6 +908,7 @@ msgstr "Сортировать по"
msgid "State"
msgstr "Состояние"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Статус"
@@ -1067,6 +1075,7 @@ msgstr "Неизвестная"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "В сети"

View File

@@ -33,6 +33,10 @@ msgstr ""
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# ura} two {# uri} few {# ur} other {# ur}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 ura"
@@ -125,6 +129,7 @@ msgstr "Opozorila"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Vsi sistemi"
@@ -422,6 +427,7 @@ msgstr "Dokumentacija"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr ""
@@ -492,7 +498,7 @@ msgstr "Izvozi trenutne nastavitve sistema."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Preverjanje pristnosti ni uspelo"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Premor"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Zaustavljeno"
@@ -774,7 +781,7 @@ msgstr "Ustvarite skrbniški račun"
msgid "Please enable pop-ups for this site"
msgstr "Omogočite pojavna okna za to spletno mesto"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Prosimo, prijavite se znova"
@@ -901,6 +908,7 @@ msgstr "Razvrsti po"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1067,6 +1075,7 @@ msgstr "Neznana"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr ""

View File

@@ -33,6 +33,10 @@ msgstr "{0} av {1} rad(er) valda."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# timme} other {# timmar}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 timme"
@@ -125,6 +129,7 @@ msgstr "Larm"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Alla system"
@@ -422,6 +427,7 @@ msgstr "Dokumentation"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr ""
@@ -492,7 +498,7 @@ msgstr "Exportera din nuvarande systemkonfiguration."
msgid "Fahrenheit (°F)"
msgstr ""
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Autentisering misslyckades"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Paus"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Pausad"
@@ -774,7 +781,7 @@ msgstr "Vänligen skapa ett administratörskonto"
msgid "Please enable pop-ups for this site"
msgstr "Vänligen aktivera popup-fönster för den här webbplatsen"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Vänligen logga in igen"
@@ -901,6 +908,7 @@ msgstr "Sortera efter"
msgid "State"
msgstr ""
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Status"
@@ -1067,6 +1075,7 @@ msgstr "Okänd"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr ""

View File

@@ -33,6 +33,10 @@ msgstr "{1} satırdan {0} tanesi seçildi."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# saat} other {# saat}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 saat"
@@ -125,6 +129,7 @@ msgstr "Uyarılar"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Tüm Sistemler"
@@ -422,6 +427,7 @@ msgstr "Dokümantasyon"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Kapalı"
@@ -492,7 +498,7 @@ msgstr "Mevcut sistem yapılandırmanızı dışa aktarın."
msgid "Fahrenheit (°F)"
msgstr "Fahrenhayt (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Kimlik doğrulama başarısız"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Duraklat"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Duraklatıldı"
@@ -774,7 +781,7 @@ msgstr "Lütfen bir yönetici hesabı oluşturun"
msgid "Please enable pop-ups for this site"
msgstr "Lütfen bu site için açılır pencereleri etkinleştirin"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Lütfen tekrar giriş yapın"
@@ -901,6 +908,7 @@ msgstr "Sıralama Ölçütü"
msgid "State"
msgstr "Durum"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Durum"
@@ -1067,6 +1075,7 @@ msgstr ""
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Açık"

View File

@@ -33,6 +33,10 @@ msgstr "Вибрано {0} з {1} рядків."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# година} few {# години} many {# годин} other {# години}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 година"
@@ -125,6 +129,7 @@ msgstr "Сповіщення"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Всі системи"
@@ -422,6 +427,7 @@ msgstr "Документація"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Не працює"
@@ -492,7 +498,7 @@ msgstr "Експортуйте поточну конфігурацію сист
msgid "Fahrenheit (°F)"
msgstr "Фаренгейт (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Не вдалося автентифікувати"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Призупинити"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Призупинено"
@@ -774,7 +781,7 @@ msgstr "Будь ласка, створіть адміністративний
msgid "Please enable pop-ups for this site"
msgstr "Будь ласка, увімкніть спливаючі вікна для цього сайту"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Будь ласка, увійдіть знову"
@@ -901,6 +908,7 @@ msgstr "Сортувати за"
msgid "State"
msgstr "Стан"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Статус"
@@ -1067,6 +1075,7 @@ msgstr "Невідома"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Працює"

View File

@@ -33,6 +33,10 @@ msgstr "Đã chọn {0} trên {1} hàng."
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# giờ} other {# giờ}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1 giờ"
@@ -125,6 +129,7 @@ msgstr "Cảnh báo"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "Tất cả Hệ thống"
@@ -422,6 +427,7 @@ msgstr "Tài liệu"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "Mất kết nối"
@@ -492,7 +498,7 @@ msgstr "Xuất cấu hình hệ thống hiện tại của bạn."
msgid "Fahrenheit (°F)"
msgstr "Độ F (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "Xác thực thất bại"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "Tạm dừng"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "Đã tạm dừng"
@@ -774,7 +781,7 @@ msgstr "Vui lòng tạo một tài khoản quản trị viên"
msgid "Please enable pop-ups for this site"
msgstr "Vui lòng bật cửa sổ bật lên cho trang web này"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "Vui lòng đăng nhập lại"
@@ -901,6 +908,7 @@ msgstr "Sắp xếp theo"
msgid "State"
msgstr "Trạng thái"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "Trạng thái"
@@ -1067,6 +1075,7 @@ msgstr "Không xác định"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "Hoạt động"

View File

@@ -33,6 +33,10 @@ msgstr "已选择 {0} / {1} 行"
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# 小时} other {# 小时}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1小时"
@@ -125,6 +129,7 @@ msgstr "警报"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "所有客户端"
@@ -422,6 +427,7 @@ msgstr "文档"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "离线"
@@ -492,7 +498,7 @@ msgstr "导出您当前的系统配置。"
msgid "Fahrenheit (°F)"
msgstr "华氏度 (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "认证失败"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "暂停"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "已暂停"
@@ -774,7 +781,7 @@ msgstr "请创建一个管理员账户"
msgid "Please enable pop-ups for this site"
msgstr "请为此网站启用弹出窗口"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "请重新登录"
@@ -901,6 +908,7 @@ msgstr "排序依据"
msgid "State"
msgstr "状态"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "状态"
@@ -1067,6 +1075,7 @@ msgstr "未知"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "在线"

View File

@@ -33,6 +33,10 @@ msgstr "已選擇 {1} 個項目中的 {0} 個"
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# 小時} other {# 小時}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1小時"
@@ -125,6 +129,7 @@ msgstr "警報"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "所有系統"
@@ -422,6 +427,7 @@ msgstr "文件"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "中斷"
@@ -492,7 +498,7 @@ msgstr "匯出您現在的系統設定。"
msgid "Fahrenheit (°F)"
msgstr "華氏 (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "認證失敗"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "暫停"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "已暫停"
@@ -774,7 +781,7 @@ msgstr "請建立一個管理員帳號"
msgid "Please enable pop-ups for this site"
msgstr "請為此網站啟用彈出窗口"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "請重新登入"
@@ -901,6 +908,7 @@ msgstr "排序依據"
msgid "State"
msgstr "狀態"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "狀態"
@@ -1067,6 +1075,7 @@ msgstr "未知"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "上線"

View File

@@ -33,6 +33,10 @@ msgstr "已選取 {1} 個項目中的 {0} 個"
msgid "{hours, plural, one {# hour} other {# hours}}"
msgstr "{hours, plural, one {# 小時} other {# 小時}}"
#: src/components/routes/system.tsx
msgid "{mins, plural, one {# minute} other {# minutes}}"
msgstr ""
#: src/lib/utils.ts
msgid "1 hour"
msgstr "1小時"
@@ -125,6 +129,7 @@ msgstr "警報"
#: src/components/alerts/alerts-sheet.tsx
#: src/components/systems-table/systems-table.tsx
#: src/components/systems-table/systems-table.tsx
msgid "All Systems"
msgstr "所有系統"
@@ -422,6 +427,7 @@ msgstr "文件"
#: src/components/alerts-history-columns.tsx
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Down"
msgstr "離線"
@@ -492,7 +498,7 @@ msgstr "匯出您現在的系統設定。"
msgid "Fahrenheit (°F)"
msgstr "華氏 (°F)"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Failed to authenticate"
msgstr "認證失敗"
@@ -750,6 +756,7 @@ msgid "Pause"
msgstr "暫停"
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Paused"
msgstr "已暫停"
@@ -774,7 +781,7 @@ msgstr "請建立一個管理員帳號"
msgid "Please enable pop-ups for this site"
msgstr "請為此網站啟用彈出視窗"
#: src/lib/utils.ts
#: src/lib/api.ts
msgid "Please log in again"
msgstr "請重新登入"
@@ -901,6 +908,7 @@ msgstr "排序"
msgid "State"
msgstr "狀態"
#: src/components/systems-table/systems-table.tsx
#: src/lib/alerts.ts
msgid "Status"
msgstr "狀態"
@@ -1067,6 +1075,7 @@ msgstr "未知"
#. Context: System is up
#: src/components/routes/system.tsx
#: src/components/systems-table/systems-table-columns.tsx
#: src/components/systems-table/systems-table.tsx
msgid "Up"
msgstr "上線"

View File

@@ -2,26 +2,26 @@ import "./index.css"
// import { Suspense, lazy, useEffect, StrictMode } from "react"
import { Suspense, lazy, memo, useEffect } from "react"
import ReactDOM from "react-dom/client"
import Home from "./components/routes/home.tsx"
import { ThemeProvider } from "./components/theme-provider.tsx"
import { DirectionProvider } from "@radix-ui/react-direction"
import { $authenticated, $systems, pb, $publicKey, $copyContent, $direction } from "./lib/stores.ts"
import { updateUserSettings, updateFavicon, updateSystemList } from "./lib/utils.ts"
import { $authenticated, $systems, $publicKey, $copyContent, $direction } from "./lib/stores.ts"
import { pb, updateSystemList, updateUserSettings } from "./lib/api.ts"
import { useStore } from "@nanostores/react"
import { Toaster } from "./components/ui/toaster.tsx"
import { $router } from "./components/router.tsx"
import SystemDetail from "./components/routes/system.tsx"
import { updateFavicon } from "@/lib/utils"
import Navbar from "./components/navbar.tsx"
import { I18nProvider } from "@lingui/react"
import { i18n } from "@lingui/core"
import { getLocale, dynamicActivate } from "./lib/i18n"
import { SystemStatus } from "./lib/enums"
import { alertManager } from "./lib/alerts"
import Settings from "./components/routes/settings/layout.tsx"
// const ServerDetail = lazy(() => import('./components/routes/system.tsx'))
const LoginPage = lazy(() => import("./components/login/login.tsx"))
const CopyToClipboardDialog = lazy(() => import("./components/copy-to-clipboard.tsx"))
const Settings = lazy(() => import("./components/routes/settings/layout.tsx"))
const LoginPage = lazy(() => import("@/components/login/login.tsx"))
const Home = lazy(() => import("@/components/routes/home.tsx"))
const SystemDetail = lazy(() => import("@/components/routes/system.tsx"))
const CopyToClipboardDialog = lazy(() => import("@/components/copy-to-clipboard.tsx"))
const App = memo(() => {
const page = useStore($router)
@@ -78,11 +78,7 @@ const App = memo(() => {
} else if (page.route === "system") {
return <SystemDetail name={page.params.name} />
} else if (page.route === "settings") {
return (
<Suspense>
<Settings />
</Suspense>
)
return <Settings />
}
})
@@ -106,7 +102,7 @@ const Layout = () => {
<div className="container">
<Navbar />
</div>
<div className="container mb-14 relative">
<div className="container relative">
<App />
{copyContent && (
<Suspense>

View File

@@ -29,6 +29,7 @@ export interface SystemRecord extends RecordModel {
port: string
info: SystemInfo
v: string
updated: string
}
export interface SystemInfo {
@@ -98,6 +99,8 @@ export interface SystemStats {
mp: number
/** memory buffer + cache (gb) */
mb: number
/** max used memory (gb) */
mm?: number
/** zfs arc memory (gb) */
mz?: number
/** swap space (gb) */

View File

@@ -3,7 +3,7 @@ package beszel
import "github.com/blang/semver"
const (
Version = "0.12.4"
Version = "0.12.6"
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

View File

@@ -1,3 +1,27 @@
## 0.12.6
- Add maximum 1 minute memory usage.
- Add status filters to All Systems table.
- Virtualize All Systems table to improve performance with hundreds of systems. (#1100)
- Fix Safari system link CSS bug.
- Use older cuda image for increased compatibility (#1103)
- Truncate long system names in All Systems table. (#1104)
- Fix update mirror and add `--china-mirrors` flag. (#1035)
## 0.12.5
- Downgrade `gopsutil` to `v4.25.6` to fix panic on FreeBSD (#1083)
- Exclude FreeBSD from battery charge monitoring to fix deadlock. (#1081)
- Minor hub UI improvements.
## 0.12.4
- Add battery charge monitoring.